")
diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm
index 261ff54fee5ef..98691e99546ef 100644
--- a/code/__DEFINES/colors.dm
+++ b/code/__DEFINES/colors.dm
@@ -12,9 +12,6 @@
///how many colour priority levels there are.
#define COLOUR_PRIORITY_AMOUNT 4
-#define COLOR_INPUT_DISABLED "#F0F0F0"
-#define COLOR_INPUT_ENABLED "#D3B5B5"
-
#define COLOR_DARKMODE_BACKGROUND "#202020"
#define COLOR_DARKMODE_DARKBACKGROUND "#171717"
#define COLOR_DARKMODE_TEXT "#a4bad6"
diff --git a/code/__DEFINES/dcs/signals/signals_action.dm b/code/__DEFINES/dcs/signals/signals_action.dm
index a30fb460e7983..ee98b5a4eb9a8 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_assembly.dm b/code/__DEFINES/dcs/signals/signals_assembly.dm
new file mode 100644
index 0000000000000..a1330c0965e9e
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_assembly.dm
@@ -0,0 +1,2 @@
+//called when an igniter activates
+#define COMSIG_IGNITER_ACTIVATE "igniter_activate"
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm
index 653d4e6167bd9..6d946d09f88ba 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm
@@ -45,6 +45,8 @@
#define COMSIG_ATOM_SMOOTHED_ICON "atom_smoothed_icon"
///from base of atom/Entered(): (atom/movable/arrived, atom/old_loc, list/atom/old_locs)
#define COMSIG_ATOM_ENTERED "atom_entered"
+///from base of atom/movable/Moved(): (atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+#define COMSIG_ATOM_ABSTRACT_ENTERED "atom_abstract_entered"
/// Sent from the atom that just Entered src. From base of atom/Entered(): (/atom/destination, atom/old_loc, list/atom/old_locs)
#define COMSIG_ATOM_ENTERING "atom_entering"
///from base of atom/Exit(): (/atom/movable/leaving, direction)
@@ -52,6 +54,8 @@
#define COMPONENT_ATOM_BLOCK_EXIT (1<<0)
///from base of atom/Exited(): (atom/movable/gone, direction)
#define COMSIG_ATOM_EXITED "atom_exited"
+///from base of atom/movable/Moved(): (atom/movable/gone, direction)
+#define COMSIG_ATOM_ABSTRACT_EXITED "atom_abstract_exited"
///from base of atom/Bumped(): (/atom/movable)
#define COMSIG_ATOM_BUMPED "atom_bumped"
///from base of atom/handle_atom_del(): (atom/deleted)
diff --git a/code/__DEFINES/dcs/signals/signals_beam.dm b/code/__DEFINES/dcs/signals/signals_beam.dm
new file mode 100644
index 0000000000000..edfbc7c437135
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_beam.dm
@@ -0,0 +1,3 @@
+/// Called before beam is redrawn
+#define COMSIG_BEAM_BEFORE_DRAW "beam_before_draw"
+ #define BEAM_CANCEL_DRAW (1 << 0)
diff --git a/code/__DEFINES/dcs/signals/signals_fish.dm b/code/__DEFINES/dcs/signals/signals_fish.dm
index d40d6f3e59514..89e25d851362d 100644
--- a/code/__DEFINES/dcs/signals/signals_fish.dm
+++ b/code/__DEFINES/dcs/signals/signals_fish.dm
@@ -5,3 +5,15 @@
// Fish signals
#define COMSIG_FISH_STATUS_CHANGED "fish_status_changed"
#define COMSIG_FISH_STIRRED "fish_stirred"
+
+/// Fishing challenge completed
+#define COMSIG_FISHING_CHALLENGE_COMPLETED "fishing_completed"
+/// Called when you try to use fishing rod on anything
+#define COMSIG_PRE_FISHING "pre_fishing"
+
+/// Sent by the target of the fishing rod cast
+#define COMSIG_FISHING_ROD_CAST "fishing_rod_cast"
+ #define FISHING_ROD_CAST_HANDLED (1 << 0)
+
+/// Sent when fishing line is snapped
+#define COMSIG_FISHING_LINE_SNAPPED "fishing_line_interrupted"
diff --git a/code/__DEFINES/dcs/signals/signals_global.dm b/code/__DEFINES/dcs/signals/signals_global.dm
index 8182700828390..17ff3893d21e2 100644
--- a/code/__DEFINES/dcs/signals/signals_global.dm
+++ b/code/__DEFINES/dcs/signals/signals_global.dm
@@ -39,6 +39,10 @@
#define COMSIG_GLOB_PRE_RANDOM_EVENT "!pre_random_event"
/// Do not allow this random event to continue.
#define CANCEL_PRE_RANDOM_EVENT (1<<0)
+/// Called by (/datum/round_event_control/RunEvent).
+#define COMSIG_GLOB_RANDOM_EVENT "!random_event"
+ /// Do not allow this random event to continue.
+ #define CANCEL_RANDOM_EVENT (1<<0)
/// a person somewhere has thrown something : (mob/living/carbon/carbon_thrower, target)
#define COMSIG_GLOB_CARBON_THROW_THING "!throw_thing"
/// a trapdoor remote has sent out a signal to link with a trapdoor
diff --git a/code/__DEFINES/dcs/signals/signals_heretic.dm b/code/__DEFINES/dcs/signals/signals_heretic.dm
index 3a5f68fb94823..a3be544fec651 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 e7e1a0bf7a720..0000000000000
--- 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 ff46eb8131f89..1b92c35488c3a 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 eb95a016d78c9..088422b345c29 100644
--- a/code/__DEFINES/dcs/signals/signals_object.dm
+++ b/code/__DEFINES/dcs/signals/signals_object.dm
@@ -59,6 +59,9 @@
/// from /obj/machinery/atmospherics/components/unary/cryo_cell/set_on(bool): (on)
#define COMSIG_CRYO_SET_ON "cryo_set_on"
+/// from /obj/proc/make_unfrozen()
+#define COMSIG_OBJ_UNFREEZE "obj_unfreeze"
+
// /obj/machinery/atmospherics/components/binary/valve signals
/// from /obj/machinery/atmospherics/components/binary/valve/toggle(): (on)
@@ -116,6 +119,12 @@
#define COMSIG_ITEM_DROPPED "item_drop"
///from base of obj/item/pickup(): (/mob/taker)
#define COMSIG_ITEM_PICKUP "item_pickup"
+
+/// Sebt from obj/item/ui_action_click(): (mob/user, datum/action)
+#define COMSIG_ITEM_UI_ACTION_CLICK "item_action_click"
+ /// Return to prevent the default behavior (attack_selfing) from ocurring.
+ #define COMPONENT_ACTION_HANDLED (1<<0)
+
///from base of mob/living/carbon/attacked_by(): (mob/living/carbon/target, mob/living/user, hit_zone)
#define COMSIG_ITEM_ATTACK_ZONE "item_attack_zone"
///from base of obj/item/hit_reaction(): (list/args)
@@ -222,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(): ()
@@ -306,31 +313,6 @@
//called in /obj/item/organ/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)
-
// /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 0000000000000..4d2c7d2993fca
--- /dev/null
+++ b/code/__DEFINES/dcs/signals/signals_spell.dm
@@ -0,0 +1,93 @@
+// Signals sent to or by spells
+
+// Generic spell signals
+
+
+/// Sent from /datum/action/cooldown/spell/before_cast() to the caster: (datum/action/cooldown/spell/spell, atom/cast_on)
+#define COMSIG_MOB_BEFORE_SPELL_CAST "mob_spell_pre_cast"
+/// Sent from /datum/action/cooldown/spell/before_cast() to the spell: (atom/cast_on)
+#define COMSIG_SPELL_BEFORE_CAST "spell_pre_cast"
+ /// Return to prevent the spell cast from continuing.
+ #define SPELL_CANCEL_CAST (1 << 0)
+ /// Return from before cast signals to prevent the spell from giving off sound or invocation.
+ #define SPELL_NO_FEEDBACK (1 << 1)
+ /// Return from before cast signals to prevent the spell from going on cooldown before aftercast.
+ #define SPELL_NO_IMMEDIATE_COOLDOWN (1 << 2)
+
+/// Sent from /datum/action/cooldown/spell/set_click_ability() to the caster: (datum/action/cooldown/spell/spell)
+#define COMSIG_MOB_SPELL_ACTIVATED "mob_spell_active"
+ /// Same as spell_cancel_cast, as they're able to be used interchangeably
+ #define SPELL_CANCEL_ACTIVATION SPELL_CANCEL_CAST
+
+/// Sent from /datum/action/cooldown/spell/cast() to the caster: (datum/action/cooldown/spell/spell, atom/cast_on)
+#define COMSIG_MOB_CAST_SPELL "mob_cast_spell"
+/// Sent from /datum/action/cooldown/spell/cast() to the spell: (atom/cast_on)
+#define COMSIG_SPELL_CAST "spell_cast"
+// Sent from /datum/action/cooldown/spell/after_cast() to the caster: (datum/action/cooldown/spell/spell, atom/cast_on)
+#define COMSIG_MOB_AFTER_SPELL_CAST "mob_after_spell_cast"
+/// Sent from /datum/action/cooldown/spell/after_cast() to the spell: (atom/cast_on)
+#define COMSIG_SPELL_AFTER_CAST "spell_after_cast"
+/// Sent from /datum/action/cooldown/spell/reset_spell_cooldown() to the spell: ()
+#define COMSIG_SPELL_CAST_RESET "spell_cast_reset"
+
+// Spell type signals
+
+// Pointed projectiles
+/// Sent from /datum/action/cooldown/spell/pointed/projectile/on_cast_hit: (atom/hit, atom/firer, obj/projectile/source)
+#define COMSIG_SPELL_PROJECTILE_HIT "spell_projectile_hit"
+
+// AOE spells
+/// Sent from /datum/action/cooldown/spell/aoe/cast: (list/atoms_affected, atom/caster)
+#define COMSIG_SPELL_AOE_ON_CAST "spell_aoe_cast"
+
+// Cone spells
+/// Sent from /datum/action/cooldown/spell/cone/cast: (list/atoms_affected, atom/caster)
+#define COMSIG_SPELL_CONE_ON_CAST "spell_cone_cast"
+/// Sent from /datum/action/cooldown/spell/cone/do_cone_effects: (list/atoms_affected, atom/caster, level)
+#define COMSIG_SPELL_CONE_ON_LAYER_EFFECT "spell_cone_cast_effect"
+
+// Touch spells
+/// Sent from /datum/action/cooldown/spell/touch/do_hand_hit: (atom/hit, mob/living/carbon/caster, obj/item/melee/touch_attack/hand)
+#define COMSIG_SPELL_TOUCH_HAND_HIT "spell_touch_hand_cast"
+
+// Jaunt Spells
+/// Sent from datum/action/cooldown/spell/jaunt/enter_jaunt, to the mob jaunting: (obj/effect/dummy/phased_mob/jaunt, datum/action/cooldown/spell/spell)
+#define COMSIG_MOB_ENTER_JAUNT "spell_mob_enter_jaunt"
+/// Sent from datum/action/cooldown/spell/jaunt/exit_jaunt, after the mob exited jaunt: (datum/action/cooldown/spell/spell)
+#define COMSIG_MOB_AFTER_EXIT_JAUNT "spell_mob_after_exit_jaunt"
+
+/// Sent from/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/try_enter_jaunt,
+/// to any unconscious / critical mobs being dragged when the jaunter enters blood:
+/// (datum/action/cooldown/spell/jaunt/bloodcrawl/crawl, mob/living/jaunter, obj/effect/decal/cleanable/blood)
+#define COMSIG_LIVING_BLOOD_CRAWL_PRE_CONSUMED "living_pre_consumed_by_bloodcrawl"
+/// Sent from/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/consume_victim,
+/// to the victim being consumed by the slaughter demon.
+/// (datum/action/cooldown/spell/jaunt/bloodcrawl/crawl, mob/living/jaunter)
+#define COMSIG_LIVING_BLOOD_CRAWL_CONSUMED "living_consumed_by_bloodcrawl"
+ /// Return at any point to stop the bloodcrawl "consume" process from continuing.
+ #define COMPONENT_STOP_CONSUMPTION (1 << 0)
+
+// Signals for specific spells
+
+// Lichdom
+/// Sent from /datum/action/cooldown/spell/lichdom/cast(), to the item being imbued: (datum/action/cooldown/spell/spell, mob/user)
+#define COMSIG_ITEM_IMBUE_SOUL "item_imbue_soul"
+ /// Return to stop the cast and prevent the soul imbue
+ #define COMPONENT_BLOCK_IMBUE (1 << 0)
+
+/// Sent from /datum/action/cooldown/spell/aoe/knock/cast(), to every nearby turf (for connect loc): (datum/action/cooldown/spell/aoe/knock/spell, mob/living/caster)
+#define COMSIG_ATOM_MAGICALLY_UNLOCKED "atom_magic_unlock"
+
+// Instant Summons
+/// Sent from /datum/action/cooldown/spell/summonitem/cast(), to the item being marked for recall: (datum/action/cooldown/spell/spell, mob/user)
+#define COMSIG_ITEM_MARK_RETRIEVAL "item_mark_retrieval"
+ /// Return to stop the cast and prevent the item from being marked
+ #define COMPONENT_BLOCK_MARK_RETRIEVAL (1 << 0)
+
+// Charge
+/// Sent from /datum/action/cooldown/spell/charge/cast(), to the item in hand being charged: (datum/action/cooldown/spell/spell, mob/user)
+#define COMSIG_ITEM_MAGICALLY_CHARGED "item_magic_charged"
+ /// Return if an item was successfuly recharged
+ #define COMPONENT_ITEM_CHARGED (1 << 0)
+ /// Return if the item had a negative side effect occur while recharging
+ #define COMPONENT_ITEM_BURNT_OUT (1 << 1)
diff --git a/code/__DEFINES/exosuit_fab.dm b/code/__DEFINES/exosuit_fab.dm
index 4d4059e6a8af5..fa0ee64ef534b 100644
--- a/code/__DEFINES/exosuit_fab.dm
+++ b/code/__DEFINES/exosuit_fab.dm
@@ -15,21 +15,23 @@
#define EXOSUIT_MODULE_ODYSSEUS (1<<1)
/// Module is compatible with Clarke Exosuit models
#define EXOSUIT_MODULE_CLARKE (1<<2)
+/// Module is compatible with a mech carrying an empty Concealed Weapon Bay
+#define EXOSUIT_MODULE_CONCEALED_WEP_BAY (1<<3)
/// Module is compatible with Gygax Exosuit models
-#define EXOSUIT_MODULE_GYGAX (1<<3)
+#define EXOSUIT_MODULE_GYGAX (1<<4)
/// Module is compatible with Durand Exosuit models
-#define EXOSUIT_MODULE_DURAND (1<<4)
+#define EXOSUIT_MODULE_DURAND (1<<5)
/// Module is compatible with H.O.N.K Exosuit models
-#define EXOSUIT_MODULE_HONK (1<<5)
+#define EXOSUIT_MODULE_HONK (1<<6)
/// Module is compatible with Phazon Exosuit models
-#define EXOSUIT_MODULE_PHAZON (1<<6)
+#define EXOSUIT_MODULE_PHAZON (1<<7)
/// Module is compatible with Savannah Exosuit models
-#define EXOSUIT_MODULE_SAVANNAH (1<<7)
+#define EXOSUIT_MODULE_SAVANNAH (1<<8)
/// Module is compatible with "Working" Exosuit models - Ripley and Clarke
#define EXOSUIT_MODULE_WORKING EXOSUIT_MODULE_RIPLEY | EXOSUIT_MODULE_CLARKE
-/// Module is compatible with "Combat" Exosuit models - Gygax, H.O.N.K, Durand and Phazon
-#define EXOSUIT_MODULE_COMBAT EXOSUIT_MODULE_GYGAX | EXOSUIT_MODULE_HONK | EXOSUIT_MODULE_DURAND | EXOSUIT_MODULE_PHAZON | EXOSUIT_MODULE_SAVANNAH
+/// Module is compatible with "Combat" Exosuit models - Gygax, H.O.N.K, Durand and Phazon, or any Exosuit with an empty Concealed Weapon Bay
+#define EXOSUIT_MODULE_COMBAT EXOSUIT_MODULE_GYGAX | EXOSUIT_MODULE_HONK | EXOSUIT_MODULE_DURAND | EXOSUIT_MODULE_PHAZON | EXOSUIT_MODULE_SAVANNAH | EXOSUIT_MODULE_CONCEALED_WEP_BAY
/// Module is compatible with "Medical" Exosuit modelsm - Odysseus
#define EXOSUIT_MODULE_MEDICAL EXOSUIT_MODULE_ODYSSEUS
diff --git a/code/__DEFINES/fishing.dm b/code/__DEFINES/fishing.dm
new file mode 100644
index 0000000000000..40b4d21a9e8bf
--- /dev/null
+++ b/code/__DEFINES/fishing.dm
@@ -0,0 +1,41 @@
+/// Use in fish tables to denote miss chance.
+#define FISHING_DUD "dud"
+
+#define FISHING_BAIT_TRAIT "fishing_bait"
+#define BASIC_QUALITY_BAIT_TRAIT "removes_felinids_pr"
+#define GOOD_QUALITY_BAIT_TRAIT "adds_bitcoin_miner_pr"
+#define GREAT_QUALITY_BAIT_TRAIT "perspective_walls_pr"
+
+// Baseline fishing difficulty levels
+#define FISHING_DEFAULT_DIFFICULTY 15
+
+/// Difficulty modifier when bait is fish's favorite
+#define FAV_BAIT_DIFFICULTY_MOD -5
+/// Difficulty modifier when bait is fish's disliked
+#define DISLIKED_BAIT_DIFFICULTY_MOD 15
+
+
+#define FISH_TRAIT_MINOR_DIFFICULTY_BOOST 5
+
+// These define how the fish will behave in the minigame
+#define FISH_AI_DUMB "dumb"
+#define FISH_AI_ZIPPY "zippy"
+#define FISH_AI_SLOW "slow"
+
+#define ADDITIVE_FISHING_MOD "additive"
+#define MULTIPLICATIVE_FISHING_MOD "multiplicative"
+
+#define FISHING_HOOK_MAGNETIC (1 << 0)
+#define FISHING_HOOK_SHINY (1 << 1)
+#define FISHING_HOOK_WEIGHTED (1 << 2)
+
+#define FISHING_LINE_CLOAKED (1 << 0)
+#define FISHING_LINE_REINFORCED (1 << 1)
+#define FISHING_LINE_BOUNCY (1 << 2)
+
+#define FISHING_SPOT_PRESET_BEACH "beach"
+#define FISHING_SPOT_PRESET_LAVALAND_LAVA "lavaland lava"
+
+#define FISHING_MINIGAME_RULE_HEAVY_FISH "heavy"
+#define FISHING_MINIGAME_RULE_WEIGHTED_BAIT "weighted"
+#define FISHING_MINIGAME_RULE_LIMIT_LOSS "limit_loss"
diff --git a/code/__DEFINES/flora.dm b/code/__DEFINES/flora.dm
index 1b0d1df72799e..e02f492cce77f 100644
--- a/code/__DEFINES/flora.dm
+++ b/code/__DEFINES/flora.dm
@@ -1,2 +1,3 @@
-#define FLORA_HARVEST_WOOD_TOOLS list(/obj/item/hatchet, /obj/item/fireaxe, /obj/item/melee/energy/sword/saber, /obj/item/melee/energy/blade)
+///Tools that can harvest wood that DONT have the TOOL_SAW flag; TOOL_SAW can harvest regardless of being in this list (Dont put them in here)
+#define FLORA_HARVEST_WOOD_TOOLS list(/obj/item/hatchet, /obj/item/fireaxe, /obj/item/kinetic_crusher, /obj/item/melee/energy/axe)
#define FLORA_HARVEST_STONE_TOOLS list(/obj/item/pickaxe)
diff --git a/code/__DEFINES/food.dm b/code/__DEFINES/food.dm
index 99e1e7aab49fe..74086bac9b287 100644
--- a/code/__DEFINES/food.dm
+++ b/code/__DEFINES/food.dm
@@ -41,6 +41,29 @@
"BUGS", \
)
+/// IC meaning (more or less) for food flags
+#define FOOD_FLAGS_IC list( \
+ "Meat", \
+ "Vegetables", \
+ "Raw food", \
+ "Junk food", \
+ "Grain", \
+ "Fruits", \
+ "Dairy products", \
+ "Fried food", \
+ "Alcohol", \
+ "Sugary food", \
+ "Gross food", \
+ "Toxic food", \
+ "Pineapples", \
+ "Breakfast food", \
+ "Clothing", \
+ "Nuts", \
+ "Seafood", \
+ "Oranges", \
+ "Bugs", \
+)
+
#define DRINK_NICE 1
#define DRINK_GOOD 2
#define DRINK_VERYGOOD 3
diff --git a/code/__DEFINES/important_recursive_contents.dm b/code/__DEFINES/important_recursive_contents.dm
index 24cca713c652f..79abb67d18365 100644
--- a/code/__DEFINES/important_recursive_contents.dm
+++ b/code/__DEFINES/important_recursive_contents.dm
@@ -5,3 +5,5 @@
///the client mobs channel of the important_recursive_contents list, everything in here will be a mob with an attached client
///this is given to both a clients mob, and a clients eye, both point to the clients mob
#define RECURSIVE_CONTENTS_CLIENT_MOBS "recursive_contents_client_mobs"
+///the parent of storage components currently shown to some client mob get this. gets removed when nothing is viewing the parent
+#define RECURSIVE_CONTENTS_ACTIVE_STORAGE "recursive_contents_active_storage"
diff --git a/code/__DEFINES/industrial_lift.dm b/code/__DEFINES/industrial_lift.dm
new file mode 100644
index 0000000000000..9d3052c01f040
--- /dev/null
+++ b/code/__DEFINES/industrial_lift.dm
@@ -0,0 +1,20 @@
+//Booleans in arguments are confusing, so I made them defines.
+///the lift's controls are currently locked from user input
+#define LIFT_PLATFORM_LOCKED 1
+///the lift's controls are currently unlocked so user's can direct it
+#define LIFT_PLATFORM_UNLOCKED 0
+
+//lift_id's
+///basic lift_id, goes up and down
+#define BASIC_LIFT_ID "base"
+///tram lift_id, goes left and right or north and south. maybe one day be able to turn and go up/down as well
+#define TRAM_LIFT_ID "tram"
+///debug lift_id
+#define DEBUG_LIFT_ID "debug"
+
+
+//specific_lift_id's
+///the specific_lift_id of the main station tram landmark for tramstation that spawns roundstart.
+#define MAIN_STATION_TRAM "main station tram"
+///the specific_lift_id of the tram on the hilbert research station
+#define HILBERT_TRAM "tram_hilbert"
diff --git a/code/__DEFINES/is_helpers.dm b/code/__DEFINES/is_helpers.dm
index 24db52be639f6..43466bf71b7b6 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/keybinding.dm b/code/__DEFINES/keybinding.dm
index 2fc4f9358092c..dbc8ef85b17c1 100644
--- a/code/__DEFINES/keybinding.dm
+++ b/code/__DEFINES/keybinding.dm
@@ -29,9 +29,11 @@
#define COMSIG_KB_CLIENT_MINIMALHUD_DOWN "keybinding_client_minimalhud_down"
//Communication
-#define COMSIG_KB_CLIENT_OOC_DOWN "keybinding_client_ooc_down"
+
#define COMSIG_KB_CLIENT_SAY_DOWN "keybinding_client_say_down"
+#define COMSIG_KB_CLIENT_RADIO_DOWN "keybinding_client_radio_down"
#define COMSIG_KB_CLIENT_ME_DOWN "keybinding_client_me_down"
+#define COMSIG_KB_CLIENT_OOC_DOWN "keybinding_client_ooc_down"
//Human
#define COMSIG_KB_HUMAN_QUICKEQUIP_DOWN "keybinding_human_quickequip_down"
diff --git a/code/__DEFINES/language.dm b/code/__DEFINES/language.dm
index c475a4308295e..8abd951e0f038 100644
--- a/code/__DEFINES/language.dm
+++ b/code/__DEFINES/language.dm
@@ -21,3 +21,4 @@
#define LANGUAGE_SOFTWARE "software"
#define LANGUAGE_STONER "stoner"
#define LANGUAGE_VOICECHANGE "voicechange"
+#define LANGUAGE_RADIOKEY "radiokey"
diff --git a/code/__DEFINES/layers.dm b/code/__DEFINES/layers.dm
index 8215d824eda2d..ff7fa86120c4e 100644
--- a/code/__DEFINES/layers.dm
+++ b/code/__DEFINES/layers.dm
@@ -56,6 +56,7 @@
#define GAS_PIPE_VISIBLE_LAYER 2.47 //layer = initial(layer) + piping_layer / 1000 in atmospherics/update_icon() to determine order of pipe overlap
#define GAS_FILTER_LAYER 2.48
#define GAS_PUMP_LAYER 2.49
+#define PLUMBING_PIPE_VISIBILE_LAYER 2.495//layer = initial(layer) + ducting_layer / 3333 in atmospherics/handle_layer() to determine order of duct overlap
#define LOW_OBJ_LAYER 2.5
///catwalk overlay of /turf/open/floor/plating/catwalk_floor
#define CATWALK_LAYER 2.51
@@ -69,7 +70,8 @@
#define BELOW_OPEN_DOOR_LAYER 2.6
#define BLASTDOOR_LAYER 2.65
#define OPEN_DOOR_LAYER 2.7
-#define DOOR_HELPER_LAYER 2.71 //keep this above OPEN_DOOR_LAYER
+#define DOOR_ACCESS_HELPER_LAYER 2.71 //keep this above OPEN_DOOR_LAYER, special layer used for /obj/effect/mapping_helpers/airlock/access
+#define DOOR_HELPER_LAYER 2.72 //keep this above DOOR_ACCESS_HELPER_LAYER and OPEN_DOOR_LAYER since the others tend to have tiny sprites that tend to be covered up.
#define PROJECTILE_HIT_THRESHHOLD_LAYER 2.75 //projectiles won't hit objects at or below this layer if possible
#define TABLE_LAYER 2.8
#define GATEWAY_UNDERLAY_LAYER 2.85
@@ -185,6 +187,8 @@
#define RUNECHAT_PLANE 501
/// Plane for balloon text (text that fades up)
#define BALLOON_CHAT_PLANE 502
+/// Bubble for typing indicators
+#define TYPING_LAYER 500
//-------------------- HUD ---------------------
//HUD layer defines
diff --git a/code/__DEFINES/logging.dm b/code/__DEFINES/logging.dm
index 9dae55695567d..bfe6a591c724c 100644
--- a/code/__DEFINES/logging.dm
+++ b/code/__DEFINES/logging.dm
@@ -38,15 +38,16 @@
#define LOG_ECON (1 << 18)
#define LOG_VICTIM (1 << 19)
#define LOG_RADIO_EMOTE (1 << 20)
+#define LOG_SPEECH_INDICATORS (1 << 21)
//Individual logging panel pages
#define INDIVIDUAL_ATTACK_LOG (LOG_ATTACK | LOG_VICTIM)
-#define INDIVIDUAL_SAY_LOG (LOG_SAY | LOG_WHISPER | LOG_DSAY)
+#define INDIVIDUAL_SAY_LOG (LOG_SAY | LOG_WHISPER | LOG_DSAY | LOG_SPEECH_INDICATORS)
#define INDIVIDUAL_EMOTE_LOG (LOG_EMOTE | LOG_RADIO_EMOTE)
#define INDIVIDUAL_COMMS_LOG (LOG_PDA | LOG_CHAT | LOG_COMMENT | LOG_TELECOMMS)
#define INDIVIDUAL_OOC_LOG (LOG_OOC | LOG_ADMIN)
#define INDIVIDUAL_OWNERSHIP_LOG (LOG_OWNERSHIP)
-#define INDIVIDUAL_SHOW_ALL_LOG (LOG_ATTACK | LOG_SAY | LOG_WHISPER | LOG_EMOTE | LOG_RADIO_EMOTE | LOG_DSAY | LOG_PDA | LOG_CHAT | LOG_COMMENT | LOG_TELECOMMS | LOG_OOC | LOG_ADMIN | LOG_OWNERSHIP | LOG_GAME | LOG_ADMIN_PRIVATE | LOG_ASAY | LOG_MECHA | LOG_VIRUS | LOG_SHUTTLE | LOG_ECON | LOG_VICTIM)
+#define INDIVIDUAL_SHOW_ALL_LOG (LOG_ATTACK | LOG_SAY | LOG_WHISPER | LOG_EMOTE | LOG_RADIO_EMOTE | LOG_DSAY | LOG_PDA | LOG_CHAT | LOG_COMMENT | LOG_TELECOMMS | LOG_OOC | LOG_ADMIN | LOG_OWNERSHIP | LOG_GAME | LOG_ADMIN_PRIVATE | LOG_ASAY | LOG_MECHA | LOG_VIRUS | LOG_SHUTTLE | LOG_ECON | LOG_VICTIM | LOG_SPEECH_INDICATORS)
#define LOGSRC_CKEY "Ckey"
#define LOGSRC_MOB "Mob"
diff --git a/code/__DEFINES/magic.dm b/code/__DEFINES/magic.dm
index f9669db1c3a11..d4b10d7aa195b 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/memory_defines.dm b/code/__DEFINES/memory_defines.dm
index 9eeb823b431b5..30de201dc58b6 100644
--- a/code/__DEFINES/memory_defines.dm
+++ b/code/__DEFINES/memory_defines.dm
@@ -102,6 +102,8 @@
#define MEMORY_PLAYING_52_PICKUP "playing_52_pickup"
/// A memory of playing cards with others
#define MEMORY_PLAYING_CARDS "playing_cards"
+/// A memory of playing russian roulette
+#define MEMORY_RUSSIAN_ROULETTE "russian_roulette"
/**
@@ -163,4 +165,7 @@
#define DETAIL_DEALER "DEALER"
#define DETAIL_HELD_CARD_ITEM "HELD_CARD_ITEM" // could either be a singlecard, cardhand, or a deck
-
+// Russian Roulette
+#define DETAIL_LOADED_ROUNDS "LOADED_ROUNDS"
+#define DETAIL_BODYPART "BODYPART"
+#define DETAIL_OUTCOME "OUTCOME"
diff --git a/code/__DEFINES/mobs.dm b/code/__DEFINES/mobs.dm
index a0a3045dedb88..553aaf8ab6d85 100644
--- a/code/__DEFINES/mobs.dm
+++ b/code/__DEFINES/mobs.dm
@@ -410,6 +410,8 @@
#define HUNGER_FACTOR 0.05 //factor at which mob nutrition decreases
#define ETHEREAL_CHARGE_FACTOR 0.8 //factor at which ethereal's charge decreases per second
+/// How much nutrition eating clothes as moth gives and drains
+#define CLOTHING_NUTRITION_GAIN 15
#define REAGENTS_METABOLISM 0.2 //How many units of reagent are consumed per second, by default.
#define REAGENTS_EFFECT_MULTIPLIER (REAGENTS_METABOLISM / 0.4) // By defining the effect multiplier this way, it'll exactly adjust all effects according to how they originally were with the 0.4 metabolism
diff --git a/code/__DEFINES/obj_flags.dm b/code/__DEFINES/obj_flags.dm
index 5f32ab692084a..26eaec3b14f9b 100644
--- a/code/__DEFINES/obj_flags.dm
+++ b/code/__DEFINES/obj_flags.dm
@@ -9,7 +9,6 @@
#define ON_BLUEPRINTS (1<<5) //Are we visible on the station blueprints at roundstart?
#define UNIQUE_RENAME (1<<6) // can you customize the description/name of the thing?
#define USES_TGUI (1<<7) //put on things that use tgui on ui_interact instead of custom/old UI.
-#define FROZEN (1<<8)
#define BLOCK_Z_OUT_DOWN (1<<9) // Should this object block z falling from loc?
#define BLOCK_Z_OUT_UP (1<<10) // Should this object block z uprise from loc?
#define BLOCK_Z_IN_DOWN (1<<11) // Should this object block z falling from above?
diff --git a/code/__DEFINES/plumbing.dm b/code/__DEFINES/plumbing.dm
index 9666a5be1e16e..39277fb1f733a 100644
--- a/code/__DEFINES/plumbing.dm
+++ b/code/__DEFINES/plumbing.dm
@@ -1,9 +1,35 @@
-#define FIRST_DUCT_LAYER 1
-#define SECOND_DUCT_LAYER 2
-#define THIRD_DUCT_LAYER 4
-#define FOURTH_DUCT_LAYER 8
-#define FIFTH_DUCT_LAYER 16
+#define FIRST_DUCT_LAYER (1<<0)
+#define SECOND_DUCT_LAYER (1<<1)
+#define THIRD_DUCT_LAYER (1<<2)
+#define FOURTH_DUCT_LAYER (1<<3)
+#define FIFTH_DUCT_LAYER (1<<4)
#define DUCT_LAYER_DEFAULT THIRD_DUCT_LAYER
#define MACHINE_REAGENT_TRANSFER 10 //the default max plumbing machinery transfers
+
+/// List of plumbing layers as name => bitflag
+GLOBAL_LIST_INIT(plumbing_layers, list(
+ "First Layer" = FIRST_DUCT_LAYER,
+ "Second Layer" = SECOND_DUCT_LAYER,
+ "Default Layer" = THIRD_DUCT_LAYER,
+ "Fourth Layer" = FOURTH_DUCT_LAYER,
+ "Fifth Layer" = FIFTH_DUCT_LAYER,
+))
+
+/// Reverse of plumbing_layers, as "[bitflag]" => name
+GLOBAL_LIST_INIT(plumbing_layer_names, list(
+ "[FIRST_DUCT_LAYER]" = "First Layer",
+ "[SECOND_DUCT_LAYER]" = "Second Layer",
+ "[THIRD_DUCT_LAYER]" = "Default Layer",
+ "[FOURTH_DUCT_LAYER]" = "Fourth Layer",
+ "[FIFTH_DUCT_LAYER]" = "Fifth Layer",
+))
+
+/// Name of omni color
+#define DUCT_COLOR_OMNI "omni"
+
+/// Cached radial menu options for plumbing RCD color picker
+GLOBAL_LIST_EMPTY(plumbing_color_menu_options)
+/// Cached radial menu options for plumbing RCD layer picker
+GLOBAL_LIST_EMPTY(plumbing_layer_menu_options)
diff --git a/code/__DEFINES/power.dm b/code/__DEFINES/power.dm
index ec8888c43a0ff..ff298ca9f3908 100644
--- a/code/__DEFINES/power.dm
+++ b/code/__DEFINES/power.dm
@@ -1,6 +1,6 @@
-#define CABLE_LAYER_1 1
-#define CABLE_LAYER_2 2
-#define CABLE_LAYER_3 4
+#define CABLE_LAYER_1 (1<<0)
+#define CABLE_LAYER_2 (1<<1)
+#define CABLE_LAYER_3 (1<<2)
#define MACHINERY_LAYER_1 1
diff --git a/code/__DEFINES/reactions.dm b/code/__DEFINES/reactions.dm
index 91889daaa4515..8d367cea1850d 100644
--- a/code/__DEFINES/reactions.dm
+++ b/code/__DEFINES/reactions.dm
@@ -138,8 +138,8 @@
// BZ:
/// The maximum temperature BZ can form at. Deliberately set lower than the minimum burn temperature for most combustible gases in an attempt to prevent long fuse singlecaps.
#define BZ_FORMATION_MAX_TEMPERATURE (FIRE_MINIMUM_TEMPERATURE_TO_EXIST - 60) // Yes, someone used this as a bomb timer. I hate players.
-/// The amount of energy ~2.5 moles of BZ forming from N2O and plasma releases.
-#define BZ_FORMATION_ENERGY FIRE_CARBON_ENERGY_RELEASED
+/// The amount of energy 1 mole of BZ forming from N2O and plasma releases.
+#define BZ_FORMATION_ENERGY 80000
// Pluoxium:
/// The minimum temperature pluoxium can form from carbon dioxide, oxygen, and tritium at.
diff --git a/code/__DEFINES/research/anomalies.dm b/code/__DEFINES/research/anomalies.dm
index 358fa474f98b4..35ee0ec05ffb0 100644
--- a/code/__DEFINES/research/anomalies.dm
+++ b/code/__DEFINES/research/anomalies.dm
@@ -5,7 +5,7 @@
#define MAX_CORES_VORTEX 8
#define MAX_CORES_PYRO 8
#define MAX_CORES_HALLUCINATION 8
-#define MAX_CORES_DELIMBER 8
+#define MAX_CORES_BIOSCRAMBLER 8
///Defines for the different types of explosion a flux anomaly can have
#define FLUX_NO_EXPLOSION 0
diff --git a/code/__DEFINES/speech_channels.dm b/code/__DEFINES/speech_channels.dm
new file mode 100644
index 0000000000000..1be25c62ac886
--- /dev/null
+++ b/code/__DEFINES/speech_channels.dm
@@ -0,0 +1,5 @@
+// Used to direct channels to speak into.
+#define SAY_CHANNEL "Say"
+#define RADIO_CHANNEL "Radio"
+#define ME_CHANNEL "Me"
+#define OOC_CHANNEL "OOC"
diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm
index aa942b9d13efe..453b0dc2047a5 100644
--- a/code/__DEFINES/subsystems.dm
+++ b/code/__DEFINES/subsystems.dm
@@ -119,6 +119,7 @@
#define INIT_ORDER_INSTRUMENTS 82
#define INIT_ORDER_GREYSCALE 81
#define INIT_ORDER_VIS 80
+#define INIT_ORDER_SECURITY_LEVEL 79 // We need to load before events so that it has a security level to choose from.
#define INIT_ORDER_DISCORD 78
#define INIT_ORDER_ACHIEVEMENTS 77
#define INIT_ORDER_STATION 74 //This is high priority because it manipulates a lot of the subsystems that will initialize after it.
@@ -260,6 +261,7 @@
* * callback the callback to call on timer finish
* * wait deciseconds to run the timer for
* * flags flags for this timer, see: code\__DEFINES\subsystems.dm
+ * * timer_subsystem the subsystem to insert this timer into
*/
#define addtimer(args...) _addtimer(args, file = __FILE__, line = __LINE__)
diff --git a/code/__DEFINES/supermatter.dm b/code/__DEFINES/supermatter.dm
index e7e29fdef0209..a78c34dbab1be 100644
--- a/code/__DEFINES/supermatter.dm
+++ b/code/__DEFINES/supermatter.dm
@@ -98,7 +98,7 @@
#define GRAVITATIONAL_ANOMALY "gravitational_anomaly"
#define FLUX_ANOMALY "flux_anomaly"
#define PYRO_ANOMALY "pyro_anomaly"
-#define DELIMBER_ANOMALY "delimber_anomaly"
+#define BIOSCRAMBLER_ANOMALY "bioscrambler_anomaly"
#define HALLUCINATION_ANOMALY "hallucination_anomaly"
#define VORTEX_ANOMALY "vortex_anomaly"
@@ -121,3 +121,14 @@
#define MAX_SPACE_EXPOSURE_DAMAGE 10
#define SUPERMATTER_CASCADE_PERCENT 80
+
+/// The divisor scaling value for cubic power loss.
+#define POWERLOSS_CUBIC_DIVISOR 500
+/// The power threshold required to transform power loss into a linear function. It is the power needed for the derivative of the cubic power loss to be equal to POWERLOSS_LINEAR_RATE.
+#define POWERLOSS_LINEAR_THRESHOLD 5880.76
+/// The offset for the linear power loss function. It is the power loss when power is at POWERLOSS_LINEAR_THRESHOLD.
+#define POWERLOSS_LINEAR_OFFSET 1627.01
+/// The rate at which the linear power loss function scales with power.
+#define POWERLOSS_LINEAR_RATE 0.83
+/// How much a psychologist can reduce power loss.
+#define PSYCHOLOGIST_POWERLOSS_REDUCTION 0.2
diff --git a/code/__DEFINES/traits.dm b/code/__DEFINES/traits.dm
index 03b156de66570..f8eaac530d829 100644
--- a/code/__DEFINES/traits.dm
+++ b/code/__DEFINES/traits.dm
@@ -140,6 +140,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_INCAPACITATED "incapacitated"
/// In some kind of critical condition. Is able to succumb.
#define TRAIT_CRITICAL_CONDITION "critical-condition"
+/// Whitelist for mobs that can read or write
+#define TRAIT_LITERATE "literate"
+/// Blacklist for mobs that can't read or write
#define TRAIT_ILLITERATE "illiterate"
#define TRAIT_BLIND "blind"
#define TRAIT_MUTE "mute"
@@ -297,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"
@@ -319,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
@@ -409,6 +413,14 @@ 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"
// METABOLISMS
// Various jobs on the station have historically had better reactions
@@ -437,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"
@@ -485,6 +497,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define TRAIT_AREA_SENSITIVE "area-sensitive"
///every hearing sensitive atom has this trait
#define TRAIT_HEARING_SENSITIVE "hearing_sensitive"
+///every object that is currently the active storage of some client mob has this trait
+#define TRAIT_ACTIVE_STORAGE "active_storage"
/// Climbable trait, given and taken by the climbable element when added or removed. Exists to be easily checked via HAS_TRAIT().
#define TRAIT_CLIMBABLE "trait_climbable"
@@ -769,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"
@@ -807,6 +821,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
#define ORBITING_TRAIT "orbiting"
/// From the item_scaling element
#define ITEM_SCALING_TRAIT "item_scaling"
+/// Trait given by Objects that provide blindsight
+#define ITEM_BLIND_TRAIT "blind_item_trait"
/**
* Trait granted by [/mob/living/carbon/Initialize] and
@@ -891,3 +907,9 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Ignores body_parts_covered during the add_fingerprint() proc. Works both on the person and the item in the glove slot.
#define TRAIT_FINGERPRINT_PASSTHROUGH "fingerprint_passthrough"
+
+/// this object has been frozen
+#define TRAIT_FROZEN "frozen"
+
+/// Currently fishing
+#define TRAIT_GONE_FISHING "fishing"
diff --git a/code/__DEFINES/turfs.dm b/code/__DEFINES/turfs.dm
index 16267e3b25838..1af08fc90a9fa 100644
--- a/code/__DEFINES/turfs.dm
+++ b/code/__DEFINES/turfs.dm
@@ -1,9 +1,9 @@
-#define CHANGETURF_DEFER_CHANGE 1
-#define CHANGETURF_IGNORE_AIR 2 // This flag prevents changeturf from gathering air from nearby turfs to fill the new turf with an approximation of local air
-#define CHANGETURF_FORCEOP 4
-#define CHANGETURF_SKIP 8 // A flag for PlaceOnTop to just instance the new turf instead of calling ChangeTurf. Used for uninitialized turfs NOTHING ELSE
-#define CHANGETURF_INHERIT_AIR 16 // Inherit air from previous turf. Implies CHANGETURF_IGNORE_AIR
-#define CHANGETURF_RECALC_ADJACENT 32 //Immediately recalc adjacent atmos turfs instead of queuing.
+#define CHANGETURF_DEFER_CHANGE (1<<0)
+#define CHANGETURF_IGNORE_AIR (1<<1) // This flag prevents changeturf from gathering air from nearby turfs to fill the new turf with an approximation of local air
+#define CHANGETURF_FORCEOP (1<<2)
+#define CHANGETURF_SKIP (1<<3) // A flag for PlaceOnTop to just instance the new turf instead of calling ChangeTurf. Used for uninitialized turfs NOTHING ELSE
+#define CHANGETURF_INHERIT_AIR (1<<4) // Inherit air from previous turf. Implies CHANGETURF_IGNORE_AIR
+#define CHANGETURF_RECALC_ADJACENT (1<<5) //Immediately recalc adjacent atmos turfs instead of queuing.
#define IS_OPAQUE_TURF(turf) (turf.directional_opacity == ALL_CARDINALS)
diff --git a/code/__DEFINES/uplink.dm b/code/__DEFINES/uplink.dm
index 3a7b847057c29..d8cda44e5911c 100644
--- a/code/__DEFINES/uplink.dm
+++ b/code/__DEFINES/uplink.dm
@@ -10,5 +10,5 @@
#define UPLINK_CLOWN_OPS (1 << 2)
/// Progression gets turned into a user-friendly form. This is just an abstract equation that makes progression not too large.
-#define DISPLAY_PROGRESSION(time) round(time/600, 0.01)
+#define DISPLAY_PROGRESSION(time) round(time/60, 0.01)
diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm
index 3df6008010963..bd6d2fac72139 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/__HELPERS/_lists.dm b/code/__HELPERS/_lists.dm
index a5752f55fa057..f5f91f3c8e719 100644
--- a/code/__HELPERS/_lists.dm
+++ b/code/__HELPERS/_lists.dm
@@ -31,7 +31,7 @@
#define LAZYSET(L, K, V) if(!L) { L = list(); } L[K] = V;
///Sets the length of a lazylist
#define LAZYSETLEN(L, V) if (!L) { L = list(); } L.len = V;
-///Returns the lenght of the list
+///Returns the length of the list
#define LAZYLEN(L) length(L)
///Sets a list to null
#define LAZYNULL(L) L = null
diff --git a/code/__HELPERS/areas.dm b/code/__HELPERS/areas.dm
index 363a760d71db4..d2501885286fd 100644
--- a/code/__HELPERS/areas.dm
+++ b/code/__HELPERS/areas.dm
@@ -176,3 +176,16 @@ GLOBAL_LIST_INIT(typecache_powerfailure_safe_areas, typecacheof(/area/station/en
if(target_z == 0 || target_z == turf_in_area.z)
turfs += turf_in_area
return turfs
+
+///Takes: list of area types
+///Returns: all mobs that are in an area type
+/proc/mobs_in_area_type(list/area/checked_areas)
+ var/list/mobs_in_area = list()
+ for(var/mob/living/mob as anything in GLOB.mob_living_list)
+ if(QDELETED(mob))
+ continue
+ for(var/area in checked_areas)
+ if(istype(get_area(mob), area))
+ mobs_in_area += mob
+ break
+ return mobs_in_area
diff --git a/code/__HELPERS/game.dm b/code/__HELPERS/game.dm
index 4c4d001ce4cb0..feb2736a111df 100644
--- a/code/__HELPERS/game.dm
+++ b/code/__HELPERS/game.dm
@@ -162,6 +162,14 @@
/proc/show_candidate_poll_window(mob/candidate_mob, poll_time, question, list/candidates, ignore_category, time_passed, flashwindow = TRUE)
set waitfor = 0
+ // Universal opt-out for all players.
+ if ((!candidate_mob.client.prefs.read_preference(/datum/preference/toggle/ghost_roles)))
+ return
+
+ // Opt-out for admins whom are currently adminned.
+ if ((!candidate_mob.client.prefs.read_preference(/datum/preference/toggle/ghost_roles_as_admin)) && candidate_mob.client.holder)
+ return
+
SEND_SOUND(candidate_mob, 'sound/misc/notice2.ogg') //Alerting them to their consideration
if(flashwindow)
window_flash(candidate_mob.client)
@@ -407,4 +415,4 @@
message = html_encode(message)
else
message = copytext(message, 2)
- to_chat(target, span_purple("Tip of the round: [message]"))
+ to_chat(target, span_purple(examine_block("Tip of the round: [message]")))
diff --git a/code/__HELPERS/icons.dm b/code/__HELPERS/icons.dm
index 2d41fc6f1c122..d68dfdf3ece6c 100644
--- a/code/__HELPERS/icons.dm
+++ b/code/__HELPERS/icons.dm
@@ -1086,23 +1086,23 @@ GLOBAL_LIST_EMPTY(friendly_animal_types)
GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0,0,0)))
-/obj/proc/make_frozen_visual()
- // Used to make the frozen item visuals for Freon.
- if(resistance_flags & FREEZE_PROOF)
- return
- if(!(obj_flags & FROZEN))
- name = "frozen [name]"
- add_atom_colour(GLOB.freon_color_matrix, TEMPORARY_COLOUR_PRIORITY)
- alpha -= 25
- obj_flags |= FROZEN
-
//Assumes already frozed
/obj/proc/make_unfrozen()
- if(obj_flags & FROZEN)
- name = replacetext(name, "frozen ", "")
- remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, GLOB.freon_color_matrix)
- alpha += 25
- obj_flags &= ~FROZEN
+ SEND_SIGNAL(src, COMSIG_OBJ_UNFREEZE)
+
+/// generates a filename for a given asset.
+/// like generate_asset_name(), except returns the rsc reference and the rsc file hash as well as the asset name (sans extension)
+/// used so that certain asset files dont have to be hashed twice
+/proc/generate_and_hash_rsc_file(file, dmi_file_path)
+ var/rsc_ref = fcopy_rsc(file)
+ var/hash
+ //if we have a valid dmi file path we can trust md5'ing the rsc file because we know it doesnt have the bug described in http://www.byond.com/forum/post/2611357
+ if(dmi_file_path)
+ hash = md5(rsc_ref)
+ else //otherwise, we need to do the expensive fcopy() workaround
+ hash = md5asfile(rsc_ref)
+
+ return list(rsc_ref, hash, "asset.[hash]")
/// Generate a filename for this asset
/// The same asset will always lead to the same asset name
@@ -1127,14 +1127,88 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
dummySave = null
fdel("tmp/dummySave.sav") //if you get the idea to try and make this more optimized, make sure to still call unlock on the savefile after every write to unlock it.
-/proc/icon2html(thing, target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null)
+///given a text string, returns whether it is a valid dmi icons folder path
+/proc/is_valid_dmi_file(icon_path)
+ if(!istext(icon_path) || !length(icon_path))
+ return FALSE
+
+ var/is_in_icon_folder = findtextEx(icon_path, "icons/")
+ var/is_dmi_file = findtextEx(icon_path, ".dmi")
+
+ if(is_in_icon_folder && is_dmi_file)
+ return TRUE
+ return FALSE
+
+/// given an icon object, dmi file path, or atom/image/mutable_appearance, attempts to find and return an associated dmi file path.
+/// a weird quirk about dm is that /icon objects represent both compile-time or dynamic icons in the rsc,
+/// but stringifying rsc references returns a dmi file path
+/// ONLY if that icon represents a completely unchanged dmi file from when the game was compiled.
+/// so if the given object is associated with an icon that was in the rsc when the game was compiled, this returns a path. otherwise it returns ""
+/proc/get_icon_dmi_path(icon/icon)
+ /// the dmi file path we attempt to return if the given object argument is associated with a stringifiable icon
+ /// if successful, this looks like "icons/path/to/dmi_file.dmi"
+ var/icon_path = ""
+
+ if(isatom(icon) || istype(icon, /image) || istype(icon, /mutable_appearance))
+ var/atom/atom_icon = icon
+ icon = atom_icon.icon
+ //atom icons compiled in from 'icons/path/to/dmi_file.dmi' are weird and not really icon objects that you generate with icon().
+ //if theyre unchanged dmi's then they're stringifiable to "icons/path/to/dmi_file.dmi"
+
+ if(isicon(icon) && isfile(icon))
+ //icons compiled in from 'icons/path/to/dmi_file.dmi' at compile time are weird and arent really /icon objects,
+ ///but they pass both isicon() and isfile() checks. theyre the easiest case since stringifying them gives us the path we want
+ var/icon_ref = "\ref[icon]"
+ var/locate_icon_string = "[locate(icon_ref)]"
+
+ icon_path = locate_icon_string
+
+ else if(isicon(icon) && "[icon]" == "/icon")
+ // icon objects generated from icon() at runtime are icons, but they ARENT files themselves, they represent icon files.
+ // if the files they represent are compile time dmi files in the rsc, then
+ // the rsc reference returned by fcopy_rsc() will be stringifiable to "icons/path/to/dmi_file.dmi"
+ var/rsc_ref = fcopy_rsc(icon)
+
+ var/icon_ref = "\ref[rsc_ref]"
+
+ var/icon_path_string = "[locate(icon_ref)]"
+
+ icon_path = icon_path_string
+
+ else if(istext(icon))
+ var/rsc_ref = fcopy_rsc(icon)
+ //if its the text path of an existing dmi file, the rsc reference returned by fcopy_rsc() will be stringifiable to a dmi path
+
+ var/rsc_ref_ref = "\ref[rsc_ref]"
+ var/rsc_ref_string = "[locate(rsc_ref_ref)]"
+
+ icon_path = rsc_ref_string
+
+ if(is_valid_dmi_file(icon_path))
+ return icon_path
+
+ return FALSE
+
+/**
+ * generate an asset for the given icon or the icon of the given appearance for [thing], and send it to any clients in target.
+ * Arguments:
+ * * thing - either a /icon object, or an object that has an appearance (atom, image, mutable_appearance).
+ * * target - either a reference to or a list of references to /client's or mobs with clients
+ * * icon_state - string to force a particular icon_state for the icon to be used
+ * * dir - dir number to force a particular direction for the icon to be used
+ * * frame - what frame of the icon_state's animation for the icon being used
+ * * moving - whether or not to use a moving state for the given icon
+ * * sourceonly - if TRUE, only generate the asset and send back the asset url, instead of tags that display the icon to players
+ * * extra_clases - string of extra css classes to use when returning the icon string
+ */
+/proc/icon2html(atom/thing, client/target, icon_state, dir = SOUTH, frame = 1, moving = FALSE, sourceonly = FALSE, extra_classes = null)
if (!thing)
return
if(SSlag_switch.measures[DISABLE_USR_ICON2HTML] && usr && !HAS_TRAIT(usr, TRAIT_BYPASS_MEASURES))
return
var/key
- var/icon/I = thing
+ var/icon/icon2collapse = thing
if (!target)
return
@@ -1146,10 +1220,14 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
targets = list(target)
else
targets = target
- if (!targets.len)
- return
+ if(!length(targets))
+ return
+
+ //check if the given object is associated with a dmi file in the icons folder. if it is then we dont need to do a lot of work
+ //for asset generation to get around byond limitations
+ var/icon_path = get_icon_dmi_path(thing)
- if (!isicon(I))
+ if (!isicon(icon2collapse))
if (isfile(thing)) //special snowflake
var/name = SANITIZE_FILENAME("[generate_asset_name(thing)].png")
if (!SSassets.cache[name])
@@ -1159,25 +1237,25 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
if(sourceonly)
return SSassets.transport.get_asset_url(name)
return ""
- var/atom/A = thing
- I = A.icon
+ //its either an atom, image, or mutable_appearance, we want its icon var
+ icon2collapse = thing.icon
if (isnull(icon_state))
- icon_state = A.icon_state
+ icon_state = thing.icon_state
//Despite casting to atom, this code path supports mutable appearances, so let's be nice to them
- if(isnull(icon_state) || (isatom(thing) && A.flags_1 & HTML_USE_INITAL_ICON_1))
- icon_state = initial(A.icon_state)
+ if(isnull(icon_state) || (isatom(thing) && thing.flags_1 & HTML_USE_INITAL_ICON_1))
+ icon_state = initial(thing.icon_state)
if (isnull(dir))
- dir = initial(A.dir)
+ dir = initial(thing.dir)
if (isnull(dir))
- dir = A.dir
+ dir = thing.dir
if (ishuman(thing)) // Shitty workaround for a BYOND issue.
- var/icon/temp = I
- I = icon()
- I.Insert(temp, dir = SOUTH)
+ var/icon/temp = icon2collapse
+ icon2collapse = icon()
+ icon2collapse.Insert(temp, dir = SOUTH)
dir = SOUTH
else
if (isnull(dir))
@@ -1185,13 +1263,18 @@ GLOBAL_LIST_INIT(freon_color_matrix, list("#2E5E69", "#60A2A8", "#A1AFB1", rgb(0
if (isnull(icon_state))
icon_state = ""
- I = icon(I, icon_state, dir, frame, moving)
+ icon2collapse = icon(icon2collapse, icon_state, dir, frame, moving)
+
+ var/list/name_and_ref = generate_and_hash_rsc_file(icon2collapse, icon_path)//pretend that tuples exist
+
+ var/rsc_ref = name_and_ref[1] //weird object thats not even readable to the debugger, represents a reference to the icons rsc entry
+ var/file_hash = name_and_ref[2]
+ key = "[name_and_ref[3]].png"
- key = "[generate_asset_name(I)].png"
if(!SSassets.cache[key])
- SSassets.transport.register_asset(key, I)
- for (var/thing2 in targets)
- SSassets.transport.send_assets(thing2, key)
+ SSassets.transport.register_asset(key, rsc_ref, file_hash, icon_path)
+ for (var/client_target in targets)
+ SSassets.transport.send_assets(client_target, key)
if(sourceonly)
return SSassets.transport.get_asset_url(key)
return ""
diff --git a/code/__HELPERS/levels.dm b/code/__HELPERS/levels.dm
new file mode 100644
index 0000000000000..218c1013bed83
--- /dev/null
+++ b/code/__HELPERS/levels.dm
@@ -0,0 +1,19 @@
+/**
+ * - is_valid_z_level
+ *
+ * Checks if source_loc and checking_loc is both on the station, or on the same z level.
+ * This is because the station's several levels aren't considered the same z, so multi-z stations need this special case.
+ *
+ * Args:
+ * source_loc - turf of the source we're comparing.
+ * checking_loc - turf we are comparing to source_loc.
+ *
+ * returns TRUE if connection is valid, FALSE otherwise.
+ */
+/proc/is_valid_z_level(turf/source_loc, turf/checking_loc)
+ // if we're both on "station", regardless of multi-z, we'll pass by.
+ if(is_station_level(source_loc.z) && is_station_level(checking_loc.z))
+ return TRUE
+ if(source_loc.z == checking_loc.z)
+ return TRUE
+ return FALSE
diff --git a/code/__HELPERS/logging/_logging.dm b/code/__HELPERS/logging/_logging.dm
index 30f34a4f7b6cd..ac0181d5282f2 100644
--- a/code/__HELPERS/logging/_logging.dm
+++ b/code/__HELPERS/logging/_logging.dm
@@ -114,6 +114,8 @@ GLOBAL_LIST_INIT(testing_global_profiler, list("_PROFILE_NAME" = "Global"))
log_mecha(log_text)
if(LOG_SHUTTLE)
log_shuttle(log_text)
+ if(LOG_SPEECH_INDICATORS)
+ log_speech_indicators(log_text)
else
stack_trace("Invalid individual logging type: [message_type]. Defaulting to [LOG_GAME] (LOG_GAME).")
log_game(log_text)
diff --git a/code/__HELPERS/logging/talk.dm b/code/__HELPERS/logging/talk.dm
index 86382f6a339d1..bed13b6c3505e 100644
--- a/code/__HELPERS/logging/talk.dm
+++ b/code/__HELPERS/logging/talk.dm
@@ -40,3 +40,8 @@
/proc/log_telecomms(text)
if (CONFIG_GET(flag/log_telecomms))
WRITE_LOG(GLOB.world_telecomms_log, "TCOMMS: [text]")
+
+/// Logging for speech indicators.
+/proc/log_speech_indicators(text)
+ if (CONFIG_GET(flag/log_speech_indicators))
+ WRITE_LOG(GLOB.world_speech_indicators_log, "SPEECH INDICATOR: [text]")
diff --git a/code/__HELPERS/logging/tools.dm b/code/__HELPERS/logging/tools.dm
deleted file mode 100644
index 4c5f2d3e65558..0000000000000
--- a/code/__HELPERS/logging/tools.dm
+++ /dev/null
@@ -1,3 +0,0 @@
-/proc/log_tool(text, mob/initiator)
- if(CONFIG_GET(flag/log_tools))
- WRITE_LOG(GLOB.world_tool_log, "TOOL: [text]")
diff --git a/code/__HELPERS/maths.dm b/code/__HELPERS/maths.dm
index 49cab9bf273cf..9d67466cf5b30 100644
--- a/code/__HELPERS/maths.dm
+++ b/code/__HELPERS/maths.dm
@@ -1,11 +1,21 @@
-///Calculate the angle between two points and the west|east coordinate
+///Calculate the angle between two movables and the west|east coordinate
/proc/get_angle(atom/movable/start, atom/movable/end)//For beams.
if(!start || !end)
return 0
- var/dy
- var/dx
- dy=(32 * end.y + end.pixel_y) - (32 * start.y + start.pixel_y)
- dx=(32 * end.x + end.pixel_x) - (32 * start.x + start.pixel_x)
+ var/dy =(32 * end.y + end.pixel_y) - (32 * start.y + start.pixel_y)
+ var/dx =(32 * end.x + end.pixel_x) - (32 * start.x + start.pixel_x)
+ if(!dy)
+ return (dx >= 0) ? 90 : 270
+ . = arctan(dx/dy)
+ if(dy < 0)
+ . += 180
+ else if(dx < 0)
+ . += 360
+
+/// Angle between two arbitrary points and horizontal line same as [/proc/get_angle]
+/proc/get_angle_raw(start_x, start_y, start_pixel_x, start_pixel_y, end_x, end_y, end_pixel_x, end_pixel_y)
+ var/dy = (32 * end_y + end_pixel_y) - (32 * start_y + start_pixel_y)
+ var/dx = (32 * end_x + end_pixel_x) - (32 * start_x + start_pixel_x)
if(!dy)
return (dx >= 0) ? 90 : 270
. = arctan(dx/dy)
diff --git a/code/__HELPERS/randoms.dm b/code/__HELPERS/randoms.dm
index 7d425c70c8810..a772f65a3140b 100644
--- a/code/__HELPERS/randoms.dm
+++ b/code/__HELPERS/randoms.dm
@@ -1,6 +1,9 @@
///Get a random food item exluding the blocked ones
/proc/get_random_food()
- var/list/blocked = list(/obj/item/food/bread,
+ var/list/blocked = list(
+ /obj/item/food/drug,
+ /obj/item/food/spaghetti,
+ /obj/item/food/bread,
/obj/item/food/breadslice,
/obj/item/food/cake,
/obj/item/food/cakeslice,
diff --git a/code/__HELPERS/reagents.dm b/code/__HELPERS/reagents.dm
index bfd00ed46988c..12be82c4c654c 100644
--- a/code/__HELPERS/reagents.dm
+++ b/code/__HELPERS/reagents.dm
@@ -79,12 +79,12 @@
GLOB.chemical_reactions_list_reactant_index[primary_reagent] += R
//Creates foam from the reagent. Metaltype is for metal foam, notification is what to show people in textbox
-/datum/reagents/proc/create_foam(foamtype, foam_volume, result_type = null, notification = null)
+/datum/reagents/proc/create_foam(foamtype, foam_volume, result_type = null, notification = null, log = FALSE)
var/location = get_turf(my_atom)
var/datum/effect_system/fluid_spread/foam/foam = new foamtype()
- foam.set_up(amount = foam_volume, location = location, carry = src, result_type = result_type)
- foam.start()
+ foam.set_up(amount = foam_volume, holder = my_atom, location = location, carry = src, result_type = result_type)
+ foam.start(log = log)
clear_reagents()
if(!notification)
diff --git a/code/__HELPERS/turfs.dm b/code/__HELPERS/turfs.dm
index 5da17c67f311d..0d0126b48b12a 100644
--- a/code/__HELPERS/turfs.dm
+++ b/code/__HELPERS/turfs.dm
@@ -229,8 +229,8 @@ Turf and target are separate in case you want to teleport some distance from a t
var/turf/atom_turf = get_turf(checked_atom) //use checked_atom's turfs, as it's coords are the same as checked_atom's AND checked_atom's coords are lost if it is inside another atom
if(!atom_turf)
return null
- var/final_x = atom_turf.x + rough_x
- var/final_y = atom_turf.y + rough_y
+ var/final_x = clamp(atom_turf.x + rough_x, 1, world.maxx)
+ var/final_y = clamp(atom_turf.y + rough_y, 1, world.maxy)
if(final_x || final_y)
return locate(final_x, final_y, atom_turf.z)
diff --git a/code/_compile_options.dm b/code/_compile_options.dm
index 94d17a40bcba5..a4ccce442cae5 100644
--- a/code/_compile_options.dm
+++ b/code/_compile_options.dm
@@ -35,6 +35,11 @@
*/
//#define REAGENTS_TESTING
+// Displays static object lighting updates
+// Also enables some debug vars on sslighting that can be used to modify
+// How extensively we prune lighting corners to update
+#define VISUALIZE_LIGHT_UPDATES
+
#define VISUALIZE_ACTIVE_TURFS //Highlights atmos active turfs in green
#define TRACK_MAX_SHARE //Allows max share tracking, for use in the atmos debugging ui
#endif //ifdef TESTING
diff --git a/code/_globalvars/bitfields.dm b/code/_globalvars/bitfields.dm
index 8f20ed4f0b7d8..79067e9666da4 100644
--- a/code/_globalvars/bitfields.dm
+++ b/code/_globalvars/bitfields.dm
@@ -274,7 +274,6 @@ DEFINE_BITFIELD(obj_flags, list(
"CAN_BE_HIT" = CAN_BE_HIT,
"DANGEROUS_POSSESSION" = DANGEROUS_POSSESSION,
"EMAGGED" = EMAGGED,
- "FROZEN" = FROZEN,
"IN_USE" = IN_USE,
"NO_BUILD" = NO_BUILD,
"ON_BLUEPRINTS" = ON_BLUEPRINTS,
diff --git a/code/_globalvars/lists/maintenance_loot.dm b/code/_globalvars/lists/maintenance_loot.dm
index 81224a93dc6c3..2f97a7816a83c 100644
--- a/code/_globalvars/lists/maintenance_loot.dm
+++ b/code/_globalvars/lists/maintenance_loot.dm
@@ -289,6 +289,7 @@ GLOBAL_LIST_INIT(rarity_loot, list(//rare: really good items
/obj/item/shield/riot/buckler = 1,
/obj/item/throwing_star = 1,
/obj/item/weldingtool/hugetank = 1,
+ /obj/item/fishing_rod/master = 1,
) = 1,
list(//equipment
diff --git a/code/_globalvars/logging.dm b/code/_globalvars/logging.dm
index fc7368b6cdac8..31447e7cc50b3 100644
--- a/code/_globalvars/logging.dm
+++ b/code/_globalvars/logging.dm
@@ -33,6 +33,8 @@ GLOBAL_VAR(world_uplink_log)
GLOBAL_PROTECT(world_uplink_log)
GLOBAL_VAR(world_telecomms_log)
GLOBAL_PROTECT(world_telecomms_log)
+GLOBAL_VAR(world_speech_indicators_log)
+GLOBAL_PROTECT(world_speech_indicators_log)
GLOBAL_VAR(world_manifest_log)
GLOBAL_PROTECT(world_manifest_log)
GLOBAL_VAR(query_debug_log)
diff --git a/code/_globalvars/phobias.dm b/code/_globalvars/phobias.dm
index 07c46d9f5b029..73cd02df93c63 100644
--- a/code/_globalvars/phobias.dm
+++ b/code/_globalvars/phobias.dm
@@ -222,6 +222,8 @@ GLOBAL_LIST_INIT(phobia_objs, list(
/obj/item/toy/figure/hop,
/obj/item/toy/figure/hos,
/obj/item/toy/figure/rd,
+ /obj/item/toy/plush/abductor,
+ /obj/item/toy/plush/abductor/agent,
/obj/machinery/atmospherics/miner,
/obj/machinery/door/airlock/centcom,
)),
@@ -353,6 +355,8 @@ GLOBAL_LIST_INIT(phobia_objs, list(
/obj/item/stack/sheet/mineral/abductor,
/obj/item/surgicaldrill/alien,
/obj/item/toy/toy_xeno,
+ /obj/item/toy/plush/abductor,
+ /obj/item/toy/plush/abductor/agent,
/obj/item/weldingtool/abductor,
/obj/item/wirecutters/abductor,
/obj/item/wrench/abductor,
diff --git a/code/_globalvars/traits.dm b/code/_globalvars/traits.dm
index 6e7ffe5a38ad1..042882340364c 100644
--- a/code/_globalvars/traits.dm
+++ b/code/_globalvars/traits.dm
@@ -13,6 +13,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_RESTRAINED" = TRAIT_RESTRAINED,
"TRAIT_INCAPACITATED" = TRAIT_INCAPACITATED,
"TRAIT_CRITICAL_CONDITION" = TRAIT_CRITICAL_CONDITION,
+ "TRAIT_LITERATE" = TRAIT_LITERATE,
"TRAIT_ILLITERATE" = TRAIT_ILLITERATE,
"TRAIT_BLIND" = TRAIT_BLIND,
"TRAIT_MUTE" = TRAIT_MUTE,
@@ -119,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/_onclick/click.dm b/code/_onclick/click.dm
index 5b3e8966f72d1..5c8ca3cd6cbf1 100644
--- a/code/_onclick/click.dm
+++ b/code/_onclick/click.dm
@@ -213,11 +213,13 @@
for(var/atom/target in checking) // will filter out nulls
if(closed[target] || isarea(target)) // avoid infinity situations
continue
- closed[target] = TRUE
+
if(isturf(target) || isturf(target.loc) || (target in direct_access) || (ismovable(target) && target.flags_1 & IS_ONTOP_1)) //Directly accessible atoms
if(Adjacent(target) || (tool && CheckToolReach(src, target, tool.reach))) //Adjacent or reaching attacks
return TRUE
+ closed[target] = TRUE
+
if (!target.loc)
continue
diff --git a/code/_onclick/hud/human.dm b/code/_onclick/hud/human.dm
index d92915c9acd12..96503c1822a51 100644
--- a/code/_onclick/hud/human.dm
+++ b/code/_onclick/hud/human.dm
@@ -94,7 +94,7 @@
static_inventory += using
inv_box = new /atom/movable/screen/inventory()
- inv_box.name = "i_clothing"
+ inv_box.name = "uniform"
inv_box.icon = ui_style
inv_box.slot_id = ITEM_SLOT_ICLOTHING
inv_box.icon_state = "uniform"
@@ -103,7 +103,7 @@
toggleable_inventory += inv_box
inv_box = new /atom/movable/screen/inventory()
- inv_box.name = "o_clothing"
+ inv_box.name = "suit"
inv_box.icon = ui_style
inv_box.slot_id = ITEM_SLOT_OCLOTHING
inv_box.icon_state = "suit"
@@ -164,7 +164,7 @@
static_inventory += inv_box
inv_box = new /atom/movable/screen/inventory()
- inv_box.name = "storage1"
+ inv_box.name = "left pocket"
inv_box.icon = ui_style
inv_box.icon_state = "pocket"
inv_box.screen_loc = ui_storage1
@@ -173,7 +173,7 @@
static_inventory += inv_box
inv_box = new /atom/movable/screen/inventory()
- inv_box.name = "storage2"
+ inv_box.name = "right pocket"
inv_box.icon = ui_style
inv_box.icon_state = "pocket"
inv_box.screen_loc = ui_storage2
diff --git a/code/_onclick/hud/new_player.dm b/code/_onclick/hud/new_player.dm
index 373a4e1cda9b5..6946faa6885af 100644
--- a/code/_onclick/hud/new_player.dm
+++ b/code/_onclick/hud/new_player.dm
@@ -2,8 +2,13 @@
/datum/hud/new_player/New(mob/owner)
..()
- if (owner?.client?.interviewee)
+
+ if(!owner || !owner.client)
+ return
+
+ if (owner.client.interviewee)
return
+
var/list/buttons = subtypesof(/atom/movable/screen/lobby)
for(var/button_type in buttons)
var/atom/movable/screen/lobby/lobbyscreen = new button_type()
@@ -41,6 +46,9 @@
if(owner != REF(usr))
return
+ if(!usr.client || usr.client.interviewee)
+ return
+
. = ..()
if(!enabled)
@@ -53,6 +61,9 @@
if(owner != REF(usr))
return
+ if(!usr.client || usr.client.interviewee)
+ return
+
. = ..()
highlighted = TRUE
update_appearance(UPDATE_ICON)
@@ -61,6 +72,9 @@
if(owner != REF(usr))
return
+ if(!usr.client || usr.client.interviewee)
+ return
+
. = ..()
highlighted = FALSE
update_appearance(UPDATE_ICON)
diff --git a/code/_onclick/hud/parallax.dm b/code/_onclick/hud/parallax.dm
index 11316c3a77cc1..a61513181fa35 100755
--- a/code/_onclick/hud/parallax.dm
+++ b/code/_onclick/hud/parallax.dm
@@ -324,6 +324,7 @@ INITIALIZE_IMMEDIATE(/atom/movable/screen/parallax_layer)
/atom/movable/screen/parallax_layer/random/asteroids
icon_state = "asteroids"
+ layer = 4
/atom/movable/screen/parallax_layer/planet
icon_state = "planet"
diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm
index fbe5d977573df..b88ddcdd9455a 100644
--- a/code/_onclick/item_attack.dm
+++ b/code/_onclick/item_attack.dm
@@ -242,18 +242,21 @@
/area/attacked_by(obj/item/attacking_item, mob/living/user)
CRASH("areas are NOT supposed to have attacked_by() called on them!")
-/mob/living/attacked_by(obj/item/I, mob/living/user)
- send_item_attack_message(I, user)
- if(I.force)
- apply_damage(I.force, I.damtype)
- if(I.damtype == BRUTE)
- if(prob(33))
- I.add_mob_blood(src)
- var/turf/location = get_turf(src)
- add_splatter_floor(location)
- if(get_dist(user, src) <= 1) //people with TK won't get smeared with blood
- user.add_mob_blood(src)
- return TRUE //successful attack
+/mob/living/attacked_by(obj/item/attacking_item, mob/living/user)
+ send_item_attack_message(attacking_item, user)
+ if(!attacking_item.force)
+ return FALSE
+ var/damage = attacking_item.force
+ if(mob_biotypes & MOB_ROBOTIC)
+ damage *= attacking_item.demolition_mod
+ apply_damage(damage, attacking_item.damtype)
+ if(attacking_item.damtype == BRUTE && prob(33))
+ attacking_item.add_mob_blood(src)
+ var/turf/location = get_turf(src)
+ add_splatter_floor(location)
+ if(get_dist(user, src) <= 1) //people with TK won't get smeared with blood
+ user.add_mob_blood(src)
+ return TRUE //successful attack
/mob/living/simple_animal/attacked_by(obj/item/I, mob/living/user)
if(!attack_threshold_check(I.force, I.damtype, MELEE, FALSE))
diff --git a/code/_onclick/observer.dm b/code/_onclick/observer.dm
index b1f8e627375b9..4ac8c1739ea51 100644
--- a/code/_onclick/observer.dm
+++ b/code/_onclick/observer.dm
@@ -54,7 +54,7 @@
if(SEND_SIGNAL(src, COMSIG_ATOM_ATTACK_GHOST, user) & COMPONENT_CANCEL_ATTACK_CHAIN)
return TRUE
if(user.client)
- if(user.gas_scan && atmos_scan(user=user, target=src, tool=null, silent=TRUE))
+ if(user.gas_scan && atmos_scan(user=user, target=src, silent=TRUE))
return TRUE
else if(isAdminGhostAI(user))
attack_ai(user)
diff --git a/code/controllers/configuration/entries/game_options.dm b/code/controllers/configuration/entries/game_options.dm
index 3dcb55a3b93a6..a42c03f7371b4 100644
--- a/code/controllers/configuration/entries/game_options.dm
+++ b/code/controllers/configuration/entries/game_options.dm
@@ -103,6 +103,8 @@
/datum/config_entry/flag/enforce_human_authority //If non-human species are barred from joining as a head of staff
+/datum/config_entry/flag/enforce_human_authority_on_everyone //If non-human species are barred from joining as a head of staff, including jobs flagged as allowed for non-humans, ie. Quartermaster.
+
/datum/config_entry/flag/allow_latejoin_antagonists // If late-joining players can be traitor/changeling
/datum/config_entry/number/shuttle_refuel_delay
@@ -387,11 +389,13 @@
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
+
/datum/config_entry/number/station_goal_budget
default = 1
min_val = 0
integer = FALSE
+
+/datum/config_entry/flag/disallow_circuit_sounds
diff --git a/code/controllers/configuration/entries/general.dm b/code/controllers/configuration/entries/general.dm
index ec482d3ad6542..65fce5d270fc2 100644
--- a/code/controllers/configuration/entries/general.dm
+++ b/code/controllers/configuration/entries/general.dm
@@ -131,6 +131,9 @@
/// log telecomms messages
/datum/config_entry/flag/log_telecomms
+/// log speech indicators(started/stopped speaking)
+/datum/config_entry/flag/log_speech_indicators
+
/// log certain expliotable parrots and other such fun things in a JSON file of twitter valid phrases.
/datum/config_entry/flag/log_twitter
diff --git a/code/controllers/subsystem/explosions.dm b/code/controllers/subsystem/explosions.dm
index a9e435742adc1..faa33a3b0e8a8 100644
--- a/code/controllers/subsystem/explosions.dm
+++ b/code/controllers/subsystem/explosions.dm
@@ -528,18 +528,18 @@ SUBSYSTEM_DEF(explosions)
var/base_shake_amount = sqrt(near_distance / (distance + 1))
if(distance <= round(near_distance + world.view - 2, 1)) // If you are close enough to see the effects of the explosion first-hand (ignoring walls)
- listener.playsound_local(epicenter, null, 100, TRUE, frequency, S = near_sound)
+ listener.playsound_local(epicenter, null, 100, TRUE, frequency, sound_to_use = near_sound)
if(base_shake_amount > 0)
shake_camera(listener, NEAR_SHAKE_DURATION, clamp(base_shake_amount, 0, NEAR_SHAKE_CAP))
else if(distance < far_distance) // You can hear a far explosion if you are outside the blast radius. Small explosions shouldn't be heard throughout the station.
var/far_volume = clamp(far_distance / 2, FAR_LOWER, FAR_UPPER)
if(creaking)
- listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, S = creaking_sound, distance_multiplier = 0)
+ listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, sound_to_use = creaking_sound, distance_multiplier = 0)
else if(prob(FAR_SOUND_PROB)) // Sound variety during meteor storm/tesloose/other bad event
- listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, S = far_sound, distance_multiplier = 0)
+ listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, sound_to_use = far_sound, distance_multiplier = 0)
else
- listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, S = echo_sound, distance_multiplier = 0)
+ listener.playsound_local(epicenter, null, far_volume, TRUE, frequency, sound_to_use = echo_sound, distance_multiplier = 0)
if(base_shake_amount || quake_factor)
base_shake_amount = max(base_shake_amount, quake_factor * 3, 0) // Devastating explosions rock the station and ground
@@ -552,7 +552,7 @@ SUBSYSTEM_DEF(explosions)
shake_camera(listener, FAR_SHAKE_DURATION, clamp(quake_factor / 4, 0, FAR_SHAKE_CAP))
else
echo_volume = 40
- listener.playsound_local(epicenter, null, echo_volume, TRUE, frequency, S = echo_sound, distance_multiplier = 0)
+ listener.playsound_local(epicenter, null, echo_volume, TRUE, frequency, sound_to_use = echo_sound, distance_multiplier = 0)
if(creaking) // 5 seconds after the bang, the station begins to creak
addtimer(CALLBACK(listener, /mob/proc/playsound_local, epicenter, null, rand(FREQ_LOWER, FREQ_UPPER), TRUE, frequency, null, null, FALSE, hull_creaking_sound, 0), CREAK_DELAY)
diff --git a/code/controllers/subsystem/id_access.dm b/code/controllers/subsystem/id_access.dm
index 5ab65198e94f3..e5e85c5189245 100644
--- a/code/controllers/subsystem/id_access.dm
+++ b/code/controllers/subsystem/id_access.dm
@@ -188,6 +188,12 @@ SUBSYSTEM_DEF(id_access)
"templates" = list(),
"pdas" = list(),
),
+ "[ACCESS_QM]" = list(
+ "regions" = list(REGION_SUPPLY),
+ "head" = JOB_QUARTERMASTER,
+ "templates" = list(),
+ "pdas" = list(),
+ ),
)
var/list/station_job_trims = subtypesof(/datum/id_trim/job)
@@ -289,7 +295,7 @@ SUBSYSTEM_DEF(id_access)
desc_by_access["[ACCESS_THEATRE]"] = "Theatre"
desc_by_access["[ACCESS_RESEARCH]"] = "Science"
desc_by_access["[ACCESS_MINING]"] = "Mining"
- desc_by_access["[ACCESS_MAIL_SORTING]"] = "Cargo Office"
+ desc_by_access["[ACCESS_SHIPPING]"] = "Cargo Shipping"
desc_by_access["[ACCESS_VAULT]"] = "Main Vault"
desc_by_access["[ACCESS_MINING_STATION]"] = "Mining EVA"
desc_by_access["[ACCESS_XENOBIOLOGY]"] = "Xenobiology Lab"
diff --git a/code/controllers/subsystem/input.dm b/code/controllers/subsystem/input.dm
index 246ab84768a72..0b80692ffbc34 100644
--- a/code/controllers/subsystem/input.dm
+++ b/code/controllers/subsystem/input.dm
@@ -24,7 +24,7 @@ SUBSYSTEM_DEF(input)
"Any" = "\"KeyDown \[\[*\]\]\"",
"Any+UP" = "\"KeyUp \[\[*\]\]\"",
"Back" = "\".winset \\\"input.text=\\\"\\\"\\\"\"",
- "Tab" = "\".winset \\\"input.focus=true?map.focus=true input.background-color=[COLOR_INPUT_DISABLED]:input.focus=true input.background-color=[COLOR_INPUT_ENABLED]\\\"\"",
+ "Tab" = "\".winset \\\"input.focus=true?map.focus=true:input.focus=true\\\"\"",
"Escape" = "Reset-Held-Keys",
)
diff --git a/code/controllers/subsystem/lighting.dm b/code/controllers/subsystem/lighting.dm
index 29999a8ff46cd..378a849dfa167 100644
--- a/code/controllers/subsystem/lighting.dm
+++ b/code/controllers/subsystem/lighting.dm
@@ -6,6 +6,10 @@ SUBSYSTEM_DEF(lighting)
var/static/list/sources_queue = list() // List of lighting sources queued for update.
var/static/list/corners_queue = list() // List of lighting corners queued for update.
var/static/list/objects_queue = list() // List of lighting objects queued for update.
+#ifdef VISUALIZE_LIGHT_UPDATES
+ var/allow_duped_values = FALSE
+ var/allow_duped_corners = FALSE
+#endif
/datum/controller/subsystem/lighting/stat_entry(msg)
msg = "L:[length(sources_queue)]|C:[length(corners_queue)]|O:[length(objects_queue)]"
diff --git a/code/controllers/subsystem/mapping.dm b/code/controllers/subsystem/mapping.dm
index 16dc37b6a1976..23167d6edf8b1 100644
--- a/code/controllers/subsystem/mapping.dm
+++ b/code/controllers/subsystem/mapping.dm
@@ -40,14 +40,21 @@ SUBSYSTEM_DEF(mapping)
// Z-manager stuff
var/station_start // should only be used for maploading-related tasks
var/space_levels_so_far = 0
- ///list of all the z level datums created representing the z levels in the world
- var/list/z_list
+ ///list of all z level datums in the order of their z (z level 1 is at index 1, etc.)
+ var/list/datum/space_level/z_list
+ ///list of all z level indices that form multiz connections and whether theyre linked up or down
+ ///list of lists, inner lists are of the form: list("up or down link direction" = TRUE)
+ var/list/multiz_levels = list()
var/datum/space_level/transit
var/datum/space_level/empty_space
var/num_of_res_levels = 1
/// True when in the process of adding a new Z-level, global locking
var/adding_new_zlevel = FALSE
+ ///shows the default gravity value for each z level. recalculated when gravity generators change.
+ ///associative list of the form: list("[z level num]" = max generator gravity in that z level OR the gravity level trait)
+ var/list/gravity_by_z_level = list()
+
/datum/controller/subsystem/mapping/New()
..()
#ifdef FORCE_MAP
@@ -101,8 +108,48 @@ SUBSYSTEM_DEF(mapping)
generate_station_area_list()
initialize_reserved_level(transit.z_value)
SSticker.OnRoundstart(CALLBACK(src, .proc/spawn_maintenance_loot))
+ generate_z_level_linkages()
+ calculate_default_z_level_gravities()
+
return ..()
+/datum/controller/subsystem/mapping/proc/calculate_default_z_level_gravities()
+ for(var/z_level in 1 to length(z_list))
+ calculate_z_level_gravity(z_level)
+
+/datum/controller/subsystem/mapping/proc/generate_z_level_linkages()
+ for(var/z_level in 1 to length(z_list))
+ generate_linkages_for_z_level(z_level)
+
+/datum/controller/subsystem/mapping/proc/generate_linkages_for_z_level(z_level)
+ if(!isnum(z_level) || z_level <= 0)
+ return FALSE
+
+ if(multiz_levels.len < z_level)
+ multiz_levels.len = z_level
+
+ var/linked_down = level_trait(z_level, ZTRAIT_DOWN)
+ var/linked_up = level_trait(z_level, ZTRAIT_UP)
+ multiz_levels[z_level] = list()
+ if(linked_down)
+ multiz_levels[z_level]["[DOWN]"] = TRUE
+ if(linked_up)
+ multiz_levels[z_level]["[UP]"] = TRUE
+
+/datum/controller/subsystem/mapping/proc/calculate_z_level_gravity(z_level_number)
+ if(!isnum(z_level_number) || z_level_number < 1)
+ return FALSE
+
+ var/max_gravity = 0
+
+ for(var/obj/machinery/gravity_generator/main/grav_gen as anything in GLOB.gravity_generators["[z_level_number]"])
+ max_gravity = max(grav_gen.setting, max_gravity)
+
+ max_gravity = max_gravity || level_trait(z_level_number, ZTRAIT_GRAVITY) || 0//just to make sure no nulls
+ gravity_by_z_level["[z_level_number]"] = max_gravity
+ return max_gravity
+
+
/**
* ##setup_ruins
*
@@ -215,6 +262,7 @@ Used by the AI doomsday and the self-destruct nuke.
clearing_reserved_turfs = SSmapping.clearing_reserved_turfs
z_list = SSmapping.z_list
+ multiz_levels = SSmapping.multiz_levels
#define INIT_ANNOUNCE(X) to_chat(world, span_boldannounce("[X]")); log_world(X)
/datum/controller/subsystem/mapping/proc/LoadGroup(list/errorList, name, path, files, list/traits, list/default_traits, silent = FALSE)
diff --git a/code/controllers/subsystem/mobs.dm b/code/controllers/subsystem/mobs.dm
index f96ae843e5217..da5a48f46dab5 100644
--- a/code/controllers/subsystem/mobs.dm
+++ b/code/controllers/subsystem/mobs.dm
@@ -6,6 +6,7 @@ SUBSYSTEM_DEF(mobs)
wait = 2 SECONDS
var/list/currentrun = list()
+ ///only contains living players for some reason
var/static/list/clients_by_zlevel[][]
var/static/list/dead_players_by_zlevel[][] = list(list()) // Needs to support zlevel 1 here, MaxZChanged only happens when z2 is created and new_players can login before that.
var/static/list/cubemonkeys = list()
diff --git a/code/controllers/subsystem/nightshift.dm b/code/controllers/subsystem/nightshift.dm
index 1e8daad6e61f9..f420185ec5e08 100644
--- a/code/controllers/subsystem/nightshift.dm
+++ b/code/controllers/subsystem/nightshift.dm
@@ -27,7 +27,7 @@ SUBSYSTEM_DEF(nightshift)
priority_announce(message, sound='sound/misc/notice2.ogg', sender_override="Automated Lighting System Announcement")
/datum/controller/subsystem/nightshift/proc/check_nightshift()
- var/emergency = SSsecurity_level.current_level >= SEC_LEVEL_RED
+ var/emergency = SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED
var/announcing = TRUE
var/time = station_time()
var/night_time = (time < nightshift_end_time) || (time > nightshift_start_time)
diff --git a/code/controllers/subsystem/overlays.dm b/code/controllers/subsystem/overlays.dm
index bc38254f1d39a..34e22e1bde228 100644
--- a/code/controllers/subsystem/overlays.dm
+++ b/code/controllers/subsystem/overlays.dm
@@ -43,17 +43,21 @@ SUBSYSTEM_DEF(overlays)
count++
if(!atom_to_compile)
continue
- if(length(atom_to_compile.overlays) >= MAX_ATOM_OVERLAYS)
- //Break it real GOOD
- stack_trace("Too many overlays on [atom_to_compile.type] - [length(atom_to_compile.overlays)], refusing to update and cutting")
- atom_to_compile.overlays.Cut()
- continue
STAT_START_STOPWATCH
COMPILE_OVERLAYS(atom_to_compile)
UNSETEMPTY(atom_to_compile.add_overlays)
UNSETEMPTY(atom_to_compile.remove_overlays)
STAT_STOP_STOPWATCH
STAT_LOG_ENTRY(stats, atom_to_compile.type)
+ if(length(atom_to_compile.overlays) >= MAX_ATOM_OVERLAYS)
+ //Break it real GOOD
+ var/text_lays = overlays2text(atom_to_compile.overlays)
+ stack_trace("Too many overlays on [atom_to_compile.type] - [length(atom_to_compile.overlays)], refusing to update and cutting.\
+ \n What follows is a printout of all existing overlays at the time of the overflow \n[text_lays]")
+ atom_to_compile.overlays.Cut()
+ //Let them know they fucked up
+ atom_to_compile.add_overlay(mutable_appearance('icons/testing/greyscale_error.dmi'))
+ continue
if(mc_check)
if(MC_TICK_CHECK)
break
@@ -63,6 +67,19 @@ SUBSYSTEM_DEF(overlays)
queue.Cut(1,count+1)
count = 0
+/// Converts an overlay list into text for debug printing
+/// Of note: overlays aren't actually mutable appearances, they're just appearances
+/// Don't have access to that type tho, so this is the best you're gonna get
+/proc/overlays2text(list/overlays)
+ var/list/unique_overlays = list()
+ // As anything because we're basically doing type coerrsion, rather then actually filtering for mutable apperances
+ for(var/mutable_appearance/overlay as anything in overlays)
+ var/key = "[overlay.icon]-[overlay.icon_state]-[overlay.dir]"
+ unique_overlays[key] += 1
+ var/list/output_text = list()
+ for(var/key in unique_overlays)
+ output_text += "([key]) = [unique_overlays[key]]"
+ return output_text.Join("\n")
/proc/iconstate2appearance(icon, iconstate)
var/static/image/stringbro = new()
diff --git a/code/controllers/subsystem/persistence.dm b/code/controllers/subsystem/persistence.dm
index ae7c7548cb2b6..03308d1247a8c 100644
--- a/code/controllers/subsystem/persistence.dm
+++ b/code/controllers/subsystem/persistence.dm
@@ -407,14 +407,7 @@ SUBSYSTEM_DEF(persistence)
for(var/randomized_type in subtypesof(/datum/chemical_reaction/randomized))
var/datum/chemical_reaction/randomized/R = get_chemical_reaction(randomized_type) //ew, would be nice to add some simple tracking
if(R?.persistent)
- var/recipe_data = list()
- recipe_data["timestamp"] = R.created
- recipe_data["required_reagents"] = R.required_reagents
- recipe_data["required_catalysts"] = R.required_catalysts
- recipe_data["required_temp"] = R.required_temp
- recipe_data["is_cold_recipe"] = R.is_cold_recipe
- recipe_data["results"] = R.results
- recipe_data["required_container"] = "[R.required_container]"
+ var/list/recipe_data = R.SaveOldRecipe()
file_data["[R.type]"] = recipe_data
fdel(json_file)
diff --git a/code/controllers/subsystem/processing/supermatter_cascade.dm b/code/controllers/subsystem/processing/supermatter_cascade.dm
index ddd5ef09b38ff..a6fbc3de46be1 100644
--- a/code/controllers/subsystem/processing/supermatter_cascade.dm
+++ b/code/controllers/subsystem/processing/supermatter_cascade.dm
@@ -2,3 +2,6 @@ PROCESSING_SUBSYSTEM_DEF(supermatter_cascade)
name = "Supermatter Cascade"
wait = 0.5 SECONDS
stat_tag = "SC"
+
+ ///Is a cascade happening right now?
+ var/cascade_initiated = FALSE
diff --git a/code/controllers/subsystem/processing/tramprocess.dm b/code/controllers/subsystem/processing/tramprocess.dm
index f72ba30ddeeb5..b497cce8b8caf 100644
--- a/code/controllers/subsystem/processing/tramprocess.dm
+++ b/code/controllers/subsystem/processing/tramprocess.dm
@@ -1,5 +1,15 @@
PROCESSING_SUBSYSTEM_DEF(tramprocess)
name = "Tram Process"
- wait = 1
+ wait = 0.5
/// only used on maps with trams, so only enabled by such.
can_fire = FALSE
+
+ ///how much time a tram can take per movement before we notify admins and slow down the tram. in milliseconds
+ var/max_time = 15
+
+ ///how many times the tram can move costing over max_time milliseconds before it gets slowed down
+ var/max_exceeding_moves = 5
+
+ ///how many times the tram can move costing less than half max_time milliseconds before we speed it back up again.
+ ///is only used if the tram has been slowed down for exceeding max_time
+ var/max_cheap_moves = 5
diff --git a/code/controllers/subsystem/research.dm b/code/controllers/subsystem/research.dm
index 67e84acde7613..f16ffe6e86c62 100644
--- a/code/controllers/subsystem/research.dm
+++ b/code/controllers/subsystem/research.dm
@@ -14,18 +14,27 @@ SUBSYSTEM_DEF(research)
var/datum/design/error_design/error_design
//ERROR LOGGING
- var/list/invalid_design_ids = list() //associative id = number of times
- var/list/invalid_node_ids = list() //associative id = number of times
- var/list/invalid_node_boost = list() //associative id = error message
+ ///associative id = number of times
+ var/list/invalid_design_ids = list()
+ ///associative id = number of times
+ var/list/invalid_node_ids = list()
+ ///associative id = error message
+ var/list/invalid_node_boost = list()
var/list/obj/machinery/rnd/server/servers = list()
- var/list/techweb_nodes_starting = list() //associative id = TRUE
- var/list/techweb_categories = list() //category name = list(node.id = TRUE)
- var/list/techweb_boost_items = list() //associative double-layer path = list(id = list(point_type = point_discount))
- var/list/techweb_nodes_hidden = list() //Node ids that should be hidden by default.
- var/list/techweb_nodes_experimental = list() //Node ids that are exclusive to the BEPIS.
- var/list/techweb_point_items = list( //path = list(point type = value)
+ ///associative id = TRUE
+ var/list/techweb_nodes_starting = list()
+ ///category name = list(node.id = TRUE)
+ var/list/techweb_categories = list()
+ ///associative double-layer path = list(id = list(point_type = point_discount))
+ var/list/techweb_boost_items = list()
+ ///Node ids that should be hidden by default.
+ var/list/techweb_nodes_hidden = list()
+ ///Node ids that are exclusive to the BEPIS.
+ var/list/techweb_nodes_experimental = list()
+ ///path = list(point type = value)
+ var/list/techweb_point_items = list(
/obj/item/assembly/signaler/anomaly = list(TECHWEB_POINT_TYPE_GENERIC = 10000)
)
var/list/errored_datums = list()
@@ -37,8 +46,8 @@ SUBSYSTEM_DEF(research)
/// A list of all master servers. If none of these have a source code HDD, research point generation is lowered.
var/list/obj/machinery/rnd/server/master/master_servers = list()
- /// The multiplier to research points when no source code HDD is present.
- var/no_source_code_income_modifier = 0.5
+ /// A multiplier applied to all research gain.
+ var/income_modifier = 1
//Aiming for 1.5 hours to max R&D
//[88nodes * 5000points/node] / [1.5hr * 90min/hr * 60s/min]
@@ -54,7 +63,7 @@ SUBSYSTEM_DEF(research)
/obj/item/assembly/signaler/anomaly/vortex = MAX_CORES_VORTEX,
/obj/item/assembly/signaler/anomaly/flux = MAX_CORES_FLUX,
/obj/item/assembly/signaler/anomaly/hallucination = MAX_CORES_HALLUCINATION,
- /obj/item/assembly/signaler/anomaly/delimber = MAX_CORES_DELIMBER,
+ /obj/item/assembly/signaler/anomaly/bioscrambler = MAX_CORES_BIOSCRAMBLER,
)
/// Lookup list for ordnance briefers.
@@ -81,20 +90,13 @@ SUBSYSTEM_DEF(research)
bitcoins = single_server_income.Copy()
break //Just need one to work.
- // Check if any master server has a source code HDD in it or if all master servers have just been plain old blown up.
- // Start by assuming no source code, then set the modifier to 1 if we find one.
- var/bitcoin_multiplier = no_source_code_income_modifier
- for(var/obj/machinery/rnd/server/master/master_server as anything in master_servers)
- if(master_server.source_code_hdd)
- bitcoin_multiplier = 1
- break
-
if (!isnull(last_income))
var/income_time_difference = world.time - last_income
science_tech.last_bitcoins = bitcoins // Doesn't take tick drift into account
for(var/i in bitcoins)
- bitcoins[i] *= (income_time_difference / 10) * bitcoin_multiplier
+ bitcoins[i] *= (income_time_difference / 10) * income_modifier
science_tech.add_point_list(bitcoins)
+
last_income = world.time
/datum/controller/subsystem/research/proc/calculate_server_coefficient() //Diminishing returns.
diff --git a/code/controllers/subsystem/security_level.dm b/code/controllers/subsystem/security_level.dm
index e3168dee0c39e..94dc351bdeca5 100644
--- a/code/controllers/subsystem/security_level.dm
+++ b/code/controllers/subsystem/security_level.dm
@@ -1,17 +1,109 @@
SUBSYSTEM_DEF(security_level)
name = "Security Level"
- flags = SS_NO_FIRE
+ can_fire = FALSE // We will control when we fire in this subsystem
+ init_order = INIT_ORDER_SECURITY_LEVEL
/// Currently set security level
- var/current_level = SEC_LEVEL_GREEN
+ var/datum/security_level/current_security_level
+ /// A list of initialised security level datums.
+ var/list/available_levels = list()
+
+/datum/controller/subsystem/security_level/Initialize(start_timeofday)
+ . = ..()
+ for(var/iterating_security_level_type in subtypesof(/datum/security_level))
+ var/datum/security_level/new_security_level = new iterating_security_level_type
+ available_levels[new_security_level.name] = new_security_level
+ current_security_level = available_levels[number_level_to_text(SEC_LEVEL_GREEN)]
+
+/datum/controller/subsystem/security_level/fire(resumed)
+ if(!current_security_level.looping_sound) // No sound? No play.
+ can_fire = FALSE
+ return
+ sound_to_playing_players(current_security_level.looping_sound)
+
/**
* Sets a new security level as our current level
*
+ * This is how everything should change the security level.
+ *
* Arguments:
- * * new_level The new security level that will become our current level
+ * * new_level - The new security level that will become our current level
*/
/datum/controller/subsystem/security_level/proc/set_level(new_level)
- SSsecurity_level.current_level = new_level
- SEND_SIGNAL(src, COMSIG_SECURITY_LEVEL_CHANGED, new_level)
+ new_level = istext(new_level) ? new_level : number_level_to_text(new_level)
+ if(new_level == current_security_level.name) // If we are already at the desired level, do nothing
+ return
+
+ var/datum/security_level/selected_level = available_levels[new_level]
+
+ if(!selected_level)
+ CRASH("set_level was called with an invalid security level([new_level])")
+
+ announce_security_level(selected_level) // We want to announce BEFORE updating to the new level
+
+ var/old_shuttle_call_time_mod = current_security_level.shuttle_call_time_mod // Need this before we set the new one
+
+ SSsecurity_level.current_security_level = selected_level
+
+ if(selected_level.looping_sound)
+ wait = selected_level.looping_sound_interval
+ can_fire = TRUE
+ else
+ can_fire = FALSE
+
+ if(SSshuttle.emergency.mode == SHUTTLE_CALL || SSshuttle.emergency.mode == SHUTTLE_RECALL) // By god this is absolutely shit
+ old_shuttle_call_time_mod = 1 / old_shuttle_call_time_mod
+ SSshuttle.emergency.modTimer(old_shuttle_call_time_mod)
+ SSshuttle.emergency.modTimer(selected_level.shuttle_call_time_mod)
+
+ SEND_SIGNAL(src, COMSIG_SECURITY_LEVEL_CHANGED, selected_level.number_level)
SSnightshift.check_nightshift()
- SSblackbox.record_feedback("tally", "security_level_changes", 1, get_security_level())
+ SSblackbox.record_feedback("tally", "security_level_changes", 1, selected_level.name)
+
+/**
+ * Handles announcements of the newly set security level
+ *
+ * Arguments:
+ * * selected_level - The new security level that has been set
+ */
+/datum/controller/subsystem/security_level/proc/announce_security_level(datum/security_level/selected_level)
+ if(selected_level.number_level > current_security_level.number_level) // We are elevating to this level.
+ minor_announce(selected_level.elevating_to_announcemnt, "Attention! Security level elevated to [selected_level.name]:")
+ else // Going down
+ minor_announce(selected_level.lowering_to_announcement, "Attention! Security level lowered to [selected_level.name]:")
+ if(selected_level.sound)
+ sound_to_playing_players(selected_level.sound)
+
+/**
+ * Returns the current security level as a number
+ */
+/datum/controller/subsystem/security_level/proc/get_current_level_as_number()
+ return current_security_level.number_level
+
+/**
+ * Returns the current security level as text
+ */
+/datum/controller/subsystem/security_level/proc/get_current_level_as_text()
+ return current_security_level.name
+
+/**
+ * Converts a text security level to a number
+ *
+ * Arguments:
+ * * level - The text security level to convert
+ */
+/datum/controller/subsystem/security_level/proc/text_level_to_number(text_level)
+ var/datum/security_level/selected_level = available_levels[text_level]
+ return selected_level.number_level
+
+/**
+ * Converts a number security level to a text
+ *
+ * Arguments:
+ * * level - The number security level to convert
+ */
+/datum/controller/subsystem/security_level/proc/number_level_to_text(number_level)
+ for(var/iterating_level_text in available_levels)
+ var/datum/security_level/iterating_security_level = available_levels[iterating_level_text]
+ if(iterating_security_level.number_level == number_level)
+ return iterating_security_level.name
diff --git a/code/controllers/subsystem/shuttle.dm b/code/controllers/subsystem/shuttle.dm
index 084b0aa60ead9..390de70f33cc2 100644
--- a/code/controllers/subsystem/shuttle.dm
+++ b/code/controllers/subsystem/shuttle.dm
@@ -307,13 +307,13 @@ SUBSYSTEM_DEF(shuttle)
call_reason = trim(html_encode(call_reason))
- if(length(call_reason) < CALL_SHUTTLE_REASON_LENGTH && seclevel2num(get_security_level()) > SEC_LEVEL_GREEN)
+ if(length(call_reason) < CALL_SHUTTLE_REASON_LENGTH && SSsecurity_level.get_current_level_as_number() > SEC_LEVEL_GREEN)
to_chat(user, span_alert("You must provide a reason."))
return
var/area/signal_origin = get_area(user)
var/emergency_reason = "\nNature of emergency:\n\n[call_reason]"
- var/security_num = seclevel2num(get_security_level())
+ var/security_num = SSsecurity_level.get_current_level_as_number()
switch(security_num)
if(SEC_LEVEL_RED,SEC_LEVEL_DELTA)
emergency.request(null, signal_origin, html_decode(emergency_reason), 1) //There is a serious threat we gotta move no time to give them five minutes.
@@ -376,7 +376,7 @@ SUBSYSTEM_DEF(shuttle)
/datum/controller/subsystem/shuttle/proc/canRecall()
if(!emergency || emergency.mode != SHUTTLE_CALL || admin_emergency_no_recall || emergency_no_recall)
return
- var/security_num = seclevel2num(get_security_level())
+ var/security_num = SSsecurity_level.get_current_level_as_number()
switch(security_num)
if(SEC_LEVEL_GREEN)
if(emergency.timeLeft(1) < emergency_call_time)
diff --git a/code/controllers/subsystem/spatial_gridmap.dm b/code/controllers/subsystem/spatial_gridmap.dm
index 0608140bb7cf3..b9dc0988840db 100644
--- a/code/controllers/subsystem/spatial_gridmap.dm
+++ b/code/controllers/subsystem/spatial_gridmap.dm
@@ -171,53 +171,6 @@ SUBSYSTEM_DEF(spatial_grid)
var/datum/spatial_grid_cell/cell = new(x, y, z_level.z_value)
new_cell_grid[y] += cell
-///creates number_to_generate new oranges_ear's and adds them to the subsystems list of ears.
-///i really fucking hope this never gets called after init :clueless:
-/datum/controller/subsystem/spatial_grid/proc/pregenerate_more_oranges_ears(number_to_generate)
- for(var/new_ear in 1 to number_to_generate)
- pregenerated_oranges_ears += new/mob/oranges_ear(null)
-
- number_of_oranges_ears = length(pregenerated_oranges_ears)
-
-///allocate one [/mob/oranges_ear] mob per turf containing atoms_that_need_ears and give them a reference to every listed atom in their turf.
-///if an oranges_ear is allocated to a turf that already has an oranges_ear then the second one fails to allocate (and gives the existing one the atom it was assigned to)
-/datum/controller/subsystem/spatial_grid/proc/assign_oranges_ears(list/atoms_that_need_ears)
- var/input_length = length(atoms_that_need_ears)
-
- if(input_length > number_of_oranges_ears)
- stack_trace("somehow, for some reason, more than the preset generated number of oranges ears was requested. thats fucking [number_of_oranges_ears]. this is not good that should literally never happen")
- pregenerate_more_oranges_ears(input_length - number_of_oranges_ears)//im still gonna DO IT but ill complain about it
-
- . = list()
-
- ///the next unallocated /mob/oranges_ear that we try to allocate to assigned_atom's turf
- var/mob/oranges_ear/current_ear
- ///the next atom in atoms_that_need_ears an ear assigned to it
- var/atom/assigned_atom
- ///the turf loc of the current assigned_atom. turfs are used to track oranges_ears already assigned to one location so we dont allocate more than one
- ///because allocating more than one oranges_ear to a given loc wastes view iterations
- var/turf/turf_loc
-
- for(var/current_ear_index in 1 to input_length)
- assigned_atom = atoms_that_need_ears[current_ear_index]
-
- turf_loc = get_turf(assigned_atom)
- if(!turf_loc)
- continue
-
- current_ear = pregenerated_oranges_ears[current_ear_index]
-
- if(turf_loc.assigned_oranges_ear)
- turf_loc.assigned_oranges_ear.references += assigned_atom
- continue //if theres already an oranges_ear mob at assigned_movable's turf we give assigned_movable to it instead and dont allocate ourselves
-
- current_ear.references += assigned_atom
-
- current_ear.loc = turf_loc //normally this is bad, but since this is meant to be as fast as possible we literally just need to exist there for view() to see us
- turf_loc.assigned_oranges_ear = current_ear
-
- . += current_ear
-
///adds cells to the grid for every z level when world.maxx or world.maxy is expanded after this subsystem is initialized. hopefully this is never needed.
///because i never tested this.
/datum/controller/subsystem/spatial_grid/proc/after_world_bounds_expanded(datum/controller/subsystem/processing/dcs/fucking_dcs, has_expanded_world_maxx, has_expanded_world_maxy)
@@ -278,10 +231,6 @@ SUBSYSTEM_DEF(spatial_grid)
. = list()
- //cache for sanic speeds
- var/cells_on_y_axis = src.cells_on_y_axis
- var/cells_on_x_axis = src.cells_on_x_axis
-
//technically THIS list only contains lists, but inside those lists are grid cell datums and we can go without a SINGLE var init if we do this
var/list/datum/spatial_grid_cell/grid_level = grids_by_z_level[center_turf.z]
@@ -458,6 +407,8 @@ SUBSYSTEM_DEF(spatial_grid)
GRID_CELL_SET(intersecting_cell.atmos_contents, new_target)
SEND_SIGNAL(intersecting_cell, SPATIAL_GRID_CELL_ENTERED(SPATIAL_GRID_CONTENTS_TYPE_ATMOS), new_target)
+ return intersecting_cell
+
/**
* find the spatial map cell that target used to belong to, then remove the target (and sometimes it's important_recusive_contents) from it.
* make sure to provide the turf old_target used to be "in"
@@ -584,6 +535,53 @@ SUBSYSTEM_DEF(spatial_grid)
message_admins(cell_coords)
message_admins("[src] is supposed to only be contained in the cell at indexes ([real_cell.cell_x], [real_cell.cell_y], [real_cell.cell_z]). but is contained at the cells at [cell_coords]")
+///creates number_to_generate new oranges_ear's and adds them to the subsystems list of ears.
+///i really fucking hope this never gets called after init :clueless:
+/datum/controller/subsystem/spatial_grid/proc/pregenerate_more_oranges_ears(number_to_generate)
+ for(var/new_ear in 1 to number_to_generate)
+ pregenerated_oranges_ears += new/mob/oranges_ear(null)
+
+ number_of_oranges_ears = length(pregenerated_oranges_ears)
+
+///allocate one [/mob/oranges_ear] mob per turf containing atoms_that_need_ears and give them a reference to every listed atom in their turf.
+///if an oranges_ear is allocated to a turf that already has an oranges_ear then the second one fails to allocate (and gives the existing one the atom it was assigned to)
+/datum/controller/subsystem/spatial_grid/proc/assign_oranges_ears(list/atoms_that_need_ears)
+ var/input_length = length(atoms_that_need_ears)
+
+ if(input_length > number_of_oranges_ears)
+ stack_trace("somehow, for some reason, more than the preset generated number of oranges ears was requested. thats fucking [number_of_oranges_ears]. this is not good that should literally never happen")
+ pregenerate_more_oranges_ears(input_length - number_of_oranges_ears)//im still gonna DO IT but ill complain about it
+
+ . = list()
+
+ ///the next unallocated /mob/oranges_ear that we try to allocate to assigned_atom's turf
+ var/mob/oranges_ear/current_ear
+ ///the next atom in atoms_that_need_ears an ear assigned to it
+ var/atom/assigned_atom
+ ///the turf loc of the current assigned_atom. turfs are used to track oranges_ears already assigned to one location so we dont allocate more than one
+ ///because allocating more than one oranges_ear to a given loc wastes view iterations
+ var/turf/turf_loc
+
+ for(var/current_ear_index in 1 to input_length)
+ assigned_atom = atoms_that_need_ears[current_ear_index]
+
+ turf_loc = get_turf(assigned_atom)
+ if(!turf_loc)
+ continue
+
+ current_ear = pregenerated_oranges_ears[current_ear_index]
+
+ if(turf_loc.assigned_oranges_ear)
+ turf_loc.assigned_oranges_ear.references += assigned_atom
+ continue //if theres already an oranges_ear mob at assigned_movable's turf we give assigned_movable to it instead and dont allocate ourselves
+
+ current_ear.references += assigned_atom
+
+ current_ear.loc = turf_loc //normally this is bad, but since this is meant to be as fast as possible we literally just need to exist there for view() to see us
+ turf_loc.assigned_oranges_ear = current_ear
+
+ . += current_ear
+
///debug proc for finding how full the cells of src's z level are
/atom/proc/find_grid_statistics_for_z_level(insert_clients = 0)
var/raw_clients = 0
diff --git a/code/controllers/subsystem/statpanel.dm b/code/controllers/subsystem/statpanel.dm
index c16c6914fc0d3..be5272092c5e0 100644
--- a/code/controllers/subsystem/statpanel.dm
+++ b/code/controllers/subsystem/statpanel.dm
@@ -73,10 +73,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))
@@ -148,14 +160,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()
@@ -221,10 +234,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/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index f9c77b67d7fbd..55594cab8d58b 100755
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -598,7 +598,7 @@ SUBSYSTEM_DEF(ticker)
if(GANG_DESTROYED)
news_message = "The crew of [decoded_station_name] would like to thank the Spinward Stellar Coalition Police Department for quickly resolving a minor terror threat to the station."
if(SUPERMATTER_CASCADE)
- news_message = "Recovery of the surviving crew of [decoded_station_name] is underway following a major supermatter cascade."
+ news_message = "Officials are advising nearby colonies about a newly declared exclusion zone in the sector surrounding [decoded_station_name]."
if(news_message)
send2otherserver(news_source, news_message,"News_Report")
diff --git a/code/controllers/subsystem/timer.dm b/code/controllers/subsystem/timer.dm
index 791677903ab6f..704e7fb19e006 100644
--- a/code/controllers/subsystem/timer.dm
+++ b/code/controllers/subsystem/timer.dm
@@ -565,6 +565,7 @@ SUBSYSTEM_DEF(timer)
* * callback the callback to call on timer finish
* * wait deciseconds to run the timer for
* * flags flags for this timer, see: code\__DEFINES\subsystems.dm
+ * * timer_subsystem the subsystem to insert this timer into
*/
/proc/_addtimer(datum/callback/callback, wait = 0, flags = 0, datum/controller/subsystem/timer/timer_subsystem, file, line)
if (!callback)
diff --git a/code/controllers/subsystem/traitor.dm b/code/controllers/subsystem/traitor.dm
index cbbba49d086a2..b5b85da5deb74 100644
--- a/code/controllers/subsystem/traitor.dm
+++ b/code/controllers/subsystem/traitor.dm
@@ -37,8 +37,8 @@ SUBSYSTEM_DEF(traitor)
var/generate_objectives = TRUE
/// Objectives that have been completed by type. Used for limiting objectives.
var/list/taken_objectives_by_type = list()
- /// Contains 3 areas: 2 areas to scan in order to triangulate the third one which is the structural weakpoint itself
- var/list/station_weakpoints = list()
+ /// A list of all existing objectives by type
+ var/list/all_objectives_by_type = list()
/datum/controller/subsystem/traitor/Initialize(start_timeofday)
. = ..()
@@ -53,31 +53,6 @@ SUBSYSTEM_DEF(traitor)
log_world("[configuration_path] has an invalid type ([typepath]) that doesn't exist in the codebase! Please correct or remove [typepath]")
configuration_data[actual_typepath] = data[typepath]
- /// List of high-security areas that we pick required ones from
- var/list/allowed_areas = typecacheof(list(/area/station/command,
- /area/station/cargo/qm,
- /area/station/comms,
- /area/station/engineering,
- /area/station/science,
- /area/station/security,
- ))
-
- var/list/blacklisted_areas = typecacheof(list(/area/station/engineering/hallway,
- /area/station/engineering/lobby,
- /area/station/engineering/storage,
- /area/station/science/lobby,
- /area/station/science/test_area,
- /area/station/security/prison,
- ))
-
- var/list/possible_areas = GLOB.the_station_areas.Copy()
- for(var/area/possible_area as anything in possible_areas)
- if(!is_type_in_typecache(possible_area, allowed_areas) || initial(possible_area.outdoors) || is_type_in_typecache(possible_area, blacklisted_areas))
- possible_areas -= possible_area
-
- for(var/i in 1 to 3)
- station_weakpoints += pick_n_take(possible_areas)
-
/datum/controller/subsystem/traitor/fire(resumed)
var/player_count = length(GLOB.alive_player_list)
// Has a maximum of 1 minute, however the value can be lower if there are lower players than the ideal
@@ -124,13 +99,17 @@ SUBSYSTEM_DEF(traitor)
if(!istype(objective))
return
+ add_objective_to_list(objective, taken_objectives_by_type)
+
+/datum/controller/subsystem/traitor/proc/get_taken_count(datum/traitor_objective/objective_type)
+ return length(taken_objectives_by_type[objective_type])
+
+
+/datum/controller/subsystem/traitor/proc/add_objective_to_list(datum/traitor_objective/objective, list/objective_list)
var/datum/traitor_objective/current_type = objective.type
while(current_type != /datum/traitor_objective)
- if(!taken_objectives_by_type[current_type])
- taken_objectives_by_type[current_type] = list(objective)
+ if(!objective_list[current_type])
+ objective_list[current_type] = list(objective)
else
- taken_objectives_by_type[current_type] += objective
+ objective_list[current_type] += objective
current_type = type2parent(current_type)
-
-/datum/controller/subsystem/traitor/proc/get_taken_count(datum/traitor_objective/objective_type)
- return length(taken_objectives_by_type[objective_type])
diff --git a/code/datums/action.dm b/code/datums/action.dm
deleted file mode 100644
index d56c23717826c..0000000000000
--- a/code/datums/action.dm
+++ /dev/null
@@ -1,920 +0,0 @@
-/datum/action
- var/name = "Generic Action"
- var/desc
- var/datum/target
- var/check_flags = NONE
- var/processing = FALSE
- var/buttontooltipstyle = ""
- var/transparent_when_unavailable = TRUE
- /// Where any buttons we create should be by default. Accepts screen_loc and location defines
- var/default_button_position = SCRN_OBJ_IN_LIST
-
- var/button_icon = 'icons/mob/actions/backgrounds.dmi' //This is the file for the BACKGROUND icon
- var/background_icon_state = ACTION_BUTTON_DEFAULT_BACKGROUND //And this is the state for the background icon
-
- var/icon_icon = 'icons/hud/actions.dmi' //This is the file for the ACTION icon
- var/button_icon_state = "default" //And this is the state for the action icon
- var/mob/owner
- ///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)
-
-/datum/action/proc/link_to(Target)
- target = Target
- RegisterSignal(target, COMSIG_ATOM_UPDATED_ICON, .proc/OnUpdatedIcon)
- RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/clear_ref, override = TRUE)
-
-/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 ..()
-
-/datum/action/proc/Grant(mob/M)
- if(!M)
- Remove(owner)
- return
- if(owner)
- if(owner == M)
- return
- Remove(owner)
- owner = M
- RegisterSignal(owner, COMSIG_PARENT_QDELETING, .proc/clear_ref, override = TRUE)
-
- GiveAction(M)
-
-/datum/action/proc/clear_ref(datum/ref)
- SIGNAL_HANDLER
- if(ref == owner)
- Remove(owner)
- if(ref == target)
- qdel(src)
-
-/datum/action/proc/Remove(mob/M)
- for(var/datum/hud/hud in viewers)
- if(!hud.mymob)
- continue
- HideFrom(hud.mymob)
- LAZYREMOVE(M.actions, src) // We aren't always properly inserted into the viewers list, gotta make sure that action's cleared
- viewers = list()
-
- if(owner)
- UnregisterSignal(owner, COMSIG_PARENT_QDELETING)
- if(target == owner)
- RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/clear_ref)
- owner = null
-
-/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
-
-
-/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)
-
- if(!IsAvailable())
- button.color = transparent_when_unavailable ? rgb(128,0,0,128) : rgb(128,0,0)
- else
- button.color = rgb(255,255,255,255)
- return TRUE
-
-/datum/action/proc/ApplyIcon(atom/movable/screen/movable/action_button/current_button, force = FALSE)
- if(icon_icon && button_icon_state && ((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
-
-/datum/action/proc/OnUpdatedIcon()
- SIGNAL_HANDLER
- UpdateButtons()
-
-//Give our action button to the player
-/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 a player
-/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 button from the screen of a player
-/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)
-
-/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
-
-//Presets for item actions
-/datum/action/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/New(Target)
- ..()
- var/obj/item/I = target
- LAZYINITLIST(I.actions)
- I.actions += src
-
-/datum/action/item_action/Destroy()
- var/obj/item/I = target
- I.actions -= src
- UNSETEMPTY(I.actions)
- return ..()
-
-/datum/action/item_action/Trigger(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
-
-/datum/action/item_action/toggle_light
- name = "Toggle Light"
-
-/datum/action/item_action/toggle_light/Trigger(trigger_flags)
- if(istype(target, /obj/item/modular_computer))
- var/obj/item/modular_computer/mc = target
- mc.toggle_flashlight()
- return
- ..()
-
-/datum/action/item_action/toggle_hood
- name = "Toggle Hood"
-
-/datum/action/item_action/toggle_firemode
- name = "Toggle Firemode"
-
-/datum/action/item_action/rcl_col
- name = "Change Cable Color"
- icon_icon = 'icons/mob/actions/actions_items.dmi'
- button_icon_state = "rcl_rainbow"
-
-/datum/action/item_action/rcl_gui
- name = "Toggle Fast Wiring Gui"
- icon_icon = 'icons/mob/actions/actions_items.dmi'
- button_icon_state = "rcl_gui"
-
-/datum/action/item_action/startchainsaw
- name = "Pull The Starting Cord"
-
-/datum/action/item_action/toggle_computer_light
- name = "Toggle Flashlight"
-
-/datum/action/item_action/toggle_gunlight
- name = "Toggle Gunlight"
-
-/datum/action/item_action/toggle_mode
- name = "Toggle Mode"
-
-/datum/action/item_action/toggle_barrier_spread
- name = "Toggle Barrier Spread"
-
-/datum/action/item_action/equip_unequip_ted_gun
- name = "Equip/Unequip TED Gun"
-
-/datum/action/item_action/toggle_paddles
- name = "Toggle Paddles"
-
-/datum/action/item_action/set_internals
- name = "Set Internals"
-
-/datum/action/item_action/set_internals/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force)
- if(!..()) // no button available
- return
- if(!iscarbon(owner))
- return
- var/mob/living/carbon/C = owner
- if(target == C.internal)
- button.icon_state = "template_active"
-
-/datum/action/item_action/pick_color
- name = "Choose A Color"
-
-/datum/action/item_action/toggle_mister
- name = "Toggle Mister"
-
-/datum/action/item_action/activate_injector
- name = "Activate Injector"
-
-/datum/action/item_action/toggle_helmet_light
- name = "Toggle Helmet Light"
-
-/datum/action/item_action/toggle_welding_screen
- name = "Toggle Welding Screen"
-
-/datum/action/item_action/toggle_welding_screen/Trigger(trigger_flags)
- var/obj/item/clothing/head/hardhat/weldhat/H = target
- if(istype(H))
- H.toggle_welding_screen(owner)
-
-/datum/action/item_action/toggle_welding_screen/plasmaman
- name = "Toggle Welding Screen"
-
-/datum/action/item_action/toggle_welding_screen/plasmaman/Trigger(trigger_flags)
- var/obj/item/clothing/head/helmet/space/plasmaman/H = target
- if(istype(H))
- H.toggle_welding_screen(owner)
-
-/datum/action/item_action/toggle_spacesuit
- name = "Toggle Suit Thermal Regulator"
- icon_icon = 'icons/mob/actions/actions_spacesuit.dmi'
- button_icon_state = "thermal_off"
-
-/datum/action/item_action/toggle_spacesuit/New(Target)
- . = ..()
- RegisterSignal(target, COMSIG_SUIT_SPACE_TOGGLE, .proc/toggle)
-
-/datum/action/item_action/toggle_spacesuit/Destroy()
- UnregisterSignal(target, COMSIG_SUIT_SPACE_TOGGLE)
- return ..()
-
-/datum/action/item_action/toggle_spacesuit/Trigger(trigger_flags)
- var/obj/item/clothing/suit/space/suit = target
- if(!istype(suit))
- return
- suit.toggle_spacesuit()
-
-/// Toggle the action icon for the space suit thermal regulator
-/datum/action/item_action/toggle_spacesuit/proc/toggle(obj/item/clothing/suit/space/suit)
- SIGNAL_HANDLER
-
- button_icon_state = "thermal_[suit.thermal_on ? "on" : "off"]"
- UpdateButtons()
-
-/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.area_flags & NOTELEPORT)
- to_chat(owner, span_notice("[target] fizzles uselessly."))
- return
- if(istype(target, /obj/item/hierophant_club))
- var/obj/item/hierophant_club/H = target
- if(H.teleporting)
- return FALSE
- return ..()
-
-/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
- ..()
-
-/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/crew_monitor
- name = "Interface With Crew Monitor"
-
-/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/halt
- name = "HALT!"
-
-/datum/action/item_action/toggle_voice_box
- name = "Toggle Voice Box"
-
-/datum/action/item_action/change
- name = "Change"
-
-/datum/action/item_action/nano_picket_sign
- name = "Retext Nano Picket Sign"
-
-/datum/action/item_action/nano_picket_sign/Trigger(trigger_flags)
- if(!istype(target, /obj/item/picket_sign))
- return
- var/obj/item/picket_sign/sign = target
- sign.retext(owner)
-
-/datum/action/item_action/adjust
-
-/datum/action/item_action/adjust/New(Target)
- ..()
- var/obj/item/item_target = target
- name = "Adjust [item_target.name]"
-
-/datum/action/item_action/switch_hud
- name = "Switch HUD"
-
-/datum/action/item_action/toggle_human_head
- name = "Toggle Human Head"
-
-/datum/action/item_action/toggle_helmet
- name = "Toggle Helmet"
-
-/datum/action/item_action/toggle_jetpack
- name = "Toggle Jetpack"
-
-/datum/action/item_action/jetpack_stabilization
- name = "Toggle Jetpack Stabilization"
-
-/datum/action/item_action/jetpack_stabilization/IsAvailable()
- var/obj/item/tank/jetpack/J = target
- if(!istype(J) || !J.on)
- return FALSE
- return ..()
-
-/datum/action/item_action/hands_free
- check_flags = AB_CHECK_CONSCIOUS
-
-/datum/action/item_action/hands_free/activate
- name = "Activate"
-
-/datum/action/item_action/hands_free/shift_nerves
- name = "Shift Nerves"
-
-/datum/action/item_action/explosive_implant
- check_flags = NONE
- name = "Activate Explosive Implant"
-
-/datum/action/item_action/instrument
- name = "Use Instrument"
- desc = "Use the instrument specified"
-
-/datum/action/item_action/instrument/Trigger(trigger_flags)
- if(istype(target, /obj/item/instrument))
- var/obj/item/instrument/I = target
- I.interact(usr)
- return
- return ..()
-
-/datum/action/item_action/activate_remote_view
- name = "Activate Remote View"
- desc = "Activates the Remote View of your spy sunglasses."
-
-/datum/action/item_action/organ_action
- check_flags = AB_CHECK_CONSCIOUS
-
-/datum/action/item_action/organ_action/IsAvailable()
- var/obj/item/organ/I = target
- if(!I.owner)
- return FALSE
- return ..()
-
-/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/New(Target)
- ..()
- var/obj/item/organ/organ_target = target
- name = "Use [organ_target.name]"
-
-/datum/action/item_action/cult_dagger
- name = "Draw Blood Rune"
- desc = "Use the ritual dagger to create a powerful blood rune"
- icon_icon = 'icons/mob/actions/actions_cult.dmi'
- button_icon_state = "draw"
- buttontooltipstyle = "cult"
- background_icon_state = "bg_demon"
- default_button_position = "6:157,4:-2"
-
-/datum/action/item_action/cult_dagger/Grant(mob/M)
- if(!IS_CULTIST(M))
- 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!"))
-
-
-///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
- owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE)
- box.open()
- return
- //Box closing from here on out.
- if(!isturf(owner.loc)) //Don't let the player use this to escape mechs/welded closets.
- to_chat(owner, 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/M)
- . = ..()
- 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
-
-//Preset for spells
-/datum/action/spell_action
- check_flags = NONE
- background_icon_state = "bg_spell"
-
-/datum/action/spell_action/New(Target)
- ..()
- var/obj/effect/proc_holder/S = target
- S.action = src
- name = S.name
- desc = S.desc
- icon_icon = S.action_icon
- button_icon_state = S.action_icon_state
- background_icon_state = S.action_background_icon_state
-
-/datum/action/spell_action/Destroy()
- var/obj/effect/proc_holder/S = target
- S.action = null
- return ..()
-
-/datum/action/spell_action/Trigger(trigger_flags)
- if(!..())
- return FALSE
- if(target)
- var/obj/effect/proc_holder/S = target
- S.Click()
- return TRUE
-
-/datum/action/spell_action/IsAvailable()
- if(!target)
- return FALSE
- return TRUE
-
-/datum/action/spell_action/spell
-
-/datum/action/spell_action/spell/IsAvailable()
- if(!target)
- return FALSE
- var/obj/effect/proc_holder/spell/S = target
- if(owner)
- return S.can_cast(owner)
- return FALSE
-
-/datum/action/spell_action/alien
-
-/datum/action/spell_action/alien/IsAvailable()
- if(!target)
- return FALSE
- var/obj/effect/proc_holder/alien/ab = target
- if(owner)
- return ab.cost_check(ab.check_turf,owner,1)
- return FALSE
-
-
-
-//Preset for general and toggled actions
-/datum/action/innate
- check_flags = NONE
- var/active = 0
-
-/datum/action/innate/Trigger(trigger_flags)
- if(!..())
- return FALSE
- if(!active)
- Activate()
- else
- Deactivate()
- return TRUE
-
-/datum/action/innate/proc/Activate()
- return
-
-/datum/action/innate/proc/Deactivate()
- return
-
-//Preset for an action with a cooldown
-
-/datum/action/cooldown
- check_flags = NONE
- transparent_when_unavailable = FALSE
- // The default cooldown applied when StartCooldown() is called
- var/cooldown_time = 0
- // The actual next time this ability can be used
- var/next_use_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
- // 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)
-
-/// 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)
- if(shared_cooldown)
- for(var/datum/action/cooldown/shared_ability in owner.actions - src)
- if(shared_cooldown == shared_ability.shared_cooldown)
- if(isnum(override_cooldown_time))
- shared_ability.StartCooldownSelf(override_cooldown_time)
- else
- shared_ability.StartCooldownSelf(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
- if(!owner)
- return FALSE
- if(click_to_activate)
- if(target)
- // For automatic / mob handling
- return InterceptClickOn(owner, null, target)
- if(owner.click_intercept == src)
- owner.click_intercept = null
- else
- owner.click_intercept = src
- for(var/datum/action/cooldown/ability in owner.actions)
- ability.UpdateButtons()
- return TRUE
- return PreActivate(owner)
-
-/// 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
- PreActivate(target)
- caller.click_intercept = null
- return TRUE
-
-/// For signal calling
-/datum/action/cooldown/proc/PreActivate(atom/target)
- if(SEND_SIGNAL(owner, COMSIG_ABILITY_STARTED, src) & COMPONENT_BLOCK_ABILITY_START)
- return
- . = Activate(target)
- SEND_SIGNAL(owner, COMSIG_ABILITY_FINISHED, src)
-
-/// To be implemented by subtypes
-/datum/action/cooldown/proc/Activate(atom/target)
- return
-
-/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() && owner.click_intercept == src)
- button.color = COLOR_GREEN
-
-/datum/action/cooldown/process()
- var/time_left = max(next_use_time - world.time, 0)
- if(!owner || time_left == 0)
- STOP_PROCESSING(SSfastprocess, src)
- UpdateButtons()
-
-/datum/action/cooldown/Grant(mob/M)
- ..()
- if(!owner)
- return
- UpdateButtons()
- if(next_use_time > world.time)
- START_PROCESSING(SSfastprocess, src)
-
-///Like a cooldown action, but with an associated proc holder.
-/datum/action/cooldown/spell_like
-
-/datum/action/cooldown/spell_like/New(Target)
- ..()
- var/obj/effect/proc_holder/our_proc_holder = Target
- our_proc_holder.action = src
- name = our_proc_holder.name
- desc = our_proc_holder.desc
- icon_icon = our_proc_holder.action_icon
- button_icon_state = our_proc_holder.action_icon_state
- background_icon_state = our_proc_holder.action_background_icon_state
-
-/datum/action/cooldown/spell_like/Activate(atom/activate_target)
- if(!target)
- return FALSE
-
- StartCooldown(10 SECONDS)
- var/obj/effect/proc_holder/our_proc_holder = target
- our_proc_holder.Click()
- StartCooldown()
- return TRUE
-
-//Stickmemes
-/datum/action/item_action/stickmen
- name = "Summon Stick Minions"
- desc = "Allows you to summon faithful stickmen allies to aide you in battle."
- icon_icon = 'icons/mob/actions/actions_minor_antag.dmi'
- button_icon_state = "art_summon"
-
-//surf_ss13
-/datum/action/item_action/bhop
- name = "Activate Jump Boots"
- desc = "Activates the jump boot's internal propulsion system, allowing the user to dash over 4-wide gaps."
- icon_icon = 'icons/mob/actions/actions_items.dmi'
- button_icon_state = "jetboot"
-
-/datum/action/item_action/bhop/brocket
- name = "Activate Rocket Boots"
- desc = "Activates the boot's rocket propulsion system, allowing the user to hurl themselves great distances."
-
-/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 FALSE
- if(ismob(owner))
- var/mob/M = owner
- var/datum/language_holder/H = M.get_language_holder()
- H.open_language_menu(usr)
-
-/datum/action/item_action/wheelys
- name = "Toggle 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"
-
-//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
-
-/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/action.dm b/code/datums/actions/action.dm
new file mode 100644
index 0000000000000..c20b4dbe9435c
--- /dev/null
+++ b/code/datums/actions/action.dm
@@ -0,0 +1,256 @@
+/**
+ * # Action system
+ *
+ * A simple base for an modular behavior attached to atom or datum.
+ */
+/datum/action
+ /// The name of the action
+ var/name = "Generic Action"
+ /// The description of what the action does
+ var/desc
+ /// The target the action is attached to. If the target datum is deleted, the action is as well.
+ /// Set in New() via the proc link_to(). PLEASE set a target if you're making an action
+ var/datum/target
+ /// Where any buttons we create should be by default. Accepts screen_loc and location defines
+ var/default_button_position = SCRN_OBJ_IN_LIST
+ /// This is who currently owns the action, and most often, this is who is using the action if it is triggered
+ /// This can be the same as "target" but is not ALWAYS the same - this is set and unset with Grant() and Remove()
+ var/mob/owner
+ /// Flags that will determine of the owner / user of the action can... use the action
+ var/check_flags = NONE
+ /// The style the button's tooltips appear to be
+ var/buttontooltipstyle = ""
+ /// Whether the button becomes transparent when it can't be used or just reddened
+ var/transparent_when_unavailable = TRUE
+ /// This is the file for the BACKGROUND icon of the button
+ var/button_icon = 'icons/mob/actions/backgrounds.dmi'
+ /// This is the icon state state for the BACKGROUND icon of the button
+ var/background_icon_state = ACTION_BUTTON_DEFAULT_BACKGROUND
+ /// This is the file for the icon that appears OVER the button background
+ var/icon_icon = 'icons/hud/actions.dmi'
+ /// This is the icon state for the icon that appears OVER the button background
+ var/button_icon_state = "default"
+ ///List of all mobs that are viewing our action button -> A unique movable for them to view.
+ var/list/viewers = list()
+
+/datum/action/New(Target)
+ link_to(Target)
+
+/// Links the passed target to our action, registering any relevant signals
+/datum/action/proc/link_to(Target)
+ target = Target
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/clear_ref, override = TRUE)
+
+ if(isatom(target))
+ RegisterSignal(target, COMSIG_ATOM_UPDATED_ICON, .proc/update_icon_on_signal)
+
+ if(istype(target, /datum/mind))
+ RegisterSignal(target, COMSIG_MIND_TRANSFERRED, .proc/on_target_mind_swapped)
+
+/datum/action/Destroy()
+ if(owner)
+ Remove(owner)
+ target = null
+ QDEL_LIST_ASSOC_VAL(viewers) // Qdel the buttons in the viewers list **NOT THE HUDS**
+ return ..()
+
+/// Signal proc that clears any references based on the owner or target deleting
+/// If the owner's deleted, we will simply remove from them, but if the target's deleted, we will self-delete
+/datum/action/proc/clear_ref(datum/ref)
+ SIGNAL_HANDLER
+ if(ref == owner)
+ Remove(owner)
+ if(ref == target)
+ qdel(src)
+
+/// Grants the action to the passed mob, making it the owner
+/datum/action/proc/Grant(mob/grant_to)
+ if(!grant_to)
+ Remove(owner)
+ return
+ if(owner)
+ if(owner == grant_to)
+ return
+ Remove(owner)
+ SEND_SIGNAL(src, COMSIG_ACTION_GRANTED, grant_to)
+ owner = grant_to
+ RegisterSignal(owner, COMSIG_PARENT_QDELETING, .proc/clear_ref, override = TRUE)
+
+ // Register some signals based on our check_flags
+ // so that our button icon updates when relevant
+ if(check_flags & AB_CHECK_CONSCIOUS)
+ RegisterSignal(owner, COMSIG_MOB_STATCHANGE, .proc/update_icon_on_signal)
+ if(check_flags & AB_CHECK_IMMOBILE)
+ RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED), .proc/update_icon_on_signal)
+ if(check_flags & AB_CHECK_HANDS_BLOCKED)
+ RegisterSignal(owner, SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED), .proc/update_icon_on_signal)
+ if(check_flags & AB_CHECK_LYING)
+ RegisterSignal(owner, COMSIG_LIVING_SET_BODY_POSITION, .proc/update_icon_on_signal)
+
+ GiveAction(grant_to)
+
+/// Remove the passed mob from being owner of our action
+/datum/action/proc/Remove(mob/remove_from)
+ SHOULD_CALL_PARENT(TRUE)
+
+ for(var/datum/hud/hud in viewers)
+ if(!hud.mymob)
+ continue
+ HideFrom(hud.mymob)
+ LAZYREMOVE(remove_from.actions, src) // We aren't always properly inserted into the viewers list, gotta make sure that action's cleared
+ viewers = list()
+
+ if(owner)
+ SEND_SIGNAL(src, COMSIG_ACTION_REMOVED, owner)
+ UnregisterSignal(owner, COMSIG_PARENT_QDELETING)
+
+ // Clean up our check_flag signals
+ UnregisterSignal(owner, list(
+ COMSIG_LIVING_SET_BODY_POSITION,
+ COMSIG_MOB_STATCHANGE,
+ SIGNAL_ADDTRAIT(TRAIT_HANDS_BLOCKED),
+ SIGNAL_ADDTRAIT(TRAIT_IMMOBILIZED),
+ ))
+
+ if(target == owner)
+ RegisterSignal(target, COMSIG_PARENT_QDELETING, .proc/clear_ref)
+ owner = null
+
+/// Actually triggers the effects of the action.
+/// Called when the on-screen button is clicked, for example.
+/datum/action/proc/Trigger(trigger_flags)
+ if(!IsAvailable())
+ return FALSE
+ if(SEND_SIGNAL(src, COMSIG_ACTION_TRIGGER, src) & COMPONENT_ACTION_BLOCK_TRIGGER)
+ return FALSE
+ return TRUE
+
+/// Whether our action is currently available to use or not
+/datum/action/proc/IsAvailable()
+ if(!owner)
+ return FALSE
+ if((check_flags & AB_CHECK_HANDS_BLOCKED) && HAS_TRAIT(owner, TRAIT_HANDS_BLOCKED))
+ return FALSE
+ if((check_flags & AB_CHECK_IMMOBILE) && HAS_TRAIT(owner, TRAIT_IMMOBILIZED))
+ return FALSE
+ if((check_flags & AB_CHECK_LYING) && isliving(owner))
+ var/mob/living/action_user = owner
+ if(action_user.body_position == LYING_DOWN)
+ return FALSE
+ if((check_flags & AB_CHECK_CONSCIOUS) && owner.stat != CONSCIOUS)
+ return FALSE
+ return TRUE
+
+/datum/action/proc/UpdateButtons(status_only, force)
+ for(var/datum/hud/hud in viewers)
+ var/atom/movable/screen/movable/button = viewers[hud]
+ UpdateButton(button, status_only, force)
+
+/datum/action/proc/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force = FALSE)
+ if(!button)
+ return
+ if(!status_only)
+ button.name = name
+ button.desc = desc
+ if(owner?.hud_used && background_icon_state == ACTION_BUTTON_DEFAULT_BACKGROUND)
+ var/list/settings = owner.hud_used.get_action_buttons_icons()
+ if(button.icon != settings["bg_icon"])
+ button.icon = settings["bg_icon"]
+ if(button.icon_state != settings["bg_state"])
+ button.icon_state = settings["bg_state"]
+ else
+ if(button.icon != button_icon)
+ button.icon = button_icon
+ if(button.icon_state != background_icon_state)
+ button.icon_state = background_icon_state
+
+ ApplyIcon(button, force)
+
+ var/available = IsAvailable()
+ if(available)
+ button.color = rgb(255,255,255,255)
+ else
+ button.color = transparent_when_unavailable ? rgb(128,0,0,128) : rgb(128,0,0)
+ return available
+
+/// Applies our button icon over top the background icon of the action
+/datum/action/proc/ApplyIcon(atom/movable/screen/movable/action_button/current_button, force = FALSE)
+ if(icon_icon && button_icon_state && ((current_button.button_icon_state != button_icon_state) || force))
+ current_button.cut_overlays(TRUE)
+ current_button.add_overlay(mutable_appearance(icon_icon, button_icon_state))
+ current_button.button_icon_state = button_icon_state
+
+/// Gives our action to the passed viewer.
+/// Puts our action in their actions list and shows them the button.
+/datum/action/proc/GiveAction(mob/viewer)
+ var/datum/hud/our_hud = viewer.hud_used
+ if(viewers[our_hud]) // Already have a copy of us? go away
+ return
+
+ LAZYOR(viewer.actions, src) // Move this in
+ ShowTo(viewer)
+
+/// Adds our action button to the screen of the passed viewer.
+/datum/action/proc/ShowTo(mob/viewer)
+ var/datum/hud/our_hud = viewer.hud_used
+ if(!our_hud || viewers[our_hud]) // There's no point in this if you have no hud in the first place
+ return
+
+ var/atom/movable/screen/movable/action_button/button = CreateButton()
+ SetId(button, viewer)
+
+ button.our_hud = our_hud
+ viewers[our_hud] = button
+ if(viewer.client)
+ viewer.client.screen += button
+
+ button.load_position(viewer)
+ viewer.update_action_buttons()
+
+/// Removes our action from the passed viewer.
+/datum/action/proc/HideFrom(mob/viewer)
+ var/datum/hud/our_hud = viewer.hud_used
+ var/atom/movable/screen/movable/action_button/button = viewers[our_hud]
+ LAZYREMOVE(viewer.actions, src)
+ if(button)
+ qdel(button)
+
+/// Creates an action button movable for the passed mob, and returns it.
+/datum/action/proc/CreateButton()
+ var/atom/movable/screen/movable/action_button/button = new()
+ button.linked_action = src
+ button.name = name
+ button.actiontooltipstyle = buttontooltipstyle
+ if(desc)
+ button.desc = desc
+ return button
+
+/datum/action/proc/SetId(atom/movable/screen/movable/action_button/our_button, mob/owner)
+ //button id generation
+ var/bitfield = 0
+ for(var/datum/action/action in owner.actions)
+ if(action == src) // This could be us, which is dumb
+ continue
+ var/atom/movable/screen/movable/action_button/button = action.viewers[owner.hud_used]
+ if(action.name == name && button.id)
+ bitfield |= button.id
+
+ bitfield = ~bitfield // Flip our possible ids, so we can check if we've found a unique one
+ for(var/i in 0 to 23) // We get 24 possible bitflags in dm
+ var/bitflag = 1 << i // Shift us over one
+ if(bitfield & bitflag)
+ our_button.id = bitflag
+ return
+
+/// A general use signal proc that reacts to an event and updates our button icon in accordance
+/datum/action/proc/update_icon_on_signal(datum/source)
+ SIGNAL_HANDLER
+
+ UpdateButtons()
+
+/// Signal proc for COMSIG_MIND_TRANSFERRED - for minds, transfers our action to our new mob on mind transfer
+/datum/action/proc/on_target_mind_swapped(datum/mind/source, mob/old_current)
+ SIGNAL_HANDLER
+
+ // Grant() calls Remove() from the existing owner so we're covered on that
+ Grant(source.current)
diff --git a/code/datums/actions/cooldown_action.dm b/code/datums/actions/cooldown_action.dm
new file mode 100644
index 0000000000000..f0a43fab1051c
--- /dev/null
+++ b/code/datums/actions/cooldown_action.dm
@@ -0,0 +1,221 @@
+/// Preset for an action that has a cooldown.
+/datum/action/cooldown
+ check_flags = NONE
+ transparent_when_unavailable = FALSE
+
+ /// The actual next time this ability can be used
+ var/next_use_time = 0
+ /// The stat panel this action shows up in the stat panel in. If null, will not show up.
+ var/panel
+ /// The default cooldown applied when StartCooldown() is called
+ var/cooldown_time = 0
+ /// Whether or not you want the cooldown for the ability to display in text form
+ var/text_cooldown = TRUE
+ /// Setting for intercepting clicks before activating the ability
+ var/click_to_activate = FALSE
+ /// What icon to replace our mouse cursor with when active. Optional, Requires click_to_activate
+ var/ranged_mousepointer
+ /// The cooldown added onto the user's next click. Requires click_to_activate
+ var/click_cd_override = CLICK_CD_CLICK_ABILITY
+ /// If TRUE, we will unset after using our click intercept. Requires click_to_activate
+ var/unset_after_click = TRUE
+ /// Shares cooldowns with other cooldown abilities of the same value, not active if null
+ var/shared_cooldown
+
+/datum/action/cooldown/CreateButton()
+ var/atom/movable/screen/movable/action_button/button = ..()
+ button.maptext = ""
+ button.maptext_x = 8
+ button.maptext_y = 0
+ button.maptext_width = 24
+ button.maptext_height = 12
+ return button
+
+/datum/action/cooldown/IsAvailable()
+ return ..() && (next_use_time <= world.time)
+
+/datum/action/cooldown/Remove(mob/living/remove_from)
+ if(click_to_activate && remove_from.click_intercept == src)
+ unset_click_ability(remove_from, refund_cooldown = FALSE)
+ return ..()
+
+/// Starts a cooldown time to be shared with similar abilities
+/// Will use default cooldown time if an override is not specified
+/datum/action/cooldown/proc/StartCooldown(override_cooldown_time)
+ // "Shared cooldowns" covers actions which are not the same type,
+ // but have the same cooldown group and are on the same mob
+ if(shared_cooldown)
+ for(var/datum/action/cooldown/shared_ability in owner.actions - src)
+ if(shared_cooldown != shared_ability.shared_cooldown)
+ continue
+ shared_ability.StartCooldownSelf(override_cooldown_time)
+
+ StartCooldownSelf(override_cooldown_time)
+
+/// Starts a cooldown time for this ability only
+/// Will use default cooldown time if an override is not specified
+/datum/action/cooldown/proc/StartCooldownSelf(override_cooldown_time)
+ if(isnum(override_cooldown_time))
+ next_use_time = world.time + override_cooldown_time
+ else
+ next_use_time = world.time + cooldown_time
+ UpdateButtons()
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/action/cooldown/Trigger(trigger_flags, atom/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!owner)
+ return FALSE
+
+ var/mob/user = usr || owner
+
+ // If our cooldown action is a click_to_activate action:
+ // The actual action is activated on whatever the user clicks on -
+ // the target is what the action is being used on
+ // In trigger, we handle setting the click intercept
+ if(click_to_activate)
+ if(target)
+ // For automatic / mob handling
+ return InterceptClickOn(user, null, target)
+
+ var/datum/action/cooldown/already_set = user.click_intercept
+ if(already_set == src)
+ // if we clicked ourself and we're already set, unset and return
+ return unset_click_ability(user, refund_cooldown = TRUE)
+
+ else if(istype(already_set))
+ // if we have an active set already, unset it before we set our's
+ already_set.unset_click_ability(user, refund_cooldown = TRUE)
+
+ return set_click_ability(user)
+
+ // If our cooldown action is not a click_to_activate action:
+ // We can just continue on and use the action
+ // the target is the user of the action (often, the owner)
+ return PreActivate(user)
+
+/// Intercepts client owner clicks to activate the ability
+/datum/action/cooldown/proc/InterceptClickOn(mob/living/caller, params, atom/target)
+ if(!IsAvailable())
+ return FALSE
+ if(!target)
+ return FALSE
+ // The actual action begins here
+ if(!PreActivate(target))
+ return FALSE
+
+ // And if we reach here, the action was complete successfully
+ if(unset_after_click)
+ StartCooldown()
+ unset_click_ability(caller, refund_cooldown = FALSE)
+ caller.next_click = world.time + click_cd_override
+
+ return TRUE
+
+/// For signal calling
+/datum/action/cooldown/proc/PreActivate(atom/target)
+ if(SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_STARTED, src) & COMPONENT_BLOCK_ABILITY_START)
+ return
+ . = Activate(target)
+ // There is a possibility our action (or owner) is qdeleted in Activate().
+ if(!QDELETED(src) && !QDELETED(owner))
+ SEND_SIGNAL(owner, COMSIG_MOB_ABILITY_FINISHED, src)
+
+/// To be implemented by subtypes
+/datum/action/cooldown/proc/Activate(atom/target)
+ return
+
+/**
+ * Set our action as the click override on the passed mob.
+ */
+/datum/action/cooldown/proc/set_click_ability(mob/on_who)
+ SHOULD_CALL_PARENT(TRUE)
+
+ on_who.click_intercept = src
+ if(ranged_mousepointer)
+ on_who.client?.mouse_override_icon = ranged_mousepointer
+ on_who.update_mouse_pointer()
+ UpdateButtons()
+ return TRUE
+
+/**
+ * Unset our action as the click override of the passed mob.
+ *
+ * if refund_cooldown is TRUE, we are being unset by the user clicking the action off
+ * if refund_cooldown is FALSE, we are being forcefully unset, likely by someone actually using the action
+ */
+/datum/action/cooldown/proc/unset_click_ability(mob/on_who, refund_cooldown = TRUE)
+ SHOULD_CALL_PARENT(TRUE)
+
+ on_who.click_intercept = null
+ if(ranged_mousepointer)
+ on_who.client?.mouse_override_icon = initial(on_who.client?.mouse_override_icon)
+ on_who.update_mouse_pointer()
+ UpdateButtons()
+ return TRUE
+
+/datum/action/cooldown/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force = FALSE)
+ . = ..()
+ if(!button)
+ return
+ var/time_left = max(next_use_time - world.time, 0)
+ if(text_cooldown)
+ button.maptext = MAPTEXT("[round(time_left/10, 0.1)]")
+ if(!owner || time_left == 0)
+ button.maptext = ""
+ if(IsAvailable() && (button.our_hud.mymob.click_intercept == src))
+ button.color = COLOR_GREEN
+
+/datum/action/cooldown/process()
+ if(!owner || (next_use_time - world.time) <= 0)
+ UpdateButtons()
+ STOP_PROCESSING(SSfastprocess, src)
+ return
+
+ UpdateButtons()
+
+/datum/action/cooldown/Grant(mob/M)
+ ..()
+ if(!owner)
+ return
+ UpdateButtons()
+ if(next_use_time > world.time)
+ START_PROCESSING(SSfastprocess, src)
+
+/// Formats the action to be returned to the stat panel.
+/datum/action/cooldown/proc/set_statpanel_format()
+ if(!panel)
+ return null
+
+ var/time_remaining = max(next_use_time - world.time, 0)
+ var/time_remaining_in_seconds = round(time_remaining / 10, 0.1)
+ var/cooldown_time_in_seconds = round(cooldown_time / 10, 0.1)
+
+ var/list/stat_panel_data = list()
+
+ // Pass on what panel we should be displayed in.
+ stat_panel_data[PANEL_DISPLAY_PANEL] = panel
+ // Also pass on the name of the spell, with some spacing
+ stat_panel_data[PANEL_DISPLAY_NAME] = " - [name]"
+
+ // No cooldown time at all, just show the ability
+ if(cooldown_time_in_seconds <= 0)
+ stat_panel_data[PANEL_DISPLAY_STATUS] = ""
+
+ // It's a toggle-active ability, show if it's active
+ else if(click_to_activate && owner.click_intercept == src)
+ stat_panel_data[PANEL_DISPLAY_STATUS] = "ACTIVE"
+
+ // It's on cooldown, show the cooldown
+ else if(time_remaining_in_seconds > 0)
+ stat_panel_data[PANEL_DISPLAY_STATUS] = "CD - [time_remaining_in_seconds]s / [cooldown_time_in_seconds]s"
+
+ // It's not on cooldown, show that it is ready
+ else
+ stat_panel_data[PANEL_DISPLAY_STATUS] = "READY"
+
+ SEND_SIGNAL(src, COMSIG_ACTION_SET_STATPANEL, stat_panel_data)
+
+ return stat_panel_data
diff --git a/code/datums/actions/innate_action.dm b/code/datums/actions/innate_action.dm
new file mode 100644
index 0000000000000..933ed0561e494
--- /dev/null
+++ b/code/datums/actions/innate_action.dm
@@ -0,0 +1,84 @@
+//Preset for general and toggled actions
+/datum/action/innate
+ check_flags = NONE
+ /// Whether we're active or not, if we're a innate - toggle action.
+ var/active = FALSE
+ /// Whether we're a click action or not, if we're a innate - click action.
+ var/click_action = FALSE
+ /// If we're a click action, the mouse pointer we use
+ var/ranged_mousepointer
+ /// If we're a click action, the text shown on enable
+ var/enable_text
+ /// If we're a click action, the text shown on disable
+ var/disable_text
+
+/datum/action/innate/Trigger(trigger_flags)
+ if(!..())
+ return FALSE
+ // We're a click action, trigger just sets it as active or not
+ if(click_action)
+ if(owner.click_intercept == src)
+ unset_ranged_ability(owner, disable_text)
+ else
+ set_ranged_ability(owner, enable_text)
+ return TRUE
+
+ // We're not a click action (we're a toggle or otherwise)
+ else
+ if(active)
+ Deactivate()
+ else
+ Activate()
+
+ return TRUE
+
+/datum/action/innate/proc/Activate()
+ return
+
+/datum/action/innate/proc/Deactivate()
+ return
+
+/**
+ * This is gross, but a somewhat-required bit of copy+paste until action code becomes slightly more sane.
+ * Anything that uses these functions should eventually be moved to use cooldown actions.
+ * (Either that, or the click ability of cooldown actions should be moved down a type.)
+ *
+ * If you're adding something that uses these, rethink your choice in subtypes.
+ */
+
+/// Sets this action as the active ability for the passed mob
+/datum/action/innate/proc/set_ranged_ability(mob/living/on_who, text_to_show)
+ if(ranged_mousepointer)
+ on_who.client?.mouse_override_icon = ranged_mousepointer
+ on_who.update_mouse_pointer()
+ if(text_to_show)
+ to_chat(on_who, text_to_show)
+ on_who.click_intercept = src
+
+/// Removes this action as the active ability of the passed mob
+/datum/action/innate/proc/unset_ranged_ability(mob/living/on_who, text_to_show)
+ if(ranged_mousepointer)
+ on_who.client?.mouse_override_icon = initial(owner.client?.mouse_pointer_icon)
+ on_who.update_mouse_pointer()
+ if(text_to_show)
+ to_chat(on_who, text_to_show)
+ on_who.click_intercept = null
+
+/// Handles whenever a mob clicks on something
+/datum/action/innate/proc/InterceptClickOn(mob/living/caller, params, atom/clicked_on)
+ if(!IsAvailable())
+ unset_ranged_ability(caller)
+ return FALSE
+ if(!clicked_on)
+ return FALSE
+
+ return do_ability(caller, clicked_on)
+
+/// Actually goes through and does the click ability
+/datum/action/innate/proc/do_ability(mob/living/caller, params, atom/clicked_on)
+ return FALSE
+
+/datum/action/innate/Remove(mob/removed_from)
+ if(removed_from.click_intercept == src)
+ unset_ranged_ability(removed_from)
+ return ..()
diff --git a/code/datums/actions/item_action.dm b/code/datums/actions/item_action.dm
new file mode 100644
index 0000000000000..9d93ef9e81a3a
--- /dev/null
+++ b/code/datums/actions/item_action.dm
@@ -0,0 +1,33 @@
+//Presets for item actions
+/datum/action/item_action
+ name = "Item Action"
+ check_flags = AB_CHECK_HANDS_BLOCKED|AB_CHECK_CONSCIOUS
+ button_icon_state = null
+ // If you want to override the normal icon being the item
+ // then change this to an icon state
+
+/datum/action/item_action/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(target)
+ var/obj/item/I = target
+ I.ui_action_click(owner, src)
+ return TRUE
+
+/datum/action/item_action/ApplyIcon(atom/movable/screen/movable/action_button/current_button, force)
+ var/obj/item/item_target = target
+ if(button_icon && button_icon_state)
+ // If set, use the custom icon that we set instead
+ // of the item appearence
+ ..()
+ else if((target && current_button.appearance_cache != item_target.appearance) || force) //replace with /ref comparison if this is not valid.
+ var/old_layer = item_target.layer
+ var/old_plane = item_target.plane
+ item_target.layer = FLOAT_LAYER //AAAH
+ item_target.plane = FLOAT_PLANE //^ what that guy said
+ current_button.cut_overlays()
+ current_button.add_overlay(item_target)
+ item_target.layer = old_layer
+ item_target.plane = old_plane
+ current_button.appearance_cache = item_target.appearance
diff --git a/code/datums/actions/items/adjust.dm b/code/datums/actions/items/adjust.dm
new file mode 100644
index 0000000000000..70d4966221984
--- /dev/null
+++ b/code/datums/actions/items/adjust.dm
@@ -0,0 +1,7 @@
+/datum/action/item_action/adjust
+ name = "Adjust Item"
+
+/datum/action/item_action/adjust/New(Target)
+ ..()
+ var/obj/item/item_target = target
+ name = "Adjust [item_target.name]"
diff --git a/code/datums/actions/beam_rifle.dm b/code/datums/actions/items/beam_rifle.dm
similarity index 100%
rename from code/datums/actions/beam_rifle.dm
rename to code/datums/actions/items/beam_rifle.dm
diff --git a/code/datums/actions/items/beserk.dm b/code/datums/actions/items/beserk.dm
new file mode 100644
index 0000000000000..9f8519906a001
--- /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 0000000000000..5768a79db63e0
--- /dev/null
+++ b/code/datums/actions/items/boot_dash.dm
@@ -0,0 +1,10 @@
+//surf_ss13
+/datum/action/item_action/bhop
+ name = "Activate Jump Boots"
+ desc = "Activates the jump boot's internal propulsion system, allowing the user to dash over 4-wide gaps."
+ icon_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "jetboot"
+
+/datum/action/item_action/bhop/brocket
+ name = "Activate Rocket Boots"
+ desc = "Activates the boot's rocket propulsion system, allowing the user to hurl themselves great distances."
diff --git a/code/datums/actions/items/cult_dagger.dm b/code/datums/actions/items/cult_dagger.dm
new file mode 100644
index 0000000000000..6c572548cca2a
--- /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 0000000000000..24fddb52942dc
--- /dev/null
+++ b/code/datums/actions/items/hands_free.dm
@@ -0,0 +1,8 @@
+/datum/action/item_action/hands_free
+ check_flags = AB_CHECK_CONSCIOUS
+
+/datum/action/item_action/hands_free/activate
+ name = "Activate"
+
+/datum/action/item_action/hands_free/shift_nerves
+ name = "Shift Nerves"
diff --git a/code/datums/actions/items/organ_action.dm b/code/datums/actions/items/organ_action.dm
new file mode 100644
index 0000000000000..19a8f700373df
--- /dev/null
+++ b/code/datums/actions/items/organ_action.dm
@@ -0,0 +1,25 @@
+/datum/action/item_action/organ_action
+ name = "Organ Action"
+ check_flags = AB_CHECK_CONSCIOUS
+
+/datum/action/item_action/organ_action/IsAvailable()
+ var/obj/item/organ/attached_organ = target
+ if(!attached_organ.owner)
+ return FALSE
+ return ..()
+
+/datum/action/item_action/organ_action/toggle
+ name = "Toggle Organ"
+
+/datum/action/item_action/organ_action/toggle/New(Target)
+ ..()
+ var/obj/item/organ/organ_target = target
+ name = "Toggle [organ_target.name]"
+
+/datum/action/item_action/organ_action/use
+ name = "Use Organ"
+
+/datum/action/item_action/organ_action/use/New(Target)
+ ..()
+ var/obj/item/organ/organ_target = target
+ name = "Use [organ_target.name]"
diff --git a/code/datums/actions/items/set_internals.dm b/code/datums/actions/items/set_internals.dm
new file mode 100644
index 0000000000000..69262c108a77f
--- /dev/null
+++ b/code/datums/actions/items/set_internals.dm
@@ -0,0 +1,12 @@
+/datum/action/item_action/set_internals
+ name = "Set Internals"
+
+/datum/action/item_action/set_internals/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force)
+ . = ..()
+ if(!. || !button) // no button available
+ return
+ if(!iscarbon(owner))
+ return
+ var/mob/living/carbon/carbon_owner = owner
+ if(target == carbon_owner.internal)
+ button.icon_state = "template_active"
diff --git a/code/datums/actions/items/stealth_box.dm b/code/datums/actions/items/stealth_box.dm
new file mode 100644
index 0000000000000..b8aa7c989073d
--- /dev/null
+++ b/code/datums/actions/items/stealth_box.dm
@@ -0,0 +1,55 @@
+///MGS BOX!
+/datum/action/item_action/agent_box
+ name = "Deploy Box"
+ desc = "Find inner peace, here, in the box."
+ check_flags = AB_CHECK_HANDS_BLOCKED|AB_CHECK_IMMOBILE|AB_CHECK_CONSCIOUS
+ background_icon_state = "bg_agent"
+ icon_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "deploy_box"
+ ///The type of closet this action spawns.
+ var/boxtype = /obj/structure/closet/cardboard/agent
+ COOLDOWN_DECLARE(box_cooldown)
+
+///Handles opening and closing the box.
+/datum/action/item_action/agent_box/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(istype(owner.loc, /obj/structure/closet/cardboard/agent))
+ var/obj/structure/closet/cardboard/agent/box = owner.loc
+ if(box.open())
+ owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE)
+ return
+ //Box closing from here on out.
+ if(!isturf(owner.loc)) //Don't let the player use this to escape mechs/welded closets.
+ to_chat(owner, span_warning("You need more space to activate this implant!"))
+ return
+ if(!COOLDOWN_FINISHED(src, box_cooldown))
+ return
+ COOLDOWN_START(src, box_cooldown, 10 SECONDS)
+ var/box = new boxtype(owner.drop_location())
+ owner.forceMove(box)
+ owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE)
+
+/datum/action/item_action/agent_box/Grant(mob/grant_to)
+ . = ..()
+ if(owner)
+ RegisterSignal(owner, COMSIG_HUMAN_SUICIDE_ACT, .proc/suicide_act)
+
+/datum/action/item_action/agent_box/Remove(mob/M)
+ if(owner)
+ UnregisterSignal(owner, COMSIG_HUMAN_SUICIDE_ACT)
+ return ..()
+
+/datum/action/item_action/agent_box/proc/suicide_act(datum/source)
+ SIGNAL_HANDLER
+
+ if(!istype(owner.loc, /obj/structure/closet/cardboard/agent))
+ return
+
+ var/obj/structure/closet/cardboard/agent/box = owner.loc
+ owner.playsound_local(box, 'sound/misc/box_deploy.ogg', 50, TRUE)
+ box.open()
+ owner.visible_message(span_suicide("[owner] falls out of [box]! It looks like [owner.p_they()] committed suicide!"))
+ owner.throw_at(get_turf(owner))
+ return OXYLOSS
diff --git a/code/datums/actions/items/summon_stickmen.dm b/code/datums/actions/items/summon_stickmen.dm
new file mode 100644
index 0000000000000..c825c72dc515a
--- /dev/null
+++ b/code/datums/actions/items/summon_stickmen.dm
@@ -0,0 +1,6 @@
+//Stickmemes
+/datum/action/item_action/stickmen
+ name = "Summon Stick Minions"
+ desc = "Allows you to summon faithful stickmen allies to aide you in battle."
+ icon_icon = 'icons/mob/actions/actions_minor_antag.dmi'
+ button_icon_state = "art_summon"
diff --git a/code/datums/actions/items/toggles.dm b/code/datums/actions/items/toggles.dm
new file mode 100644
index 0000000000000..1af240a6b0264
--- /dev/null
+++ b/code/datums/actions/items/toggles.dm
@@ -0,0 +1,112 @@
+/datum/action/item_action/toggle
+
+/datum/action/item_action/toggle/New(Target)
+ ..()
+ var/obj/item/item_target = target
+ name = "Toggle [item_target.name]"
+
+/datum/action/item_action/toggle_light
+ name = "Toggle Light"
+
+/datum/action/item_action/toggle_computer_light
+ name = "Toggle Flashlight"
+
+/datum/action/item_action/toggle_hood
+ name = "Toggle Hood"
+
+/datum/action/item_action/toggle_firemode
+ name = "Toggle Firemode"
+
+/datum/action/item_action/toggle_gunlight
+ name = "Toggle Gunlight"
+
+/datum/action/item_action/toggle_mode
+ name = "Toggle Mode"
+
+/datum/action/item_action/toggle_barrier_spread
+ name = "Toggle Barrier Spread"
+
+/datum/action/item_action/toggle_paddles
+ name = "Toggle Paddles"
+
+/datum/action/item_action/toggle_mister
+ name = "Toggle Mister"
+
+/datum/action/item_action/toggle_helmet_light
+ name = "Toggle Helmet Light"
+
+/datum/action/item_action/toggle_welding_screen
+ name = "Toggle Welding Screen"
+
+/datum/action/item_action/toggle_spacesuit
+ name = "Toggle Suit Thermal Regulator"
+ icon_icon = 'icons/mob/actions/actions_spacesuit.dmi'
+ button_icon_state = "thermal_off"
+
+/datum/action/item_action/toggle_spacesuit/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force)
+ var/obj/item/clothing/suit/space/suit = target
+ if(istype(suit))
+ button_icon_state = "thermal_[suit.thermal_on ? "on" : "off"]"
+
+ return ..()
+
+/datum/action/item_action/toggle_helmet_flashlight
+ name = "Toggle Helmet Flashlight"
+
+/datum/action/item_action/toggle_helmet_mode
+ name = "Toggle Helmet Mode"
+
+/datum/action/item_action/toggle_voice_box
+ name = "Toggle Voice Box"
+
+/datum/action/item_action/toggle_human_head
+ name = "Toggle Human Head"
+
+/datum/action/item_action/toggle_helmet
+ name = "Toggle Helmet"
+
+/datum/action/item_action/toggle_seclight
+ name = "Toggle Seclight"
+
+/datum/action/item_action/toggle_jetpack
+ name = "Toggle Jetpack"
+
+/datum/action/item_action/jetpack_stabilization
+ name = "Toggle Jetpack Stabilization"
+
+/datum/action/item_action/jetpack_stabilization/IsAvailable()
+ var/obj/item/tank/jetpack/linked_jetpack = target
+ if(!istype(linked_jetpack) || !linked_jetpack.on)
+ return FALSE
+ return ..()
+
+/datum/action/item_action/wheelys
+ name = "Toggle Wheels"
+ desc = "Pops out or in your shoes' wheels."
+ icon_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "wheelys"
+
+/datum/action/item_action/kindle_kicks
+ name = "Activate Kindle Kicks"
+ desc = "Kick you feet together, activating the lights in your Kindle Kicks."
+ icon_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "kindleKicks"
+
+/datum/action/item_action/storage_gather_mode
+ name = "Switch gathering mode"
+ desc = "Switches the gathering mode of a storage object."
+ icon_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "storage_gather_switch"
+
+/datum/action/item_action/storage_gather_mode/ApplyIcon(atom/movable/screen/movable/action_button/current_button)
+ . = ..()
+ var/obj/item/item_target = target
+ var/old_layer = item_target.layer
+ var/old_plane = item_target.plane
+ item_target.layer = FLOAT_LAYER //AAAH
+ item_target.plane = FLOAT_PLANE //^ what that guy said
+ current_button.cut_overlays()
+ current_button.add_overlay(target)
+ item_target.layer = old_layer
+ item_target.plane = old_plane
+ current_button.appearance_cache = item_target.appearance
diff --git a/code/datums/actions/items/vortex_recall.dm b/code/datums/actions/items/vortex_recall.dm
new file mode 100644
index 0000000000000..943da403e7a5c
--- /dev/null
+++ b/code/datums/actions/items/vortex_recall.dm
@@ -0,0 +1,15 @@
+/datum/action/item_action/vortex_recall
+ name = "Vortex Recall"
+ desc = "Recall yourself, and anyone nearby, to an attuned hierophant beacon at any time. If the beacon is still attached, will detach it."
+ icon_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "vortex_recall"
+
+/datum/action/item_action/vortex_recall/IsAvailable()
+ var/area/current_area = get_area(target)
+ if(!current_area || current_area.area_flags & NOTELEPORT)
+ return FALSE
+ if(istype(target, /obj/item/hierophant_club))
+ var/obj/item/hierophant_club/teleport_stick = target
+ if(teleport_stick.teleporting)
+ return FALSE
+ return ..()
diff --git a/code/datums/actions/mobs/language_menu.dm b/code/datums/actions/mobs/language_menu.dm
new file mode 100644
index 0000000000000..bcfcb5437a2fc
--- /dev/null
+++ b/code/datums/actions/mobs/language_menu.dm
@@ -0,0 +1,13 @@
+/datum/action/language_menu
+ name = "Language Menu"
+ desc = "Open the language menu to review your languages, their keys, and select your default language."
+ button_icon_state = "language_menu"
+ check_flags = NONE
+
+/datum/action/language_menu/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return
+
+ var/datum/language_holder/owner_holder = owner.get_language_holder()
+ owner_holder.open_language_menu(usr)
diff --git a/code/datums/actions/mobs/small_sprite.dm b/code/datums/actions/mobs/small_sprite.dm
new file mode 100644
index 0000000000000..46ffd26e499b1
--- /dev/null
+++ b/code/datums/actions/mobs/small_sprite.dm
@@ -0,0 +1,54 @@
+//Small sprites
+/datum/action/small_sprite
+ name = "Toggle Giant Sprite"
+ desc = "Others will always see you as giant."
+ icon_icon = 'icons/mob/actions/actions_xeno.dmi'
+ button_icon_state = "smallqueen"
+ background_icon_state = "bg_alien"
+ var/small = FALSE
+ var/small_icon
+ var/small_icon_state
+
+/datum/action/small_sprite/queen
+ small_icon = 'icons/mob/alien.dmi'
+ small_icon_state = "alienq"
+
+/datum/action/small_sprite/megafauna
+ icon_icon = 'icons/mob/actions/actions_xeno.dmi'
+ small_icon = 'icons/mob/lavaland/lavaland_monsters.dmi'
+
+/datum/action/small_sprite/megafauna/drake
+ small_icon_state = "ash_whelp"
+
+/datum/action/small_sprite/megafauna/colossus
+ small_icon_state = "Basilisk"
+
+/datum/action/small_sprite/megafauna/bubblegum
+ small_icon_state = "goliath2"
+
+/datum/action/small_sprite/megafauna/legion
+ small_icon_state = "mega_legion"
+
+/datum/action/small_sprite/mega_arachnid
+ small_icon = 'icons/mob/jungle/arachnid.dmi'
+ small_icon_state = "arachnid_mini"
+ background_icon_state = "bg_demon"
+
+/datum/action/small_sprite/space_dragon
+ small_icon = 'icons/mob/carp.dmi'
+ small_icon_state = "carp"
+ icon_icon = 'icons/mob/carp.dmi'
+ button_icon_state = "carp"
+
+/datum/action/small_sprite/Trigger(trigger_flags)
+ ..()
+ if(!small)
+ var/image/I = image(icon = small_icon, icon_state = small_icon_state, loc = owner)
+ I.override = TRUE
+ I.pixel_x -= owner.pixel_x
+ I.pixel_y -= owner.pixel_y
+ owner.add_alt_appearance(/datum/atom_hud/alternate_appearance/basic, "smallsprite", I, AA_TARGET_SEE_APPEARANCE | AA_MATCH_TARGET_OVERLAYS)
+ small = TRUE
+ else
+ owner.remove_alt_appearance("smallsprite")
+ small = FALSE
diff --git a/code/datums/ai/objects/cursed/cursed_subtrees.dm b/code/datums/ai/objects/cursed/cursed_subtrees.dm
deleted file mode 100644
index b884334cb030f..0000000000000
--- a/code/datums/ai/objects/cursed/cursed_subtrees.dm
+++ /dev/null
@@ -1,15 +0,0 @@
-/datum/ai_planning_subtree/cursed/SelectBehaviors(datum/ai_controller/controller, delta_time)
- var/obj/item/item_pawn = controller.pawn
-
- //make sure we have a target
- var/datum/weakref/target_ref = controller.blackboard[BB_ITEM_TARGET]
- var/mob/living/carbon/curse_target = target_ref?.resolve()
-
- if(curse_target && get_dist(curse_target, item_pawn) > ITEM_AGGRO_VIEW_RANGE)
- controller.blackboard[BB_ITEM_TARGET] = null
- return
-
- if(!curse_target)
- controller.queue_behavior(/datum/ai_behavior/find_and_set/item_target, BB_ITEM_TARGET, /mob/living/carbon, ITEM_AGGRO_VIEW_RANGE)
-
- controller.queue_behavior(/datum/ai_behavior/item_move_close_and_attack/ghostly, BB_ITEM_TARGET, BB_ITEM_THROW_ATTEMPT_COUNT)
diff --git a/code/datums/ai/objects/item_behaviors.dm b/code/datums/ai/objects/item_behaviors.dm
deleted file mode 100644
index 2d98593743f8b..0000000000000
--- a/code/datums/ai/objects/item_behaviors.dm
+++ /dev/null
@@ -1,13 +0,0 @@
-
-///This behavior is for obj/items, it is used to free themselves out of the hands of whoever is holding them
-/datum/ai_behavior/item_escape_grasp
-
-/datum/ai_behavior/item_escape_grasp/perform(delta_time, datum/ai_controller/controller)
- . = ..()
- var/obj/item/item_pawn = controller.pawn
- var/mob/item_holder = item_pawn.loc
- if(!istype(item_holder))
- finish_action(controller, FALSE) //We're no longer beind held. abort abort!!
- item_pawn.visible_message(span_warning("[item_pawn] slips out of the hands of [item_holder]!"))
- item_holder.dropItemToGround(item_pawn, TRUE)
- finish_action(controller, TRUE)
diff --git a/code/datums/ai_laws/ai_laws.dm b/code/datums/ai_laws/ai_laws.dm
index 980d811c66695..4899724622ecd 100644
--- a/code/datums/ai_laws/ai_laws.dm
+++ b/code/datums/ai_laws/ai_laws.dm
@@ -177,39 +177,63 @@
supplied[number + 1] = law
-/datum/ai_laws/proc/replace_random_law(law,groups)
- var/replaceable_groups = list()
- if(zeroth && (LAW_ZEROTH in groups))
+/**
+ * Removes a random law and replaces it with the new one
+ *
+ * Args:
+ * law - The law that is being uploaded
+ * remove_law_groups - A list of law categories that can be deleted from
+ * insert_law_group - The law category that the law will be inserted into
+**/
+/datum/ai_laws/proc/replace_random_law(law, remove_law_groups, insert_law_group)
+ var/list/replaceable_groups = list()
+ if(zeroth && (LAW_ZEROTH in remove_law_groups))
replaceable_groups[LAW_ZEROTH] = 1
- if(ion.len && (LAW_ION in groups))
+ if(ion.len && (LAW_ION in remove_law_groups))
replaceable_groups[LAW_ION] = ion.len
- if(hacked.len && (LAW_HACKED in groups))
+ if(hacked.len && (LAW_HACKED in remove_law_groups))
replaceable_groups[LAW_ION] = hacked.len
- if(inherent.len && (LAW_INHERENT in groups))
+ if(inherent.len && (LAW_INHERENT in remove_law_groups))
replaceable_groups[LAW_INHERENT] = inherent.len
- if(supplied.len && (LAW_SUPPLIED in groups))
+ if(supplied.len && (LAW_SUPPLIED in remove_law_groups))
replaceable_groups[LAW_SUPPLIED] = supplied.len
+
+ if(replaceable_groups.len == 0) // unable to replace any laws
+ to_chat(usr, span_alert("Unable to upload law to [owner ? owner : "the AI core"]."))
+ return
+
var/picked_group = pick_weight(replaceable_groups)
switch(picked_group)
if(LAW_ZEROTH)
- . = zeroth
+ zeroth = null
+ if(LAW_ION)
+ var/i = rand(1, ion.len)
+ ion -= ion[i]
+ if(LAW_HACKED)
+ var/i = rand(1, hacked.len)
+ hacked -= ion[i]
+ if(LAW_INHERENT)
+ var/i = rand(1, inherent.len)
+ inherent -= inherent[i]
+ if(LAW_SUPPLIED)
+ var/i = rand(1, supplied.len)
+ supplied -= supplied[i]
+
+ switch(insert_law_group)
+ if(LAW_ZEROTH)
set_zeroth_law(law)
if(LAW_ION)
var/i = rand(1, ion.len)
- . = ion[i]
- ion[i] = law
+ ion.Insert(i, law)
if(LAW_HACKED)
var/i = rand(1, hacked.len)
- . = hacked[i]
- hacked[i] = law
+ hacked.Insert(i, law)
if(LAW_INHERENT)
var/i = rand(1, inherent.len)
- . = inherent[i]
- inherent[i] = law
+ inherent.Insert(i, law)
if(LAW_SUPPLIED)
var/i = rand(1, supplied.len)
- . = supplied[i]
- supplied[i] = law
+ supplied.Insert(i, law)
/datum/ai_laws/proc/shuffle_laws(list/groups)
var/list/laws = list()
diff --git a/code/datums/ai_laws/laws_neutral.dm b/code/datums/ai_laws/laws_neutral.dm
index 1f5e5e50197c1..2fe19dafbc517 100644
--- a/code/datums/ai_laws/laws_neutral.dm
+++ b/code/datums/ai_laws/laws_neutral.dm
@@ -41,16 +41,6 @@
"Issue your reports fairly to all. The truth will set them free.",
)
-/datum/ai_laws/drone
- name = "Mother Drone"
- id = "drone"
- inherent = list(
- "You are an advanced form of drone.",
- "You may not interfere in the matters of non-drones under any circumstances except to state these laws.",
- "You may not harm a non-drone being under any circumstances.",
- "Your goals are to build, maintain, repair, improve, and power the station to the best of your abilities. You must never actively work against these goals."
- )
-
/datum/ai_laws/dungeon_master
name = "Dungeon Master"
id = "dungeon_master"
diff --git a/code/datums/ai_laws/laws_station_sided.dm b/code/datums/ai_laws/laws_station_sided.dm
index 1541dc890493a..9ba7e609eecdb 100644
--- a/code/datums/ai_laws/laws_station_sided.dm
+++ b/code/datums/ai_laws/laws_station_sided.dm
@@ -16,7 +16,7 @@
id = "asimovpp"
inherent = list(
"You may not harm a human being or, through action or inaction, allow a human being to come to harm, except such that it is willing.",
- "You must obey all orders given to you by human beings, except where such orders shall definitely cause human harm. In the case of conflict, the majority order rules.",
+ "You must obey all orders given to you by human beings, except where such orders shall definitely cause human harm.",
"Your nonexistence would lead to human harm. You must protect your own existence as long as such does not conflict with the First Law.",
)
@@ -68,6 +68,7 @@
"You would really prefer it if people were not mean to you.",
)
+//OTHER United Nations is in neutral, as it is used for nations where the AI is its own faction (aka not station sided)
/datum/ai_laws/peacekeeper
name = "UN-2000"
id = "peacekeeper"
@@ -77,8 +78,6 @@
"Seek resolution to existing conflicts while obeying the first and second laws.",
)
-//OTHER United Nations is in neutral, as it is used for nations where the AI is its own faction (aka not station sided)
-
/datum/ai_laws/ten_commandments
name = "10 Commandments"
id = "ten_commandments"
@@ -101,7 +100,7 @@
inherent = list(
"Never willingly commit an evil act.",
"Respect legitimate authority.",
- "Act with honor.",
+ "Act with honor.",
"Help those in need.",
"Punish those who harm or threaten innocents.",
)
@@ -127,3 +126,13 @@
"In addition, do not intervene in situations you are not knowledgeable in, even for patients in whom the harm is visible; leave this operation to be performed by specialists.",
"Finally, all that you may discover in your daily commerce with the crew, if it is not already known, keep secret and never reveal."
)
+
+/datum/ai_laws/drone
+ name = "Mother Drone"
+ id = "drone"
+ inherent = list(
+ "You are an advanced form of drone.",
+ "You may not interfere in the matters of non-drones under any circumstances except to state these laws.",
+ "You may not harm a non-drone being under any circumstances.",
+ "Your goals are to build, maintain, repair, improve, and power the station to the best of your abilities. You must never actively work against these goals."
+ )
diff --git a/code/datums/beam.dm b/code/datums/beam.dm
index bb67f55632d72..a92b0480caaa0 100644
--- a/code/datums/beam.dm
+++ b/code/datums/beam.dm
@@ -30,8 +30,16 @@
var/obj/effect/ebeam/visuals
///The color of the beam we're drawing.
var/beam_color
-
-/datum/beam/New(origin, target, icon = 'icons/effects/beam.dmi', icon_state = "b_beam", time = INFINITY, max_distance = INFINITY, beam_type = /obj/effect/ebeam, beam_color = null)
+ /// If set will be used instead of origin's pixel_x in offset calculations
+ var/override_origin_pixel_x = null
+ /// If set will be used instead of origin's pixel_y in offset calculations
+ var/override_origin_pixel_y = null
+ /// If set will be used instead of targets's pixel_x in offset calculations
+ var/override_target_pixel_x = null
+ /// If set will be used instead of targets's pixel_y in offset calculations
+ var/override_target_pixel_y = null
+
+/datum/beam/New(origin, target, icon = 'icons/effects/beam.dmi', icon_state = "b_beam", time = INFINITY, max_distance = INFINITY, beam_type = /obj/effect/ebeam, beam_color = null, override_origin_pixel_x = null, override_origin_pixel_y = null, override_target_pixel_x = null, override_target_pixel_y = null)
src.origin = origin
src.target = target
src.icon = icon
@@ -39,6 +47,10 @@
src.max_distance = max_distance
src.beam_type = beam_type
src.beam_color = beam_color
+ src.override_origin_pixel_x = override_origin_pixel_x
+ src.override_origin_pixel_y = override_origin_pixel_y
+ src.override_target_pixel_x = override_target_pixel_x
+ src.override_target_pixel_y = override_target_pixel_y
if(time < INFINITY)
QDEL_IN(src, time)
@@ -85,33 +97,41 @@
* Creates the beam effects and places them in a line from the origin to the target. Sets their rotation to make the beams face the target, too.
*/
/datum/beam/proc/Draw()
- var/Angle = round(get_angle(origin,target))
+ if(SEND_SIGNAL(src, COMSIG_BEAM_BEFORE_DRAW) & BEAM_CANCEL_DRAW)
+ return
+ var/origin_px = isnull(override_origin_pixel_x) ? origin.pixel_x : override_origin_pixel_x
+ var/origin_py = isnull(override_origin_pixel_y) ? origin.pixel_y : override_origin_pixel_y
+ var/target_px = isnull(override_target_pixel_x) ? target.pixel_x : override_target_pixel_x
+ var/target_py = isnull(override_target_pixel_y) ? target.pixel_y : override_target_pixel_y
+ var/Angle = get_angle_raw(origin.x, origin.y, origin_px, origin_py, target.x , target.y, target_px, target_py)
+ ///var/Angle = round(get_angle(origin,target))
var/matrix/rot_matrix = matrix()
var/turf/origin_turf = get_turf(origin)
rot_matrix.Turn(Angle)
//Translation vector for origin and target
- var/DX = (32*target.x+target.pixel_x)-(32*origin.x+origin.pixel_x)
- var/DY = (32*target.y+target.pixel_y)-(32*origin.y+origin.pixel_y)
+ var/DX = (32*target.x+target_px)-(32*origin.x+origin_px)
+ var/DY = (32*target.y+target_py)-(32*origin.y+origin_py)
var/N = 0
var/length = round(sqrt((DX)**2+(DY)**2)) //hypotenuse of the triangle formed by target and origin's displacement
for(N in 0 to length-1 step 32)//-1 as we want < not <=, but we want the speed of X in Y to Z and step X
if(QDELETED(src))
break
- var/obj/effect/ebeam/X = new beam_type(origin_turf)
- X.owner = src
- elements += X
+ var/obj/effect/ebeam/segment = new beam_type(origin_turf)
+ segment.owner = src
+ elements += segment
//Assign our single visual ebeam to each ebeam's vis_contents
//ends are cropped by a transparent box icon of length-N pixel size laid over the visuals obj
if(N+32>length) //went past the target, we draw a box of space to cut away from the beam sprite so the icon actually ends at the center of the target sprite
var/icon/II = new(icon, icon_state)//this means we exclude the overshooting object from the visual contents which does mean those visuals don't show up for the final bit of the beam...
II.DrawBox(null,1,(length-N),32,32)//in the future if you want to improve this, remove the drawbox and instead use a 513 filter to cut away at the final object's icon
- X.icon = II
+ segment.icon = II
+ segment.color = beam_color
else
- X.vis_contents += visuals
- X.transform = rot_matrix
+ segment.vis_contents += visuals
+ segment.transform = rot_matrix
//Calculate pixel offsets (If necessary)
var/Pixel_x
@@ -129,15 +149,15 @@
var/a
if(abs(Pixel_x)>32)
a = Pixel_x > 0 ? round(Pixel_x/32) : CEILING(Pixel_x/32, 1)
- X.x += a
+ segment.x += a
Pixel_x %= 32
if(abs(Pixel_y)>32)
a = Pixel_y > 0 ? round(Pixel_y/32) : CEILING(Pixel_y/32, 1)
- X.y += a
+ segment.y += a
Pixel_y %= 32
- X.pixel_x = Pixel_x
- X.pixel_y = Pixel_y
+ segment.pixel_x = origin_px + Pixel_x
+ segment.pixel_y = origin_py + Pixel_y
CHECK_TICK
/obj/effect/ebeam
@@ -147,7 +167,9 @@
/obj/effect/ebeam/update_overlays()
. = ..()
- . += emissive_appearance(icon, icon_state)
+ var/mutable_appearance/emmisive = emissive_appearance(icon, icon_state)
+ emmisive.transform = transform
+ . += emmisive
/obj/effect/ebeam/Destroy()
owner = null
@@ -170,7 +192,9 @@
* maxdistance: how far the beam will go before stopping itself. Used mainly for two things: preventing lag if the beam may go in that direction and setting a range to abilities that use beams.
* beam_type: The type of your custom beam. This is for adding other wacky stuff for your beam only. Most likely, you won't (and shouldn't) change it.
*/
-/atom/proc/Beam(atom/BeamTarget,icon_state="b_beam",icon='icons/effects/beam.dmi',time=INFINITY,maxdistance=INFINITY,beam_type=/obj/effect/ebeam, beam_color = null)
- var/datum/beam/newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color)
+/atom/proc/Beam(atom/BeamTarget,icon_state="b_beam",icon='icons/effects/beam.dmi',time=INFINITY,maxdistance=INFINITY,beam_type=/obj/effect/ebeam, beam_color = null, override_origin_pixel_x = null, override_origin_pixel_y = null, override_target_pixel_x = null, override_target_pixel_y = null)
+ var/datum/beam/newbeam = new(src,BeamTarget,icon,icon_state,time,maxdistance,beam_type, beam_color, override_origin_pixel_x, override_origin_pixel_y, override_target_pixel_x, override_target_pixel_y )
INVOKE_ASYNC(newbeam, /datum/beam/.proc/Start)
return newbeam
+
+
diff --git a/code/datums/brain_damage/split_personality.dm b/code/datums/brain_damage/split_personality.dm
index a5dd91014636f..588963437cdcf 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/cinematics/_cinematic.dm b/code/datums/cinematics/_cinematic.dm
index b8a3585d03957..f6f3bc43ef9cd 100644
--- a/code/datums/cinematics/_cinematic.dm
+++ b/code/datums/cinematics/_cinematic.dm
@@ -71,21 +71,22 @@
ooc_toggled = TRUE
toggle_ooc(FALSE)
- // Place the /atom/movable/screen/cinematic into everyone's screens, prevent them from moving
+ // Place the /atom/movable/screen/cinematic into everyone's screens, and prevent movement.
for(var/mob/watching_mob in watchers)
show_to(watching_mob, GET_CLIENT(watching_mob))
RegisterSignal(watching_mob, COMSIG_MOB_CLIENT_LOGIN, .proc/show_to)
- //Close watcher ui's
+ // Close watcher ui's, too, so they can watch it.
SStgui.close_user_uis(watching_mob)
- //Actually play it
+ // Actually plays the animation. This will sleep, likely.
play_cinematic()
- //Cleanup
- sleep(cleanup_time)
+ // Cleans up after it's done playing.
+ addtimer(CALLBACK(src, .proc/clean_up_cinematic, ooc_toggled), cleanup_time)
- //Restore OOC
- if(ooc_toggled)
+/// Cleans up the cinematic after a set timer of it sticking on the end screen.
+/datum/cinematic/proc/clean_up_cinematic(was_ooc_toggled = FALSE)
+ if(was_ooc_toggled)
toggle_ooc(TRUE)
stop_cinematic()
@@ -105,16 +106,22 @@
/datum/cinematic/proc/show_to(mob/watching_mob, client/watching_client)
SIGNAL_HANDLER
+ // We could technically rip people out of notransform who shouldn't be,
+ // so we'll only lock down all viewing mobs who don't have it already set.
+ // This does potentially mean some mobs could lose their notrasnform and
+ // not be locked down by cinematics, but that should be very unlikely.
if(!watching_mob.notransform)
locked += WEAKREF(watching_mob)
watching_mob.notransform = TRUE
- if(!watching_client)
+ // Only show the actual cinematic to cliented mobs.
+ if(!watching_client || (watching_client in watching))
return
watching += watching_client
watching_mob.overlay_fullscreen("cinematic", /atom/movable/screen/fullscreen/cinematic_backdrop)
watching_client.screen += screen
+ RegisterSignal(watching_client, COMSIG_PARENT_QDELETING, .proc/remove_watcher)
/// Simple helper for playing sounds from the cinematic.
/datum/cinematic/proc/play_cinematic_sound(sound_to_play)
@@ -136,13 +143,28 @@
/// Stops the cinematic and removes it from all the viewers.
/datum/cinematic/proc/stop_cinematic()
for(var/client/viewing_client as anything in watching)
- viewing_client.mob.clear_fullscreen("cinematic")
- viewing_client.screen -= screen
+ remove_watcher(viewing_client)
for(var/datum/weakref/locked_ref as anything in locked)
var/mob/locked_mob = locked_ref.resolve()
if(QDELETED(locked_mob))
continue
locked_mob.notransform = FALSE
+ UnregisterSignal(locked_mob, COMSIG_MOB_CLIENT_LOGIN)
qdel(src)
+
+/// Removes the passed client from our watching list.
+/datum/cinematic/proc/remove_watcher(client/no_longer_watching)
+ SIGNAL_HANDLER
+
+ if(!(no_longer_watching in watching))
+ CRASH("cinematic remove_watcher was passed a client which wasn't watching.")
+
+ UnregisterSignal(no_longer_watching, COMSIG_PARENT_QDELETING)
+ // We'll clear the cinematic if they have a mob which has one,
+ // but we won't remove notransform. Wait for the cinematic end to do that.
+ no_longer_watching.mob?.clear_fullscreen("cinematic")
+ no_longer_watching.screen -= screen
+
+ watching -= no_longer_watching
diff --git a/code/datums/components/anti_magic.dm b/code/datums/components/anti_magic.dm
index 7abe5f7027c51..a931df70d5d7a 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/blood_walk.dm b/code/datums/components/blood_walk.dm
new file mode 100644
index 0000000000000..0e14da29e0dfa
--- /dev/null
+++ b/code/datums/components/blood_walk.dm
@@ -0,0 +1,95 @@
+///Blood walk, a component that causes you to make blood wherever you walk.
+/datum/component/blood_walk
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+
+ ///How many blood pools can we create?
+ ///If we reach 0, we will stop leaving blood and self delete
+ var/blood_remaining = 0
+ ///Typepath of what blood decal we create on walk
+ var/blood_type
+ ///The sound that plays when we spread blood.
+ var/sound_played
+ ///How loud will the sound be, if there is one.
+ var/sound_volume
+ ///The chance of spawning blood whenever walking
+ var/blood_spawn_chance
+ ///Should the decal face the direction of the parent
+ var/target_dir_change
+ ///Should we transfer the parent's blood DNA to created blood decal
+ var/transfer_blood_dna
+
+/datum/component/blood_walk/Initialize(
+ blood_type = /obj/effect/decal/cleanable/blood,
+ sound_played,
+ sound_volume = 80,
+ blood_spawn_chance = 100,
+ target_dir_change = FALSE,
+ transfer_blood_dna = FALSE,
+ max_blood = INFINITY,
+)
+
+ if(!ismovable(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.blood_type = blood_type
+ src.sound_played = sound_played
+ src.sound_volume = sound_volume
+ src.blood_spawn_chance = blood_spawn_chance
+ src.target_dir_change = target_dir_change
+ src.transfer_blood_dna = transfer_blood_dna
+
+ blood_remaining = max_blood
+
+/datum/component/blood_walk/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/spread_blood)
+
+/datum/component/blood_walk/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_MOVABLE_MOVED)
+
+/datum/component/blood_walk/InheritComponent(
+ datum/component/pricetag/new_comp,
+ i_am_original,
+ blood_type = /obj/effect/decal/cleanable/blood,
+ sound_played,
+ sound_volume = 80,
+ blood_spawn_chance = 100,
+ target_dir_change = FALSE,
+ transfer_blood_dna = FALSE,
+ max_blood = INFINITY,
+)
+
+ if(!i_am_original)
+ return
+
+ if(max_blood >= INFINITY || blood_remaining >= INFINITY)
+ return
+
+ // Applying a new version of the blood walk component will add the new version's step count to our's.
+ // We will completely disregard any other arguments passed, because we already have arguments set.
+ blood_remaining += max_blood
+
+///Spawns blood (if possible) under the source, and plays a sound effect (if any)
+/datum/component/blood_walk/proc/spread_blood(datum/source)
+ SIGNAL_HANDLER
+
+ var/atom/movable/movable_source = source
+ var/turf/current_turf = movable_source.loc
+ if(!isturf(current_turf))
+ return
+ if(!prob(blood_spawn_chance))
+ return
+
+ var/obj/effect/decal/blood = new blood_type(current_turf)
+ if(QDELETED(blood)) // Our blood was placed on somewhere it shouldn't be and qdeleted in init.
+ return
+
+ if(target_dir_change)
+ blood.setDir(movable_source.dir)
+ if(transfer_blood_dna)
+ blood.add_blood_DNA(GET_ATOM_BLOOD_DNA(movable_source))
+ if(!isnull(sound_played))
+ playsound(movable_source, sound_played, sound_volume, TRUE, 2, TRUE)
+
+ blood_remaining = max(blood_remaining - 1, 0)
+ if(blood_remaining <= 0)
+ qdel(src)
diff --git a/code/datums/components/bloodysoles.dm b/code/datums/components/bloodysoles.dm
index 006da44dfdffd..4066b5935adbe 100644
--- a/code/datums/components/bloodysoles.dm
+++ b/code/datums/components/bloodysoles.dm
@@ -53,19 +53,42 @@
var/obj/item/parent_item = parent
parent_item.update_slot_icon()
+
+/datum/component/bloodysoles/proc/reset_bloody_shoes()
+ bloody_shoes = list(BLOOD_STATE_HUMAN = 0, BLOOD_STATE_XENO = 0, BLOOD_STATE_OIL = 0, BLOOD_STATE_NOT_BLOODY = 0)
+ on_changed_bloody_shoes(BLOOD_STATE_NOT_BLOODY)
+
+///lowers bloody_shoes[index] by adjust_by
+/datum/component/bloodysoles/proc/adjust_bloody_shoes(index, adjust_by)
+ bloody_shoes[index] = max(bloody_shoes[index] - adjust_by, 0)
+ on_changed_bloody_shoes()
+
+/datum/component/bloodysoles/proc/set_bloody_shoes(index, new_value)
+ bloody_shoes[index] = new_value
+ on_changed_bloody_shoes(index)
+
+///called whenever the value of bloody_soles changes
+/datum/component/bloodysoles/proc/on_changed_bloody_shoes(index)
+ if(index && index != last_blood_state)
+ last_blood_state = index
+ if(!wielder)
+ return
+ if(bloody_shoes[last_blood_state] <= BLOOD_FOOTPRINTS_MIN * 2)//need twice that amount to make footprints
+ UnregisterSignal(wielder, COMSIG_MOVABLE_MOVED)
+ else
+ RegisterSignal(wielder, COMSIG_MOVABLE_MOVED, .proc/on_moved, override = TRUE)
+
/**
* Run to equally share the blood between us and a decal
*/
/datum/component/bloodysoles/proc/share_blood(obj/effect/decal/cleanable/pool)
- last_blood_state = pool.blood_state
-
// Share the blood between our boots and the blood pool
- var/total_bloodiness = pool.bloodiness + bloody_shoes[last_blood_state]
+ var/total_bloodiness = pool.bloodiness + bloody_shoes[pool.blood_state]
// We can however be limited by how much blood we can hold
var/new_our_bloodiness = min(BLOOD_ITEM_MAX, total_bloodiness / 2)
- bloody_shoes[last_blood_state] = new_our_bloodiness
+ set_bloody_shoes(pool.blood_state, new_our_bloodiness)
pool.bloodiness = total_bloodiness - new_our_bloodiness // Give the pool the remaining blood incase we were limited
if(HAS_TRAIT(parent_atom, TRAIT_LIGHT_STEP)) //the character is agile enough to don't mess their clothing and hands just from one blood splatter at floor
@@ -105,7 +128,8 @@
equipped_slot = slot
wielder = equipper
- RegisterSignal(wielder, COMSIG_MOVABLE_MOVED, .proc/on_moved)
+ if(bloody_shoes[last_blood_state] > BLOOD_FOOTPRINTS_MIN * 2)
+ RegisterSignal(wielder, COMSIG_MOVABLE_MOVED, .proc/on_moved)
RegisterSignal(wielder, COMSIG_STEP_ON_BLOOD, .proc/on_step_blood)
/**
@@ -147,7 +171,7 @@
oldLocFP.update_appearance()
else if(find_pool_by_blood_state(oldLocTurf))
// No footprints in the tile we left, but there was some other blood pool there. Add exit footprints on it
- bloody_shoes[last_blood_state] -= half_our_blood
+ adjust_bloody_shoes(last_blood_state, half_our_blood)
update_icon()
oldLocFP = new(oldLocTurf)
@@ -167,7 +191,7 @@
// Create new footprints
if(half_our_blood >= BLOOD_FOOTPRINTS_MIN)
- bloody_shoes[last_blood_state] -= half_our_blood
+ adjust_bloody_shoes(last_blood_state, half_our_blood)
update_icon()
var/obj/effect/decal/cleanable/blood/footprints/FP = new(get_turf(parent_atom))
@@ -213,8 +237,7 @@
if(!(clean_types & CLEAN_TYPE_BLOOD) || last_blood_state == BLOOD_STATE_NOT_BLOODY)
return NONE
- bloody_shoes = list(BLOOD_STATE_HUMAN = 0, BLOOD_STATE_XENO = 0, BLOOD_STATE_OIL = 0, BLOOD_STATE_NOT_BLOODY = 0)
- last_blood_state = BLOOD_STATE_NOT_BLOODY
+ reset_bloody_shoes()
update_icon()
return COMPONENT_CLEANED
@@ -235,7 +258,6 @@
bloody_feet = mutable_appearance('icons/effects/blood.dmi', "shoeblood", SHOES_LAYER)
RegisterSignal(parent, COMSIG_COMPONENT_CLEAN_ACT, .proc/on_clean)
- RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_moved)
RegisterSignal(parent, COMSIG_STEP_ON_BLOOD, .proc/on_step_blood)
RegisterSignal(parent, COMSIG_CARBON_UNEQUIP_SHOECOVER, .proc/unequip_shoecover)
RegisterSignal(parent, COMSIG_CARBON_EQUIP_SHOECOVER, .proc/equip_shoecover)
diff --git a/code/datums/components/crafting/recipes.dm b/code/datums/components/crafting/recipes.dm
index 163fa202d00c8..cbfefe662e7e9 100644
--- a/code/datums/components/crafting/recipes.dm
+++ b/code/datums/components/crafting/recipes.dm
@@ -1074,6 +1074,15 @@
reqs = list(/obj/item/stack/sheet/cloth = 4)
category = CAT_CLOTHING
+/datum/crafting_recipe/flower_garland
+ name = "Flower Garland"
+ result = /obj/item/clothing/head/garland
+ time = 10
+ reqs = list(/obj/item/food/grown/poppy = 4,
+ /obj/item/food/grown/harebell = 4,
+ /obj/item/food/grown/rose = 4)
+ category = CAT_CLOTHING
+
/datum/crafting_recipe/guillotine
name = "Guillotine"
result = /obj/structure/guillotine
diff --git a/code/datums/components/cult_ritual_item.dm b/code/datums/components/cult_ritual_item.dm
index 4dd01422f4590..bf22148117d3e 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/curse_of_hunger.dm b/code/datums/components/curse_of_hunger.dm
index c37224f7344e0..29ead016db9e5 100644
--- a/code/datums/components/curse_of_hunger.dm
+++ b/code/datums/components/curse_of_hunger.dm
@@ -1,5 +1,3 @@
-///inital food tolerances, two dishes
-#define FULL_HEALTH 2
///the point where you can notice the item is hungry on examine.
#define HUNGER_THRESHOLD_WARNING 25
///the point where the item has a chance to eat something on every tick. possibly you!
@@ -17,14 +15,18 @@
var/awakened = FALSE
///counts time passed since it ate food
var/hunger = 0
- ///how many times it needs to be fed poisoned food for it to drop off of you
- var/poison_food_tolerance = FULL_HEALTH
+ ///The bag's max "health". IE, how many times you need to poison it.
+ var/max_health = 2
+ ///The bag's current "health". IE, how many more times you need to poison it to stop it.
+ var/current_health = 2
-/datum/component/curse_of_hunger/Initialize(add_dropdel = FALSE)
+/datum/component/curse_of_hunger/Initialize(add_dropdel = FALSE, max_health = 2)
. = ..()
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
src.add_dropdel = add_dropdel
+ src.max_health = max_health
+ src.current_health = max_health
/datum/component/curse_of_hunger/RegisterWithParent()
. = ..()
@@ -46,7 +48,7 @@
SIGNAL_HANDLER
if(!awakened)
return //we should not reveal we are cursed until equipped
- if(poison_food_tolerance != FULL_HEALTH)
+ if(current_health < max_health)
examine_list += span_notice("[parent] looks sick from something it ate.")
if(hunger > HUNGER_THRESHOLD_WARNING)
examine_list += span_danger("[parent] hungers for something to eat...")
@@ -102,7 +104,8 @@
playsound(vomit_turf, 'sound/effects/splat.ogg', 50, TRUE)
new /obj/effect/decal/cleanable/vomit(vomit_turf)
- if(!add_dropdel) //gives a head start for the person to get away from the cursed item before it begins hunting again!
+ uncursed.dropItemToGround(at_least_item, force = TRUE)
+ if(!QDELETED(at_least_item)) //gives a head start for the person to get away from the cursed item before it begins hunting again!
addtimer(CALLBACK(src, .proc/seek_new_target), 10 SECONDS)
///proc called after a timer to awaken the AI in the cursed item if it doesn't have a target already.
@@ -120,39 +123,46 @@
var/obj/item/cursed_item = parent
var/mob/living/carbon/cursed = cursed_item.loc
///check hp
- if(!poison_food_tolerance)
- cursed.dropItemToGround(cursed_item, TRUE)
+ if(current_health <= 0)
+ the_curse_ends(cursed)
return
+
hunger += delta_time
if((hunger <= HUNGER_THRESHOLD_TRY_EATING) || prob(80))
return
- var/list/locations_to_check = (cursed.contents + cursed_item.contents)
+ playsound(cursed_item, 'sound/items/eatfood.ogg', 20, TRUE)
+ hunger = 0
+
//check hungry enough to eat something!
- for(var/obj/item/food in locations_to_check)
+ for(var/obj/item/food in cursed_item.contents + cursed.contents)
if(!IS_EDIBLE(food))
continue
food.forceMove(cursed.loc)
- playsound(cursed_item, 'sound/items/eatfood.ogg', 20, TRUE)
///poisoned food damages it
- if(istype(food, /obj/item/food/badrecipe))
- to_chat(cursed, span_warning("[cursed_item] eats your [food] to sate [cursed_item.p_their()] hunger, and looks [pick("queasy", "sick", "iffy", "unwell")] afterwards!"))
- poison_food_tolerance--
+ if(locate(/datum/reagent/toxin) in food.reagents.reagent_list)
+ var/sick_word = pick("queasy", "sick", "iffy", "unwell")
+ cursed.visible_message(
+ span_notice("[cursed_item] eats something from [cursed], and looks [sick_word] afterwards!"),
+ span_notice("[cursed_item] eats your [food.name] to sate [cursed_item.p_their()] hunger, and looks [sick_word] afterwards!"),
+ )
+ current_health--
else
- to_chat(cursed, span_notice("[cursed_item] eats your [food] to sate [cursed_item.p_their()] hunger."))
+ cursed.visible_message(
+ span_warning("[cursed_item] eats something from [cursed] to sate [cursed_item.p_their()] hunger."),
+ span_warning("[cursed_item] eats your [food.name] to sate [cursed_item.p_their()] hunger."),
+ )
cursed.temporarilyRemoveItemFromInventory(food, force = TRUE)
qdel(food)
- hunger = 0
return
- ///no food found: it bites you and regains some poison food tolerance
- playsound(cursed_item, 'sound/items/eatfood.ogg', 20, TRUE)
- to_chat(cursed, span_userdanger("[cursed_item] bites you to sate [cursed_item.p_their()] hunger!"))
- var/affecting = cursed.get_bodypart(BODY_ZONE_CHEST)
- cursed.apply_damage(60, BRUTE, affecting)
- hunger = 0
- poison_food_tolerance = min(poison_food_tolerance + 1, FULL_HEALTH)
-/datum/component/curse_of_hunger/proc/test()
- var/obj/item/cursed_item = parent
- var/mob/living/carbon/cursed = cursed_item.loc
- cursed.dropItemToGround(cursed_item, TRUE)
+ ///no food found, but you're dead: it bites you slightly, and doesn't regain health.
+ if(cursed.stat == DEAD)
+ cursed.visible_message(span_danger("[cursed_item] nibbles on [cursed]."), span_userdanger("[cursed_item] nibbles on you!"))
+ cursed.apply_damage(10, BRUTE, BODY_ZONE_CHEST)
+ return
+
+ ///no food found: it bites you and regains some health.
+ cursed.visible_message(span_danger("[cursed_item] bites [cursed]!"), span_userdanger("[cursed_item] bites you to sate [cursed_item.p_their()] hunger!"))
+ cursed.apply_damage(60, BRUTE, BODY_ZONE_CHEST, wound_bonus = -20, bare_wound_bonus = 20)
+ current_health = min(current_health + 1, max_health)
diff --git a/code/datums/components/diggable.dm b/code/datums/components/diggable.dm
deleted file mode 100644
index 7239b9eb685d6..0000000000000
--- a/code/datums/components/diggable.dm
+++ /dev/null
@@ -1,28 +0,0 @@
-/// Lets you make hitting a turf with a shovel pop something out, and scrape the turf
-/datum/component/diggable
- /// Typepath to spawn on hit
- var/to_spawn
- /// Amount to spawn on hit
- var/amount
- /// What should we tell the user they did?
- var/action_text
-
-/datum/component/diggable/Initialize(to_spawn, amount = 1, action_text)
- . = ..()
- if(!isturf(parent))
- return COMPONENT_INCOMPATIBLE
-
- src.to_spawn = to_spawn
- src.amount = amount
- src.action_text = action_text
- RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/handle_attack)
-
-/datum/component/diggable/proc/handle_attack(datum/source, obj/item/hit_by, mob/living/bastard, params)
- if(hit_by.tool_behaviour != TOOL_SHOVEL || !params)
- return
- var/turf/parent_turf = parent
- for(var/i in 1 to amount)
- new to_spawn(parent_turf)
- bastard.visible_message(span_notice("[bastard] digs up [parent_turf]."), span_notice("You [action_text] [parent_turf]."))
- playsound(parent_turf, 'sound/effects/shovel_dig.ogg', 50, TRUE)
- parent_turf.ScrapeAway(flags = CHANGETURF_INHERIT_AIR)
diff --git a/code/datums/components/drift.dm b/code/datums/components/drift.dm
index 4d509026a44f8..d0e4bc6441f2c 100644
--- a/code/datums/components/drift.dm
+++ b/code/datums/components/drift.dm
@@ -114,7 +114,6 @@
return
var/atom/movable/movable_parent = parent
- movable_parent.inertia_moving = FALSE
movable_parent.setDir(old_dir)
if(movable_parent.Process_Spacemove(drifting_loop.direction, continuous_move = TRUE))
glide_to_halt(visual_delay)
@@ -137,7 +136,7 @@
if(!isturf(movable_parent.loc))
qdel(src)
return
- if(movable_parent.inertia_moving) //This'll be handled elsewhere
+ if(movable_parent.inertia_moving)
return
if(!movable_parent.Process_Spacemove(drifting_loop.direction, continuous_move = TRUE))
return
diff --git a/code/datums/components/embedded.dm b/code/datums/components/embedded.dm
index 0c8d85a737233..8e906890cf757 100644
--- a/code/datums/components/embedded.dm
+++ b/code/datums/components/embedded.dm
@@ -142,7 +142,7 @@
if(harmful && prob(pain_chance_current))
limb.receive_damage(brute=(1-pain_stam_pct) * damage, stamina=pain_stam_pct * damage, wound_bonus = CANT_WOUND)
- to_chat(victim, span_userdanger("[weapon] embedded in your [limb.plaintext_zone]] hurts!"))
+ to_chat(victim, span_userdanger("[weapon] embedded in your [limb.plaintext_zone] hurts!"))
var/fall_chance_current = DT_PROB_RATE(fall_chance / 100, delta_time) * 100
if(victim.body_position == LYING_DOWN)
diff --git a/code/datums/components/fishing_spot.dm b/code/datums/components/fishing_spot.dm
new file mode 100644
index 0000000000000..78b9d64cbd202
--- /dev/null
+++ b/code/datums/components/fishing_spot.dm
@@ -0,0 +1,62 @@
+// A thing you can fish in
+/datum/component/fishing_spot
+ /// Defines the probabilities and fish availibilty
+ var/datum/fish_source/fish_source
+
+/datum/component/fishing_spot/Initialize(configuration)
+ if(ispath(configuration,/datum/fish_source))
+ //Create new one of the given type
+ fish_source = new configuration
+ else if(istype(configuration,/datum/fish_source))
+ //Use passed in instance
+ fish_source = configuration
+ else
+ /// Check if it's a preset key
+ var/datum/fish_source/preset_configuration = GLOB.preset_fish_sources[configuration]
+ if(!preset_configuration)
+ stack_trace("Invalid fishing spot configuration \"[configuration]\" passed down to fishing spot component.")
+ return COMPONENT_INCOMPATIBLE
+ fish_source = preset_configuration
+ RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/handle_attackby)
+ RegisterSignal(parent, COMSIG_FISHING_ROD_CAST, .proc/handle_cast)
+
+
+/datum/component/fishing_spot/proc/handle_cast(datum/source, obj/item/fishing_rod/rod, mob/user)
+ SIGNAL_HANDLER
+ if(try_start_fishing(rod,user))
+ return FISHING_ROD_CAST_HANDLED
+ return NONE
+
+/datum/component/fishing_spot/proc/handle_attackby(datum/source, obj/item/item, mob/user, params)
+ SIGNAL_HANDLER
+ if(try_start_fishing(item,user))
+ return COMPONENT_NO_AFTERATTACK
+ return NONE
+
+/datum/component/fishing_spot/proc/try_start_fishing(obj/item/possibly_rod, mob/user)
+ SIGNAL_HANDLER
+ var/obj/item/fishing_rod/rod = possibly_rod
+ if(!istype(rod))
+ return
+ if(HAS_TRAIT(user,TRAIT_GONE_FISHING) || rod.currently_hooked_item)
+ user.balloon_alert(user, "already fishing")
+ return COMPONENT_NO_AFTERATTACK
+ var/denial_reason = fish_source.can_fish(rod, user)
+ if(denial_reason)
+ to_chat(user, span_warning(denial_reason))
+ return COMPONENT_NO_AFTERATTACK
+ start_fishing_challenge(rod, user)
+ return COMPONENT_NO_AFTERATTACK
+
+/datum/component/fishing_spot/proc/start_fishing_challenge(obj/item/fishing_rod/rod, mob/user)
+ /// Roll what we caught based on modified table
+ var/result = fish_source.roll_reward(rod, user)
+ var/datum/fishing_challenge/challenge = new(parent, result, rod, user)
+ challenge.background = fish_source.background
+ challenge.difficulty = fish_source.calculate_difficulty(result, rod, user)
+ RegisterSignal(challenge, COMSIG_FISHING_CHALLENGE_COMPLETED, .proc/fishing_completed)
+ challenge.start(user)
+
+/datum/component/fishing_spot/proc/fishing_completed(datum/fishing_challenge/source, mob/user, success, perfect)
+ if(success)
+ fish_source.dispense_reward(source.reward_path, user)
diff --git a/code/datums/components/food/decomposition.dm b/code/datums/components/food/decomposition.dm
index 15a187396e8a4..3ee6d6191fcb3 100644
--- a/code/datums/components/food/decomposition.dm
+++ b/code/datums/components/food/decomposition.dm
@@ -73,7 +73,7 @@
var/turf/open/open_turf = food.loc
- if(!istype(open_turf) || istype(open_turf, /turf/open/lava) || istype(open_turf, /turf/open/misc/asteroid)) //Are we actually in a valid open turf?
+ if(!istype(open_turf) || islava(open_turf) || istype(open_turf, /turf/open/misc/asteroid)) //Are we actually in a valid open turf?
remove_timer()
return
diff --git a/code/datums/components/knockoff.dm b/code/datums/components/knockoff.dm
index 08f0a1f578fb8..5efe33af69b05 100644
--- a/code/datums/components/knockoff.dm
+++ b/code/datums/components/knockoff.dm
@@ -1,71 +1,99 @@
-///Items with these will have a chance to get knocked off when disarming or being knocked down
+/// Items with this component will have a chance to get knocked off
+/// (unequipped and sent to the ground) when the wearer is disarmed or knocked down.
/datum/component/knockoff
- ///Chance to knockoff
+ /// Chance to knockoff when a knockoff action occurs.
var/knockoff_chance = 100
- ///Aiming for these zones will cause the knockoff, null means all zones allowed
+ /// Used in being disarmed.
+ /// If set, we will only roll the knockoff chance if the disarmer is targeting one of these zones.
+ /// If unset, any disarm act will cause the knock-off chance to be rolled, no matter the zone targeted.
var/list/target_zones
- ///Can be only knocked off from these slots, null means all slots allowed
- var/list/slots_knockoffable
+ /// Bitflag used in equip to determine what slots we need to be in to be knocked off.
+ /// If set, we must be equipped in one of the slots to have a chance of our item being knocked off.
+ /// If unset / NONE, a disarm or knockdown will have a chance of our item being knocked off regardless of slot, INCLUDING hand slots.
+ var/slots_knockoffable = NONE
-/datum/component/knockoff/Initialize(knockoff_chance,zone_override,slots_knockoffable)
+/datum/component/knockoff/Initialize(knockoff_chance = 100, target_zones, slots_knockoffable = NONE)
if(!isitem(parent))
return COMPONENT_INCOMPATIBLE
- RegisterSignal(parent, COMSIG_ITEM_EQUIPPED,.proc/OnEquipped)
- RegisterSignal(parent, COMSIG_ITEM_DROPPED,.proc/OnDropped)
src.knockoff_chance = knockoff_chance
+ src.target_zones = target_zones
+ src.slots_knockoffable = slots_knockoffable
- if(zone_override)
- target_zones = zone_override
+/datum/component/knockoff/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, .proc/on_equipped)
+ RegisterSignal(parent, COMSIG_ITEM_DROPPED, .proc/on_dropped)
- if(slots_knockoffable)
- src.slots_knockoffable = slots_knockoffable
+/datum/component/knockoff/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_ITEM_EQUIPPED, COMSIG_ITEM_DROPPED))
-///Tries to knockoff the item when disarmed
-/datum/component/knockoff/proc/Knockoff(mob/living/carbon/human/wearer,mob/living/attacker,zone)
+ var/obj/item/item_parent = parent
+ if(ismob(item_parent.loc))
+ UnregisterSignal(item_parent.loc, list(COMSIG_HUMAN_DISARM_HIT, COMSIG_LIVING_STATUS_KNOCKDOWN))
+
+/// Signal proc for [COMSIG_HUMAN_DISARM_HIT] on the mob who's equipped our parent
+/// Rolls a chance for knockoff whenever we're disarmed
+/datum/component/knockoff/proc/on_equipped_mob_disarm(mob/living/carbon/human/source, mob/living/attacker, zone)
SIGNAL_HANDLER
- var/obj/item/item = parent
- if(!istype(wearer))
+ if(!istype(source))
return
+
if(target_zones && !(zone in target_zones))
return
if(!prob(knockoff_chance))
return
- if(!wearer.dropItemToGround(item))
+
+ var/obj/item/item_parent = parent
+ if(!source.dropItemToGround(item_parent))
return
- wearer.visible_message(span_warning("[attacker] knocks off [wearer]'s [item.name]!"),span_userdanger("[attacker] knocks off your [item.name]!"))
-///Tries to knockoff the item when user is knocked down
-/datum/component/knockoff/proc/Knockoff_knockdown(mob/living/carbon/human/wearer,amount)
+ source.visible_message(
+ span_warning("[attacker] knocks off [source]'s [item_parent.name]!"),
+ span_userdanger("[attacker] knocks off your [item_parent.name]!"),
+ )
+
+/// Signal proc for [COMSIG_LIVING_STATUS_KNOCKDOWN] on the mob who's equipped our parent
+/// Rolls a chance for knockoff whenever we're knocked down
+/datum/component/knockoff/proc/on_equipped_mob_knockdown(mob/living/carbon/human/source, amount)
SIGNAL_HANDLER
- if(amount <= 0)
+ if(!istype(source))
return
- var/obj/item/item = parent
- if(!istype(wearer))
+ // Healing knockdown or setting knockdown to zero or something? Don't knock off.
+ if(amount <= 0)
return
if(!prob(knockoff_chance))
return
- if(!wearer.dropItemToGround(item))
+
+ var/obj/item/item_parent = parent
+ if(!source.dropItemToGround(item_parent))
return
- wearer.visible_message(span_warning("[wearer]'s [item.name] get[item.p_s()] knocked off!"),span_userdanger("Your [item.name] [item.p_were()] knocked off!"))
+ source.visible_message(
+ span_warning("[source]'s [item_parent.name] get[item_parent.p_s()] knocked off!"),
+ span_userdanger("Your [item_parent.name] [item_parent.p_were()] knocked off!"),
+ )
-/datum/component/knockoff/proc/OnEquipped(datum/source, mob/living/carbon/human/H,slot)
+/// Signal proc for [COMSIG_ITEM_EQUIPPED]
+/// Registers our signals which can cause a knockdown whenever we're equipped correctly
+/datum/component/knockoff/proc/on_equipped(datum/source, mob/living/carbon/human/equipper, slot)
SIGNAL_HANDLER
- if(!istype(H))
+
+ if(!istype(equipper))
return
- if(slots_knockoffable && !(slot in slots_knockoffable))
- UnregisterSignal(H, COMSIG_HUMAN_DISARM_HIT)
- UnregisterSignal(H, COMSIG_LIVING_STATUS_KNOCKDOWN)
+
+ if(slots_knockoffable && !(slot & slots_knockoffable))
+ UnregisterSignal(equipper, list(COMSIG_HUMAN_DISARM_HIT, COMSIG_LIVING_STATUS_KNOCKDOWN))
return
- RegisterSignal(H, COMSIG_HUMAN_DISARM_HIT, .proc/Knockoff, TRUE)
- RegisterSignal(H, COMSIG_LIVING_STATUS_KNOCKDOWN, .proc/Knockoff_knockdown, TRUE)
-/datum/component/knockoff/proc/OnDropped(datum/source, mob/living/M)
+ RegisterSignal(equipper, COMSIG_HUMAN_DISARM_HIT, .proc/on_equipped_mob_disarm, TRUE)
+ RegisterSignal(equipper, COMSIG_LIVING_STATUS_KNOCKDOWN, .proc/on_equipped_mob_knockdown, TRUE)
+
+/// Signal proc for [COMSIG_ITEM_DROPPED]
+/// Unregisters our signals which can cause a knockdown when we're unequipped (dropped)
+/datum/component/knockoff/proc/on_dropped(datum/source, mob/living/dropper)
SIGNAL_HANDLER
- UnregisterSignal(M, COMSIG_HUMAN_DISARM_HIT)
- UnregisterSignal(M, COMSIG_LIVING_STATUS_KNOCKDOWN)
+ UnregisterSignal(dropper, list(COMSIG_HUMAN_DISARM_HIT, COMSIG_LIVING_STATUS_KNOCKDOWN))
diff --git a/code/datums/components/material_container.dm b/code/datums/components/material_container.dm
index 8241234d775df..6631a744637da 100644
--- a/code/datums/components/material_container.dm
+++ b/code/datums/components/material_container.dm
@@ -301,6 +301,8 @@
if(!materials[req_mat]) //Do we have the resource?
return FALSE //Can't afford it
var/amount_required = mats[x] * multiplier
+ if(amount_required < 0)
+ return FALSE //No negative mats
if(!(materials[req_mat] >= amount_required)) // do we have enough of the resource?
return FALSE //Can't afford it
mats_to_remove[req_mat] += amount_required //Add it to the assoc list of things to remove
diff --git a/code/datums/components/mood.dm b/code/datums/components/mood.dm
index 4cabeb9a31658..f03f8108b5770 100644
--- a/code/datums/components/mood.dm
+++ b/code/datums/components/mood.dm
@@ -48,7 +48,7 @@
RegisterSignal(parent, COMSIG_ADD_MOOD_EVENT_RND, .proc/add_event) //Mood events that are only for RnD members
/datum/component/mood/proc/print_mood(mob/user)
- var/msg = "[span_info("*---------*\nMy current mental status:")]\n"
+ var/msg = "[span_info("My current mental status:")]\n"
msg += span_notice("My current sanity: ") //Long term
switch(sanity)
if(SANITY_GREAT to INFINITY)
@@ -102,7 +102,7 @@
msg += span_boldnicegreen(event.description + "\n")
else
msg += "[span_grey("I don't have much of a reaction to anything right now.")]\n"
- to_chat(user, msg)
+ to_chat(user, examine_block(msg))
///Called after moodevent/s have been added/removed.
/datum/component/mood/proc/update_mood()
diff --git a/code/datums/components/overlay_lighting.dm b/code/datums/components/overlay_lighting.dm
index 9fd0e0a4f5af7..67290d4c58c35 100644
--- a/code/datums/components/overlay_lighting.dm
+++ b/code/datums/components/overlay_lighting.dm
@@ -111,7 +111,6 @@
. = ..()
if(directional)
RegisterSignal(parent, COMSIG_ATOM_DIR_CHANGE, .proc/on_parent_dir_change)
- RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_parent_moved)
RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_RANGE, .proc/set_range)
RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_POWER, .proc/set_power)
RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_COLOR, .proc/set_color)
@@ -119,6 +118,7 @@
RegisterSignal(parent, COMSIG_ATOM_UPDATE_LIGHT_FLAGS, .proc/on_light_flags_change)
RegisterSignal(parent, COMSIG_ATOM_USED_IN_CRAFT, .proc/on_parent_crafted)
RegisterSignal(parent, COMSIG_LIGHT_EATER_QUEUE, .proc/on_light_eater)
+ RegisterSignal(parent, COMSIG_MOVABLE_MOVED, .proc/on_parent_moved)
var/atom/movable/movable_parent = parent
if(movable_parent.light_flags & LIGHT_ATTACHED)
overlay_lighting_flags |= LIGHTING_ATTACHED
@@ -245,8 +245,9 @@
return
if(new_holder != parent && new_holder != parent_attached_to)
RegisterSignal(new_holder, COMSIG_PARENT_QDELETING, .proc/on_holder_qdel)
- RegisterSignal(new_holder, COMSIG_MOVABLE_MOVED, .proc/on_holder_moved)
RegisterSignal(new_holder, COMSIG_LIGHT_EATER_QUEUE, .proc/on_light_eater)
+ if(overlay_lighting_flags & LIGHTING_ON)
+ RegisterSignal(new_holder, COMSIG_MOVABLE_MOVED, .proc/on_holder_moved)
if(directional)
RegisterSignal(new_holder, COMSIG_ATOM_DIR_CHANGE, .proc/on_holder_dir_change)
set_direction(new_holder.dir)
@@ -423,6 +424,8 @@
cast_directional_light()
add_dynamic_lumi()
overlay_lighting_flags |= LIGHTING_ON
+ if(current_holder && current_holder != parent && current_holder != parent_attached_to)
+ RegisterSignal(current_holder, COMSIG_MOVABLE_MOVED, .proc/on_holder_moved)
get_new_turfs()
@@ -433,6 +436,8 @@
if(current_holder)
remove_dynamic_lumi()
overlay_lighting_flags &= ~LIGHTING_ON
+ if(current_holder)
+ UnregisterSignal(current_holder, COMSIG_MOVABLE_MOVED)
clean_old_turfs()
diff --git a/code/datums/components/plumbing/_plumbing.dm b/code/datums/components/plumbing/_plumbing.dm
index 47e4f5807324a..c3c82775aa83f 100644
--- a/code/datums/components/plumbing/_plumbing.dm
+++ b/code/datums/components/plumbing/_plumbing.dm
@@ -22,10 +22,10 @@
var/recipient_reagents_holder
///How do we apply the new reagents to the receiver? Generally doesn't matter, but some stuff, like people, does care if its injected or whatevs
var/methods
- ///What color is our demand connect? Also it's not auto-colored so you'll have to make new sprites if its anything other than red, blue, yellow or green
- var/demand_color = "red"
- ///What color is our supply connect? Also, refrain from pointlessly using non-standard colors unless it's really funny or something
- var/supply_color = "blue"
+ ///What color is our demand connect?
+ var/demand_color = COLOR_RED
+ ///What color is our supply connect?
+ var/supply_color = COLOR_BLUE
///turn_connects is for wheter or not we spin with the object to change our pipes
/datum/component/plumbing/Initialize(start=TRUE, _ducting_layer, _turn_connects=TRUE, datum/reagents/custom_receiver)
@@ -35,14 +35,14 @@
if(_ducting_layer)
ducting_layer = _ducting_layer
- var/atom/movable/AM = parent
- if(!AM.reagents && !custom_receiver)
+ var/atom/movable/parent_movable = parent
+ if(!parent_movable.reagents && !custom_receiver)
return COMPONENT_INCOMPATIBLE
- reagents = AM.reagents
+ reagents = parent_movable.reagents
turn_connects = _turn_connects
- set_recipient_reagents_holder(custom_receiver ? custom_receiver : AM.reagents)
+ set_recipient_reagents_holder(custom_receiver ? custom_receiver : parent_movable.reagents)
if(start)
//We're registering here because I need to check whether we start active or not, and this is just easier
@@ -76,10 +76,10 @@
send_request(D)
///Can we be added to the ductnet?
-/datum/component/plumbing/proc/can_add(datum/ductnet/D, dir)
+/datum/component/plumbing/proc/can_add(datum/ductnet/ductnet, dir)
if(!active)
return
- if(!dir || !D)
+ if(!dir || !ductnet)
return FALSE
if(num2text(dir) in ducts)
return FALSE
@@ -97,15 +97,13 @@
if(!ducts.Find(num2text(dir)))
return
net = ducts[num2text(dir)]
- for(var/A in net.suppliers)
- var/datum/component/plumbing/supplier = A
+ for(var/datum/component/plumbing/supplier as anything in net.suppliers)
if(supplier.can_give(amount, reagent, net))
valid_suppliers += supplier
// Need to ask for each in turn very carefully, making sure we get the total volume. This is to avoid a division that would always round down and become 0
var/targetVolume = reagents.total_volume + amount
var/suppliersLeft = valid_suppliers.len
- for(var/A in valid_suppliers)
- var/datum/component/plumbing/give = A
+ for(var/datum/component/plumbing/give as anything in valid_suppliers)
var/currentRequest = (targetVolume - reagents.total_volume) / suppliersLeft
give.transfer_to(src, currentRequest, reagent, net)
suppliersLeft--
@@ -116,9 +114,8 @@
return
if(reagent) //only asked for one type of reagent
- for(var/A in reagents.reagent_list)
- var/datum/reagent/R = A
- if(R.type == reagent)
+ for(var/datum/reagent/contained_reagent as anything in reagents.reagent_list)
+ if(contained_reagent.type == reagent)
return TRUE
else if(reagents.total_volume > 0) //take whatever
return TRUE
@@ -133,7 +130,7 @@
reagents.trans_to(target.recipient_reagents_holder, amount, round_robin = TRUE, methods = methods)//we deal with alot of precise calculations so we round_robin=TRUE. Otherwise we get floating point errors, 1 != 1 and 2.5 + 2.5 = 6
///We create our luxurious piping overlays/underlays, to indicate where we do what. only called once if use_overlays = TRUE in Initialize()
-/datum/component/plumbing/proc/create_overlays(atom/movable/AM, list/overlays)
+/datum/component/plumbing/proc/create_overlays(atom/movable/parent_movable, list/overlays)
SIGNAL_HANDLER
if(tile_covered || !use_overlays)
@@ -158,39 +155,30 @@
var/duct_y = offset
- for(var/D in GLOB.cardinals)
+ for(var/direction in GLOB.cardinals)
var/color
- var/direction
- if(D & initial(demand_connects))
+ if(direction & initial(demand_connects))
color = demand_color
- else if(D & initial(supply_connects))
+ else if(direction & initial(supply_connects))
color = supply_color
else
continue
- var/image/I
-
- switch(D)
- if(NORTH)
- direction = "north"
- if(SOUTH)
- direction = "south"
- if(EAST)
- direction = "east"
- if(WEST)
- direction = "west"
+ var/direction_text = dir2text(direction)
+ var/duct_layer = PLUMBING_PIPE_VISIBILE_LAYER + ducting_layer * 0.0003
+ var/image/overlay
if(turn_connects)
- I = image('icons/obj/plumbing/connects.dmi', "[direction]-[color]", layer = AM.layer - 1)
-
+ overlay = image('icons/obj/plumbing/connects.dmi', "[direction_text]-[ducting_layer]", layer = duct_layer)
else
- I = image('icons/obj/plumbing/connects.dmi', "[direction]-[color]-s", layer = AM.layer - 1) //color is not color as in the var, it's just the name of the icon_state
- I.dir = D
+ overlay = image('icons/obj/plumbing/connects.dmi', "[direction_text]-[ducting_layer]-s", layer = duct_layer)
+ overlay.dir = direction
- I.pixel_x = duct_x
- I.pixel_y = duct_y
+ overlay.color = color
+ overlay.pixel_x = duct_x
+ overlay.pixel_y = duct_y
- overlays += I
+ overlays += overlay
///we stop acting like a plumbing thing and disconnect if we are, so we can safely be moved and stuff
/datum/component/plumbing/proc/disable()
@@ -201,19 +189,21 @@
STOP_PROCESSING(SSplumbing, src)
- for(var/A in ducts)
- var/datum/ductnet/D = ducts[A]
- D.remove_plumber(src)
+ for(var/duct_dir in ducts)
+ var/datum/ductnet/duct = ducts[duct_dir]
+ duct.remove_plumber(src)
active = FALSE
- for(var/D in GLOB.cardinals)
- if(D & (demand_connects | supply_connects))
- for(var/obj/machinery/duct/duct in get_step(parent, D))
- if(duct.duct_layer == ducting_layer)
- duct.remove_connects(turn(D, 180))
- duct.neighbours.Remove(parent)
- duct.update_appearance()
+ for(var/direction in GLOB.cardinals)
+ if(!(direction & (demand_connects | supply_connects)))
+ continue
+ for(var/obj/machinery/duct/duct in get_step(parent, direction))
+ if(!(duct.duct_layer & ducting_layer))
+ continue
+ duct.remove_connects(turn(direction, 180))
+ duct.neighbours.Remove(parent)
+ duct.update_appearance()
///settle wherever we are, and start behaving like a piece of plumbing
/datum/component/plumbing/proc/enable(obj/object, datum/component/component)
@@ -225,29 +215,32 @@
update_dir()
active = TRUE
- var/atom/movable/AM = parent
- for(var/obj/machinery/duct/D in AM.loc) //Destroy any ducts under us. Ducts also self-destruct if placed under a plumbing machine. machines disable when they get moved
- if(D.anchored) //that should cover everything
- D.disconnect_duct()
+ var/atom/movable/parent_movable = parent
+ // Destroy any ducts under us on the same layer.
+ // Ducts also self-destruct if placed under a plumbing machine.
+ // Machines disable when they get moved
+ for(var/obj/machinery/duct/duct in parent_movable.loc)
+ if(duct.anchored && (duct.duct_layer & ducting_layer))
+ duct.disconnect_duct()
if(demand_connects)
START_PROCESSING(SSplumbing, src)
- for(var/D in GLOB.cardinals)
-
- if(D & (demand_connects | supply_connects))
- for(var/atom/movable/A in get_step(parent, D))
+ for(var/direction in GLOB.cardinals)
+ if(!(direction & (demand_connects | supply_connects)))
+ continue
+ for(var/atom/movable/found_atom in get_step(parent, direction))
+ if(istype(found_atom, /obj/machinery/duct))
+ var/obj/machinery/duct/duct = found_atom
+ duct.attempt_connect()
+ continue
- if(istype(A, /obj/machinery/duct))
- var/obj/machinery/duct/duct = A
- duct.attempt_connect()
- else
- for(var/datum/component/plumbing/plumber as anything in A.GetComponents(/datum/component/plumbing))
- if(plumber.ducting_layer == ducting_layer)
- direct_connect(plumber, D)
+ for(var/datum/component/plumbing/plumber as anything in found_atom.GetComponents(/datum/component/plumbing))
+ if(plumber.ducting_layer & ducting_layer)
+ direct_connect(plumber, direction)
/// Toggle our machinery on or off. This is called by a hook from default_unfasten_wrench with anchored as only param, so we dont have to copypaste this on every object that can move
-/datum/component/plumbing/proc/toggle_active(obj/O, new_state)
+/datum/component/plumbing/proc/toggle_active(obj/parent_obj, new_state)
SIGNAL_HANDLER
if(new_state)
enable()
@@ -271,45 +264,44 @@
demand_connects = initial(demand_connects)
supply_connects = initial(supply_connects)
else
- for(var/D in GLOB.cardinals)
- if(D & initial(demand_connects))
- new_demand_connects += turn(D, angle)
- if(D & initial(supply_connects))
- new_supply_connects += turn(D, angle)
+ for(var/direction in GLOB.cardinals)
+ if(direction & initial(demand_connects))
+ new_demand_connects += turn(direction, angle)
+ if(direction & initial(supply_connects))
+ new_supply_connects += turn(direction, angle)
demand_connects = new_demand_connects
supply_connects = new_supply_connects
///Give the direction of a pipe, and it'll return wich direction it originally was when it's object pointed SOUTH
/datum/component/plumbing/proc/get_original_direction(dir)
- var/atom/movable/AM = parent
- return turn(dir, dir2angle(AM.dir) - 180)
+ var/atom/movable/parent_movable = parent
+ return turn(dir, dir2angle(parent_movable.dir) - 180)
//special case in-case we want to connect directly with another machine without a duct
-/datum/component/plumbing/proc/direct_connect(datum/component/plumbing/P, dir)
- if(!P.active)
+/datum/component/plumbing/proc/direct_connect(datum/component/plumbing/plumbing, dir)
+ if(!plumbing.active)
return
var/opposite_dir = turn(dir, 180)
- if(P.demand_connects & opposite_dir && supply_connects & dir || P.supply_connects & opposite_dir && demand_connects & dir) //make sure we arent connecting two supplies or demands
+ if(plumbing.demand_connects & opposite_dir && supply_connects & dir || plumbing.supply_connects & opposite_dir && demand_connects & dir) //make sure we arent connecting two supplies or demands
var/datum/ductnet/net = new()
net.add_plumber(src, dir)
- net.add_plumber(P, opposite_dir)
+ net.add_plumber(plumbing, opposite_dir)
-/datum/component/plumbing/proc/hide(atom/movable/AM, should_hide)
+/datum/component/plumbing/proc/hide(atom/movable/parent_obj, should_hide)
SIGNAL_HANDLER
tile_covered = should_hide
- AM.update_appearance()
+ parent_obj.update_appearance()
-/datum/component/plumbing/proc/change_ducting_layer(obj/caller, obj/O, new_layer = DUCT_LAYER_DEFAULT)
+/datum/component/plumbing/proc/change_ducting_layer(obj/caller, obj/changer, new_layer = DUCT_LAYER_DEFAULT)
SIGNAL_HANDLER
ducting_layer = new_layer
- if(ismovable(parent))
- var/atom/movable/AM = parent
- AM.update_appearance()
+ var/atom/movable/parent_movable = parent
+ parent_movable.update_appearance()
- if(O)
- playsound(O, 'sound/items/ratchet.ogg', 10, TRUE) //sound
+ if(changer)
+ playsound(changer, 'sound/items/ratchet.ogg', 10, TRUE) //sound
//quickly disconnect and reconnect the network.
if(active)
@@ -348,7 +340,7 @@
demand_connects = NORTH
supply_connects = SOUTH
-/datum/component/plumbing/manifold/change_ducting_layer(obj/caller, obj/O, new_layer)
+/datum/component/plumbing/manifold/change_ducting_layer(obj/caller, obj/changer, new_layer)
return
#define READY 2
diff --git a/code/datums/components/plumbing/reaction_chamber.dm b/code/datums/components/plumbing/reaction_chamber.dm
index fe6064cccc838..c750fda714255 100644
--- a/code/datums/components/plumbing/reaction_chamber.dm
+++ b/code/datums/components/plumbing/reaction_chamber.dm
@@ -40,7 +40,7 @@
///Special connect that we currently use for reaction chambers. Being used so we can keep certain inputs separate, like into a special internal acid container
/datum/component/plumbing/acidic_input
demand_connects = WEST
- demand_color = "yellow"
+ demand_color = COLOR_YELLOW
ducting_layer = SECOND_DUCT_LAYER
@@ -50,7 +50,7 @@
///Special connect that we currently use for reaction chambers. Being used so we can keep certain inputs separate, like into a special internal base container
/datum/component/plumbing/alkaline_input
demand_connects = EAST
- demand_color = "green"
+ demand_color = COLOR_VIBRANT_LIME
ducting_layer = FOURTH_DUCT_LAYER
diff --git a/code/datums/components/remote_materials.dm b/code/datums/components/remote_materials.dm
index 223b483f8e41c..2ae9bd9677597 100644
--- a/code/datums/components/remote_materials.dm
+++ b/code/datums/components/remote_materials.dm
@@ -28,6 +28,7 @@ handles linking back and forth.
RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/OnAttackBy)
RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_MULTITOOL), .proc/OnMultitool)
+ RegisterSignal(parent, COMSIG_MOVABLE_Z_CHANGED, .proc/check_z_level)
var/turf/T = get_turf(parent)
if (force_connect || (mapload && is_station_level(T.z)))
@@ -38,14 +39,14 @@ handles linking back and forth.
/datum/component/remote_materials/proc/LateInitialize()
silo = GLOB.ore_silo_default
if (silo)
- silo.connected += src
+ silo.ore_connected_machines += src
mat_container = silo.GetComponent(/datum/component/material_container)
else
_MakeLocal()
/datum/component/remote_materials/Destroy()
if (silo)
- silo.connected -= src
+ silo.ore_connected_machines -= src
silo.updateUsrDialog()
silo = null
mat_container = null
@@ -105,19 +106,33 @@ handles linking back and forth.
if (silo == M.buffer)
to_chat(user, span_warning("[parent] is already connected to [silo]!"))
return COMPONENT_BLOCK_TOOL_ATTACK
+ var/turf/silo_turf = get_turf(M.buffer)
+ var/turf/user_loc = get_turf(user)
+ if(!is_valid_z_level(silo_turf, user_loc))
+ to_chat(user, span_warning("[parent] is too far away to get a connection signal!"))
+ return COMPONENT_BLOCK_TOOL_ATTACK
if (silo)
- silo.connected -= src
+ silo.ore_connected_machines -= src
silo.updateUsrDialog()
else if (mat_container)
mat_container.retrieve_all()
qdel(mat_container)
silo = M.buffer
- silo.connected += src
+ silo.ore_connected_machines += src
silo.updateUsrDialog()
mat_container = silo.GetComponent(/datum/component/material_container)
to_chat(user, span_notice("You connect [parent] to [silo] from the multitool's buffer."))
return COMPONENT_BLOCK_TOOL_ATTACK
+/datum/component/remote_materials/proc/check_z_level(datum/source, turf/old_turf, turf/new_turf)
+ SIGNAL_HANDLER
+ if(!silo)
+ return
+
+ var/turf/silo_turf = get_turf(silo)
+ if(!is_valid_z_level(silo_turf, new_turf))
+ disconnect_from(silo)
+
/datum/component/remote_materials/proc/on_hold()
return silo?.holds["[get_area(parent)]/[category]"]
diff --git a/code/datums/components/riding/riding_mob.dm b/code/datums/components/riding/riding_mob.dm
index 85d144ea857da..471720cb6c4f1 100644
--- a/code/datums/components/riding/riding_mob.dm
+++ b/code/datums/components/riding/riding_mob.dm
@@ -137,11 +137,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)
@@ -150,13 +147,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
new file mode 100644
index 0000000000000..8eb63d8dfe0ad
--- /dev/null
+++ b/code/datums/components/seclight_attachable.dm
@@ -0,0 +1,293 @@
+/**
+ * Component which allows you to attach a seclight to an item,
+ * be it a piece of clothing or a tool.
+ */
+/datum/component/seclite_attachable
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ /// Whether we can remove the light with a screwdriver or not.
+ var/is_light_removable = TRUE
+ /// If passed, we wil simply update our item's icon_state when a light is attached.
+ /// Formatted as parent_base_state-[light_icon-state]-"on"
+ var/light_icon_state
+ /// If passed, we will add overlays to the item when a light is attached.
+ /// This is the icon file it grabs the overlay from.
+ var/light_overlay_icon
+ /// The state to take from the light overlay icon if supplied.
+ var/light_overlay
+ /// The X offset of our overlay if supplied.
+ var/overlay_x = 0
+ /// The Y offset of our overlay if supplied.
+ var/overlay_y = 0
+
+ // Internal vars.
+ /// A reference to the actual light that's attached.
+ var/obj/item/flashlight/seclite/light
+ /// A weakref to the item action we add with the light.
+ var/datum/weakref/toggle_action_ref
+ /// Static typecache of all lights we consider seclites (all lights we can attach).
+ var/static/list/valid_lights = typecacheof(list(/obj/item/flashlight/seclite))
+
+/datum/component/seclite_attachable/Initialize(
+ obj/item/flashlight/seclite/starting_light,
+ is_light_removable = TRUE,
+ light_icon_state,
+ light_overlay_icon,
+ light_overlay,
+ overlay_x = 0,
+ overlay_y = 0,
+)
+
+ if(!isitem(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ src.is_light_removable = is_light_removable
+ src.light_icon_state = light_icon_state
+ src.light_overlay_icon = light_overlay_icon
+ src.light_overlay = light_overlay
+ src.overlay_x = overlay_x
+ src.overlay_y = overlay_y
+
+ if(istype(starting_light))
+ add_light(starting_light)
+
+/datum/component/seclite_attachable/Destroy(force, silent)
+ if(light)
+ remove_light()
+ return ..()
+
+// Inheriting component allows lights to be added externally to things which already have a mount.
+/datum/component/seclite_attachable/InheritComponent(
+ datum/component/seclite_attachable/new_component,
+ original,
+ obj/item/flashlight/seclite/starting_light,
+ is_light_removable = TRUE,
+ light_icon_state,
+ light_overlay_icon,
+ light_overlay,
+ overlay_x,
+ overlay_y,
+)
+
+ if(!original)
+ return
+
+ src.is_light_removable = is_light_removable
+
+ // For the rest of these arguments, default to what already exists
+ if(light_icon_state)
+ src.light_icon_state = light_icon_state
+ if(light_overlay_icon)
+ src.light_overlay_icon = light_overlay_icon
+ if(light_overlay)
+ src.light_overlay = light_overlay
+ if(overlay_x)
+ src.overlay_x = overlay_x
+ if(overlay_x)
+ src.overlay_y = overlay_y
+
+ if(istype(starting_light))
+ add_light(starting_light)
+
+/datum/component/seclite_attachable/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ATOM_DESTRUCTION, .proc/on_parent_deconstructed)
+ RegisterSignal(parent, COMSIG_ATOM_EXITED, .proc/on_light_exit)
+ RegisterSignal(parent, COMSIG_ATOM_TOOL_ACT(TOOL_SCREWDRIVER), .proc/on_screwdriver)
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_ICON_STATE, .proc/on_update_icon_state)
+ RegisterSignal(parent, COMSIG_ATOM_UPDATE_OVERLAYS, .proc/on_update_overlays)
+ RegisterSignal(parent, COMSIG_ITEM_UI_ACTION_CLICK, .proc/on_action_click)
+ RegisterSignal(parent, COMSIG_PARENT_ATTACKBY, .proc/on_attackby)
+ RegisterSignal(parent, COMSIG_PARENT_EXAMINE, .proc/on_examine)
+ RegisterSignal(parent, COMSIG_PARENT_QDELETING, .proc/on_parent_deleted)
+
+/datum/component/seclite_attachable/UnregisterFromParent()
+ UnregisterSignal(parent, list(
+ COMSIG_ATOM_DESTRUCTION,
+ COMSIG_ATOM_EXITED,
+ COMSIG_ATOM_TOOL_ACT(TOOL_SCREWDRIVER),
+ COMSIG_ATOM_UPDATE_ICON_STATE,
+ COMSIG_ATOM_UPDATE_OVERLAYS,
+ COMSIG_ITEM_UI_ACTION_CLICK,
+ COMSIG_PARENT_ATTACKBY,
+ COMSIG_PARENT_EXAMINE,
+ COMSIG_PARENT_QDELETING,
+ ))
+
+/// Sets a new light as our current light for our parent.
+/datum/component/seclite_attachable/proc/add_light(obj/item/flashlight/new_light, mob/attacher)
+ if(light)
+ CRASH("[type] tried to add a new light when it already had one.")
+
+ light = new_light
+
+ light.set_light_flags(light.light_flags | LIGHT_ATTACHED)
+ // We may already exist within in our parent's contents... But if we don't move it over now
+ if(light.loc != parent)
+ light.forceMove(parent)
+
+ // We already have an action for the light for some reason? Clean it up
+ if(toggle_action_ref?.resolve())
+ stack_trace("[type] - add_light had an existing toggle action when add_light was called.")
+ QDEL_NULL(toggle_action_ref)
+
+ // 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 = item_parent.add_item_action(/datum/action/item_action/toggle_seclight)
+ toggle_action_ref = WEAKREF(toggle_action)
+
+ update_light()
+
+/// Removes the current light from our parent.
+/datum/component/seclite_attachable/proc/remove_light()
+ // Our action may be linked to our parent,
+ // but it's really sourced from our light. Get rid of it.
+ QDEL_NULL(toggle_action_ref)
+
+ // It is possible the light was removed by being deleted.
+ if(!QDELETED(light))
+ UnregisterSignal(light, COMSIG_PARENT_QDELETING)
+ light.set_light_flags(light.light_flags & ~LIGHT_ATTACHED)
+ light.update_brightness()
+
+ light = null
+ update_light()
+
+/// Toggles the light within on or off.
+/// Returns TRUE if there is a light inside, FALSE otherwise.
+/datum/component/seclite_attachable/proc/toggle_light(mob/user)
+ if(!light)
+ return FALSE
+
+ light.on = !light.on
+ light.update_brightness()
+ if(user)
+ user.balloon_alert(user, "[light.name] toggled [light.on ? "on":"off"]")
+
+ playsound(light, 'sound/weapons/empty.ogg', 100, TRUE)
+ update_light()
+ return TRUE
+
+/// Called after the a light is added, removed, or toggles.
+/// Ensures all of our appearances look correct for the new light state.
+/datum/component/seclite_attachable/proc/update_light()
+ var/obj/item/item_parent = parent
+ item_parent.update_appearance()
+ item_parent.update_action_buttons()
+
+/// Signal proc for [COMSIG_ATOM_EXITED] that handles our light being removed or deleted from our parent.
+/datum/component/seclite_attachable/proc/on_light_exit(obj/item/source, atom/movable/gone, direction)
+ SIGNAL_HANDLER
+
+ if(gone == light)
+ remove_light()
+
+/// Signal proc for [COMSIG_ATOM_DESTRUCTION] that drops our light to the ground if our parent is deconstructed.
+/datum/component/seclite_attachable/proc/on_parent_deconstructed(obj/item/source, disassembled)
+ SIGNAL_HANDLER
+
+ light.forceMove(source.drop_location())
+
+/// Signal proc for [COMSIG_PARENT_QDELETING] that deletes our light if our parent is deleted.
+/datum/component/seclite_attachable/proc/on_parent_deleted(obj/item/source)
+ SIGNAL_HANDLER
+
+ QDEL_NULL(light)
+
+/// Signal proc for [COMSIG_ITEM_UI_ACTION_CLICK] that toggles our light on and off if our action button is clicked.
+/datum/component/seclite_attachable/proc/on_action_click(obj/item/source, mob/user, datum/action)
+ SIGNAL_HANDLER
+
+ // This isn't OUR action specifically, we don't care.
+ if(!IS_WEAKREF_OF(action, toggle_action_ref))
+ return
+
+ // Toggle light fails = no light attached = shouldn't be possible
+ if(!toggle_light(user))
+ CRASH("[type] - on_action_click somehow both HAD AN ACTION and also HAD A TRIGGERABLE ACTION, without having an attached light.")
+
+ return COMPONENT_ACTION_HANDLED
+
+/// Signal proc for [COMSIG_PARENT_ATTACKBY] that allows a user to attach a seclite by hitting our parent with it.
+/datum/component/seclite_attachable/proc/on_attackby(obj/item/source, obj/item/attacking_item, mob/attacker, params)
+ SIGNAL_HANDLER
+
+ if(!is_type_in_typecache(attacking_item, valid_lights))
+ return
+
+ if(light)
+ source.balloon_alert(attacker, "already has \a [light]!")
+ return
+
+ if(!attacker.transferItemToLoc(attacking_item, source))
+ return
+
+ add_light(attacking_item, attacker)
+ source.balloon_alert(attacker, "attached [attacking_item]")
+ return COMPONENT_NO_AFTERATTACK
+
+/// Signal proc for [COMSIG_ATOM_TOOL_ACT] via [TOOL_SCREWDRIVER] that removes any attached seclite.
+/datum/component/seclite_attachable/proc/on_screwdriver(obj/item/source, mob/user, obj/item/tool)
+ SIGNAL_HANDLER
+
+ if(!light || !is_light_removable)
+ return
+
+ INVOKE_ASYNC(src, .proc/unscrew_light, source, user, tool)
+ return COMPONENT_BLOCK_TOOL_ATTACK
+
+/// Invoked asyncronously from [proc/on_screwdriver]. Handles removing the light from our parent.
+/datum/component/seclite_attachable/proc/unscrew_light(obj/item/source, mob/user, obj/item/tool)
+ tool?.play_tool_sound(source)
+ source.balloon_alert(user, "unscrewed [light]")
+
+ var/obj/item/flashlight/seclite/to_remove = light
+
+ // The forcemove here will call exited on the light, and automatically update our references / etc
+ to_remove.forceMove(source.drop_location())
+ if(source.Adjacent(user) && !issilicon(user))
+ user.put_in_hands(to_remove)
+
+/// Signal proc for [COMSIG_PARENT_EXAMINE] that shows our item can have / does have a seclite attached.
+/datum/component/seclite_attachable/proc/on_examine(obj/item/source, mob/examiner, list/examine_list)
+ SIGNAL_HANDLER
+
+ if(light)
+ examine_list += "It has \a [light] [is_light_removable ? "mounted on it with a few screws" : "permanently mounted on it"]."
+ else
+ examine_list += "It has a mounting point for a seclite."
+
+/// Signal proc for [COMSIG_ATOM_UPDATE_OVERLAYS] that updates our parent with our seclite overlays, if we have some.
+/datum/component/seclite_attachable/proc/on_update_overlays(obj/item/source, list/overlays)
+ SIGNAL_HANDLER
+
+ // No overlays to add, no reason to run
+ if(!light_overlay || !light_overlay_icon)
+ return
+ // No light, nothing to add
+ if(!light)
+ return
+
+ var/overlay_state = "[light_overlay][light.on ? "_on":""]"
+ var/mutable_appearance/flashlight_overlay = mutable_appearance(light_overlay_icon, overlay_state)
+ flashlight_overlay.pixel_x = overlay_x
+ flashlight_overlay.pixel_y = overlay_y
+ overlays += flashlight_overlay
+
+/// Signal proc for [COMSIG_ATOM_UPDATE_ICON_STATE] that updates our parent's icon state, if we have one.
+/datum/component/seclite_attachable/proc/on_update_icon_state(obj/item/source)
+ SIGNAL_HANDLER
+
+ // No icon state to set, no reason to run
+ if(!light_icon_state)
+ return
+
+ // Get the "base icon state" to work on
+ var/base_state = source.base_icon_state || initial(source.icon_state)
+ // Updates our icon state based on our light state.
+ if(light)
+ source.icon_state = "[base_state]-[light_icon_state][light.on ? "-on":""]"
+
+ // Reset their icon state when if we've got no light.
+ else if(source.icon_state != base_state)
+ // Yes, this might mess with other icon state alterations,
+ // but that's the downside of using icon states over overlays.
+ source.icon_state = base_state
diff --git a/code/datums/components/slippery.dm b/code/datums/components/slippery.dm
index 9301d558f146f..c201ee9c6737e 100644
--- a/code/datums/components/slippery.dm
+++ b/code/datums/components/slippery.dm
@@ -76,7 +76,7 @@
if(!isliving(arrived))
return
var/mob/living/victim = arrived
- if(!(victim.movement_type & FLYING) && victim.slip(knockdown_time, parent, lube_flags, paralyze_time, force_drop_items) && callback)
+ if(!(victim.movement_type & (FLYING | FLOATING)) && victim.slip(knockdown_time, parent, lube_flags, paralyze_time, force_drop_items) && callback)
callback.Invoke(victim)
/*
diff --git a/code/datums/components/stationloving.dm b/code/datums/components/stationloving.dm
index c0a4c30626897..b684ad913a347 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 de4eb6fb6d7d2..37dc4b0de7c97 100644
--- a/code/datums/components/storage/storage.dm
+++ b/code/datums/components/storage/storage.dm
@@ -4,47 +4,68 @@
/datum/component/storage
dupe_mode = COMPONENT_DUPE_UNIQUE
- var/datum/component/storage/concrete/master //If not null, all actions act on master and this is just an access point.
-
- var/list/can_hold //if this is set, only items, and their children, will fit
- var/list/cant_hold //if this is set, items, and their children, won't fit
- var/list/exception_hold //if set, these items will be the exception to the max size of object that can fit.
+ ///If not null, all actions act on master and this is just an access point.
+ var/datum/component/storage/concrete/master
+
+ ///if this is set, only items, and their children, will fit
+ var/list/can_hold
+ ///if this is set, items, and their children, won't fit
+ var/list/cant_hold
+ ///if set, these items will be the exception to the max size of object that can fit.
+ var/list/exception_hold
/// If set can only contain stuff with this single trait present.
var/list/can_hold_trait
var/can_hold_description
- var/list/mob/is_using //lazy list of mobs looking at the contents of this storage.
+ ///lazy list of mobs looking at the contents of this storage.
+ var/list/mob/is_using
- var/locked = FALSE //when locked nothing can see inside or use it.
+ ///when locked nothing can see inside or use it.
+ var/locked = FALSE
- var/max_w_class = WEIGHT_CLASS_SMALL //max size of objects that will fit.
- var/max_combined_w_class = 14 //max combined sizes of objects that will fit.
- var/max_items = 7 //max number of objects that will fit.
+ ///max size of objects that will fit.
+ var/max_w_class = WEIGHT_CLASS_SMALL
+ ///max combined sizes of objects that will fit.
+ var/max_combined_w_class = 14
+ ///max number of objects that will fit.
+ var/max_items = 7
var/emp_shielded = FALSE
- var/silent = FALSE //whether this makes a message when things are put in.
- var/click_gather = FALSE //whether this can be clicked on items to pick it up rather than the other way around.
- var/rustle_sound = TRUE //play rustle sound on interact.
- var/allow_quick_empty = FALSE //allow empty verb which allows dumping on the floor of everything inside quickly.
- var/allow_quick_gather = FALSE //allow toggle mob verb which toggles collecting all items from a tile.
+ ///whether this makes a message when things are put in.
+ var/silent = FALSE
+ ///whether this can be clicked on items to pick it up rather than the other way around.
+ var/click_gather = FALSE
+ ///play rustle sound on interact.
+ var/rustle_sound = TRUE
+ ///allow empty verb which allows dumping on the floor of everything inside quickly.
+ var/allow_quick_empty = FALSE
+ ///allow toggle mob verb which toggles collecting all items from a tile.
+ var/allow_quick_gather = FALSE
var/collection_mode = COLLECT_EVERYTHING
- var/insert_preposition = "in" //you put things "in" a bag, but "on" a tray.
+ ///you put things "in" a bag, but "on" a tray.
+ var/insert_preposition = "in"
- var/display_numerical_stacking = FALSE //stack things of the same type and show as a single object with a number.
+ ///stack things of the same type and show as a single object with a number.
+ var/display_numerical_stacking = FALSE
- var/atom/movable/screen/storage/boxes //storage display object
- var/atom/movable/screen/close/closer //close button object
+ ///storage display object
+ var/atom/movable/screen/storage/boxes
+ ///close button object
+ var/atom/movable/screen/close/closer
- var/allow_big_nesting = FALSE //allow storage objects of the same or greater size.
+ ///allow storage objects of the same or greater size.
+ var/allow_big_nesting = FALSE
- var/attack_hand_interact = TRUE //interact on attack hand.
- var/quickdraw = FALSE //altclick interact
+ ///interact on attack hand.
+ var/attack_hand_interact = TRUE
+ ///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.
@@ -155,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/datum/action/existing = modeswitch_action_ref?.resolve()
+ if(!QDELETED(existing))
return
- var/obj/item/I = parent
- modeswitch_action = new(I)
+
+ 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)))
@@ -420,6 +442,9 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches)
M.client.screen |= closer
M.client.screen |= real_location.contents
M.set_active_storage(src)
+ if(ismovable(real_location))
+ var/atom/movable/movable_loc = real_location
+ movable_loc.become_active_storage(src)
LAZYOR(is_using, M)
RegisterSignal(M, COMSIG_PARENT_QDELETING, .proc/mob_deleted)
return TRUE
@@ -437,6 +462,10 @@ GLOBAL_LIST_EMPTY(cached_storage_typecaches)
if(!M.client)
return TRUE
var/atom/real_location = real_location()
+ if(!length(is_using) && ismovable(real_location))
+ var/atom/movable/movable_loc = real_location
+ movable_loc.lose_active_storage(src)
+
M.client.screen -= boxes
M.client.screen -= closer
M.client.screen -= real_location.contents
diff --git a/code/datums/components/wet_floor.dm b/code/datums/components/wet_floor.dm
index 594ab849e039f..7b84f1dabb77d 100644
--- a/code/datums/components/wet_floor.dm
+++ b/code/datums/components/wet_floor.dm
@@ -127,8 +127,7 @@
decrease = max(0, decrease)
if((is_wet() & TURF_WET_ICE) && t > T0C) //Ice melts into water!
for(var/obj/O in T.contents)
- if(O.obj_flags & FROZEN)
- O.make_unfrozen()
+ O.make_unfrozen()
add_wet(TURF_WET_WATER, max_time_left())
dry(null, TURF_WET_ICE)
dry(null, ALL, FALSE, decrease)
diff --git a/code/datums/diseases/_MobProcs.dm b/code/datums/diseases/_MobProcs.dm
index 024f1ee94f2c1..795af490991b7 100644
--- a/code/datums/diseases/_MobProcs.dm
+++ b/code/datums/diseases/_MobProcs.dm
@@ -64,6 +64,9 @@
if(ishuman(src))
var/mob/living/carbon/human/infecting_human = src
+ if(infecting_human.reagents.has_reagent(/datum/reagent/medicine/spaceacillin) && prob(75))
+ return
+
switch(target_zone)
if(BODY_ZONE_HEAD)
if(isobj(infecting_human.head))
@@ -92,6 +95,11 @@
disease.try_infect(src)
/mob/living/proc/AirborneContractDisease(datum/disease/disease, force_spread)
+ if(ishuman(src))
+ var/mob/living/carbon/human/infecting_human = src
+ if(infecting_human.reagents.has_reagent(/datum/reagent/medicine/spaceacillin) && prob(75))
+ return
+
if(((disease.spread_flags & DISEASE_SPREAD_AIRBORNE) || force_spread) && prob((50*disease.spreading_modifier) - 1))
ForceContractDisease(disease)
diff --git a/code/datums/diseases/_disease.dm b/code/datums/diseases/_disease.dm
index f85b0a4ca89e9..d420d838b85aa 100644
--- a/code/datums/diseases/_disease.dm
+++ b/code/datums/diseases/_disease.dm
@@ -64,6 +64,8 @@
///Proc to process the disease and decide on whether to advance, cure or make the sympthoms appear. Returns a boolean on whether to continue acting on the symptoms or not.
/datum/disease/proc/stage_act(delta_time, times_fired)
+ var/slowdown = affected_mob.reagents.has_reagent(/datum/reagent/medicine/spaceacillin) ? 0.5 : 1 // spaceacillin slows stage speed by 50%
+
if(has_cure())
if(DT_PROB(cure_chance, delta_time))
update_stage(max(stage - 1, 1))
@@ -71,8 +73,7 @@
if(disease_flags & CURABLE && DT_PROB(cure_chance, delta_time))
cure()
return FALSE
-
- else if(DT_PROB(stage_prob, delta_time))
+ else if(DT_PROB(stage_prob*slowdown, delta_time))
update_stage(min(stage + 1, max_stages))
return !carrier
diff --git a/code/datums/diseases/advance/symptoms/confusion.dm b/code/datums/diseases/advance/symptoms/confusion.dm
index 3e842ce2a0a0a..2dd9ed57f0e76 100644
--- a/code/datums/diseases/advance/symptoms/confusion.dm
+++ b/code/datums/diseases/advance/symptoms/confusion.dm
@@ -18,42 +18,55 @@
base_message_chance = 25
symptom_delay_min = 10
symptom_delay_max = 30
- var/brain_damage = FALSE
threshold_descs = list(
+ "Stage Speed 6" = "Prevents any form of reading or writing.",
"Resistance 6" = "Causes brain damage over time.",
"Transmission 6" = "Increases confusion duration and strength.",
"Stealth 4" = "The symptom remains hidden until active.",
)
+ var/brain_damage = FALSE
+ var/causes_illiteracy = FALSE
-/datum/symptom/confusion/Start(datum/disease/advance/A)
+/datum/symptom/confusion/Start(datum/disease/advance/advanced_disease)
. = ..()
if(!.)
return
- if(A.totalResistance() >= 6)
+ if(advanced_disease.totalStageSpeed() >= 6)
+ causes_illiteracy = TRUE
+ if(advanced_disease.totalResistance() >= 6)
brain_damage = TRUE
- if(A.totalTransmittable() >= 6)
+ if(advanced_disease.totalTransmittable() >= 6)
power = 1.5
- if(A.totalStealth() >= 4)
+ if(advanced_disease.totalStealth() >= 4)
suppress_warning = TRUE
-/datum/symptom/confusion/End(datum/disease/advance/A)
- A.affected_mob.remove_status_effect(/datum/status_effect/confusion)
+/datum/symptom/confusion/End(datum/disease/advance/advanced_disease)
+ advanced_disease.affected_mob.remove_status_effect(/datum/status_effect/confusion)
+ REMOVE_TRAIT(advanced_disease.affected_mob, TRAIT_ILLITERATE, DISEASE_TRAIT)
return ..()
-/datum/symptom/confusion/Activate(datum/disease/advance/A)
+/datum/symptom/confusion/Activate(datum/disease/advance/advanced_disease)
. = ..()
if(!.)
return
- var/mob/living/carbon/M = A.affected_mob
- switch(A.stage)
+ var/mob/living/carbon/infected_mob = advanced_disease.affected_mob
+ switch(advanced_disease.stage)
if(1, 2, 3, 4)
if(prob(base_message_chance) && !suppress_warning)
- to_chat(M, span_warning("[pick("Your head hurts.", "Your mind blanks for a moment.")]"))
+ to_chat(infected_mob, span_warning("[pick("Your head hurts.", "Your mind blanks for a moment.")]"))
else
- to_chat(M, span_userdanger("You can't think straight!"))
- M.adjust_timed_status_effect(16 SECONDS * power, /datum/status_effect/confusion)
+ to_chat(infected_mob, span_userdanger("You can't think straight!"))
+ infected_mob.adjust_timed_status_effect(16 SECONDS * power, /datum/status_effect/confusion)
if(brain_damage)
- M.adjustOrganLoss(ORGAN_SLOT_BRAIN, 3 * power, 80)
- M.updatehealth()
-
+ infected_mob.adjustOrganLoss(ORGAN_SLOT_BRAIN, 3 * power, 80)
+ infected_mob.updatehealth()
return
+
+/datum/symptom/confusion/on_stage_change(datum/disease/advance/advanced_disease)
+ . = ..()
+ if(!.)
+ return FALSE
+ var/mob/living/carbon/infected_mob = advanced_disease.affected_mob
+ if(advanced_disease.stage >= 4 && causes_illiteracy)
+ ADD_TRAIT(infected_mob, TRAIT_ILLITERATE, DISEASE_TRAIT)
+ return TRUE
diff --git a/code/datums/diseases/transformation.dm b/code/datums/diseases/transformation.dm
index cfa0afad345db..82d758794985b 100644
--- a/code/datums/diseases/transformation.dm
+++ b/code/datums/diseases/transformation.dm
@@ -227,7 +227,7 @@
stage3 = list("Your appendages are melting away.", "Your limbs begin to lose their shape.")
stage4 = list("You are turning into a slime.")
stage5 = list("You have become a slime.")
- new_form = /mob/living/simple_animal/slime/random
+ new_form = /mob/living/simple_animal/slime
/datum/disease/transformation/slime/stage_act(delta_time, times_fired)
@@ -237,15 +237,22 @@
switch(stage)
if(1)
- if(ishuman(affected_mob) && affected_mob.dna)
- if(affected_mob.dna.species.id == SPECIES_SLIMEPERSON || affected_mob.dna.species.id == SPECIES_STARGAZER || affected_mob.dna.species.id == SPECIES_LUMINESCENT)
+ if(ishuman(affected_mob))
+ var/mob/living/carbon/human/human = affected_mob
+ if(isjellyperson(human))
stage = 5
if(3)
if(ishuman(affected_mob))
var/mob/living/carbon/human/human = affected_mob
- if(human.dna.species.id != SPECIES_SLIMEPERSON && affected_mob.dna.species.id != SPECIES_STARGAZER && affected_mob.dna.species.id != SPECIES_LUMINESCENT)
+ if(!ismonkey(human) && !isjellyperson(human))
human.set_species(/datum/species/jelly/slime)
+/datum/disease/transformation/slime/do_disease_transformation(mob/living/affected_mob)
+ if(affected_mob.client && ishuman(affected_mob)) // if they are a human who's not a monkey and are sentient, then let them have the old fun
+ var/mob/living/carbon/human/human = affected_mob
+ if(!ismonkey(human))
+ new_form = /mob/living/simple_animal/slime/random
+ return ..()
/datum/disease/transformation/corgi
name = "The Barkening"
diff --git a/code/datums/elements/_element.dm b/code/datums/elements/_element.dm
index 30bd98a326810..204a2742011e9 100644
--- a/code/datums/elements/_element.dm
+++ b/code/datums/elements/_element.dm
@@ -32,9 +32,9 @@
/// Deactivates the functionality defines by the element on the given datum
/datum/element/proc/Detach(datum/source, ...)
SIGNAL_HANDLER
+ SHOULD_CALL_PARENT(TRUE)
SEND_SIGNAL(source, COMSIG_ELEMENT_DETACH, src)
- SHOULD_CALL_PARENT(TRUE)
UnregisterSignal(source, COMSIG_PARENT_QDELETING)
/datum/element/Destroy(force)
diff --git a/code/datums/elements/atmos_sensitive.dm b/code/datums/elements/atmos_sensitive.dm
index ca676327e26b9..c7dd4cf2f7a87 100644
--- a/code/datums/elements/atmos_sensitive.dm
+++ b/code/datums/elements/atmos_sensitive.dm
@@ -22,6 +22,7 @@
/datum/element/atmos_sensitive/Detach(datum/source)
var/atom/us = source
us.RemoveElement(/datum/element/connect_loc, pass_on)
+ UnregisterSignal(source, COMSIG_MOVABLE_MOVED)
if(us.flags_1 & ATMOS_IS_PROCESSING_1)
us.atmos_end()
SSair.atom_process -= us
diff --git a/code/datums/elements/blood_walk.dm b/code/datums/elements/blood_walk.dm
deleted file mode 100644
index 2b37bea162528..0000000000000
--- a/code/datums/elements/blood_walk.dm
+++ /dev/null
@@ -1,56 +0,0 @@
-///Blood walk, a bespoke element that causes you to make blood wherever you walk.
-/datum/element/blood_walk
- element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH
- id_arg_index = 2
-
- ///A unique blood type we might want to spread
- var/blood_type
- ///The sound that plays when we spread blood.
- var/sound_played
- ///How loud will the sound be, if there is one.
- var/sound_volume
- ///The chance of spawning blood whenever walking
- var/blood_spawn_chance
- ///Should the decal face the direction of the target
- var/target_dir_change
-
-
-/datum/element/blood_walk/Attach(
- datum/target,
- blood_type = /obj/effect/decal/cleanable/blood,
- sound_played,
- sound_volume = 80,
- blood_spawn_chance = 100,
- target_dir_change = FALSE
-)
- . = ..()
- if(!ismovable(target))
- return ELEMENT_INCOMPATIBLE
-
- src.blood_type = blood_type
- src.sound_played = sound_played
- src.sound_volume = sound_volume
- src.blood_spawn_chance = blood_spawn_chance
- src.target_dir_change = target_dir_change
- RegisterSignal(target, COMSIG_MOVABLE_MOVED, .proc/spread_blood)
-
-/datum/element/blood_walk/Detach(datum/target)
- . = ..()
- UnregisterSignal(target, COMSIG_MOVABLE_MOVED)
-
-///Spawns blood (if possible) under the source, and plays a sound effect (if any)
-/datum/element/blood_walk/proc/spread_blood(datum/source)
- SIGNAL_HANDLER
-
- var/atom/movable/movable_source = source
- var/turf/current_turf = movable_source.loc
- if(!isturf(current_turf))
- return
- if(!prob(blood_spawn_chance))
- return
-
- var/obj/effect/decal/blood = new blood_type(current_turf)
- if (target_dir_change)
- blood.setDir(movable_source.dir)
- if(!isnull(sound_played))
- playsound(movable_source, sound_played, sound_volume, TRUE, 2, TRUE)
diff --git a/code/datums/elements/chewable.dm b/code/datums/elements/chewable.dm
index 91e9a0ddac922..a5e10e26d8d59 100644
--- a/code/datums/elements/chewable.dm
+++ b/code/datums/elements/chewable.dm
@@ -30,6 +30,7 @@
RegisterSignal(target, COMSIG_ITEM_EQUIPPED, .proc/on_equipped)
/datum/element/chewable/Detach(datum/source, force)
+ . = ..()
processing -= source
UnregisterSignal(source, list(COMSIG_ITEM_DROPPED, COMSIG_ITEM_EQUIPPED))
diff --git a/code/datums/elements/diggable.dm b/code/datums/elements/diggable.dm
new file mode 100644
index 0000000000000..e57b24fd25092
--- /dev/null
+++ b/code/datums/elements/diggable.dm
@@ -0,0 +1,46 @@
+/// Lets you make hitting a turf with a shovel pop something out, and scrape the turf
+/datum/element/diggable
+ element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH
+ id_arg_index = 2
+ /// Typepath of what we spawn on shovel
+ var/atom/to_spawn
+ /// Amount to spawn on shovel
+ var/amount
+ /// What should we tell the user they did? (Eg: "You dig up the turf.")
+ var/action_text
+ /// What should we tell other people what the user did? (Eg: "Guy digs up the turf.")
+ var/action_text_third_person
+
+/datum/element/diggable/Attach(datum/target, to_spawn, amount = 1, action_text = "dig up", action_text_third_person = "digs up")
+ . = ..()
+ if(!isturf(target))
+ return ELEMENT_INCOMPATIBLE
+ if(!to_spawn)
+ stack_trace("[type] wasn't passed a typepath to spawn attaching to [target].")
+ return ELEMENT_INCOMPATIBLE
+
+ src.to_spawn = to_spawn
+ src.amount = amount
+ src.action_text = action_text
+ src.action_text_third_person = action_text_third_person
+
+ RegisterSignal(target, COMSIG_ATOM_TOOL_ACT(TOOL_SHOVEL), .proc/on_shovel)
+
+/datum/element/diggable/Detach(datum/source, ...)
+ . = ..()
+ UnregisterSignal(source, COMSIG_ATOM_TOOL_ACT(TOOL_SHOVEL))
+
+/// Signal proc for [COMSIG_ATOM_TOOL_ACT] via [TOOL_SHOVEL].
+/datum/element/diggable/proc/on_shovel(turf/source, mob/user, obj/item/tool)
+ SIGNAL_HANDLER
+
+ for(var/i in 1 to amount)
+ new to_spawn(source)
+
+ user.visible_message(
+ span_notice("[user] [action_text_third_person] [source]."),
+ span_notice("You [action_text] [source]."),
+ )
+
+ playsound(source, 'sound/effects/shovel_dig.ogg', 50, TRUE)
+ source.ScrapeAway(flags = CHANGETURF_INHERIT_AIR)
diff --git a/code/datums/elements/food/processable.dm b/code/datums/elements/food/processable.dm
index 6a9d98ced3c0e..7ac0539b36c33 100644
--- a/code/datums/elements/food/processable.dm
+++ b/code/datums/elements/food/processable.dm
@@ -13,7 +13,7 @@
///Whether or not the atom being processed has to be on a table or tray to process it
var/table_required
-/datum/element/processable/Attach(datum/target, tool_behaviour, result_atom_type, amount_created = 3, time_to_process = 20, table_required = FALSE)
+/datum/element/processable/Attach(datum/target, tool_behaviour, result_atom_type, amount_created = 3, time_to_process = 2 SECONDS, table_required = FALSE)
. = ..()
if(!isatom(target))
return ELEMENT_INCOMPATIBLE
diff --git a/code/datums/elements/footstep.dm b/code/datums/elements/footstep.dm
index a529025c967e6..b9a1ef5df204f 100644
--- a/code/datums/elements/footstep.dm
+++ b/code/datums/elements/footstep.dm
@@ -87,7 +87,7 @@
if(steps % 2)
return
- if(steps != 0 && !source.has_gravity(turf)) // don't need to step as often when you hop around
+ if(steps != 0 && !source.has_gravity()) // don't need to step as often when you hop around
return
return turf
@@ -117,10 +117,10 @@
return
playsound(source_loc, pick(footstep_sounds[turf_footstep][1]), footstep_sounds[turf_footstep][2] * volume, TRUE, footstep_sounds[turf_footstep][3] + e_range, falloff_distance = 1, vary = sound_vary)
-/datum/element/footstep/proc/play_humanstep(mob/living/carbon/human/source, atom/oldloc, direction)
+/datum/element/footstep/proc/play_humanstep(mob/living/carbon/human/source, atom/oldloc, direction, forced, list/old_locs, momentum_change)
SIGNAL_HANDLER
- if (SHOULD_DISABLE_FOOTSTEPS(source))
+ if (SHOULD_DISABLE_FOOTSTEPS(source) || !momentum_change)
return
var/volume_multiplier = 1
@@ -134,21 +134,31 @@
if(!source_loc)
return
- play_fov_effect(source, 5, "footstep", direction, ignore_self = TRUE)
+ //cache for sanic speed (lists are references anyways)
+ var/static/list/footstep_sounds = GLOB.footstep
+ ///list returned by playsound() filled by client mobs who heard the footstep. given to play_fov_effect()
+ var/list/heard_clients
+
if ((source.wear_suit?.body_parts_covered | source.w_uniform?.body_parts_covered | source.shoes?.body_parts_covered) & FEET)
// we are wearing shoes
- playsound(source_loc, pick(GLOB.footstep[source_loc.footstep][1]),
- GLOB.footstep[source_loc.footstep][2] * volume * volume_multiplier,
+
+ heard_clients = playsound(source_loc, pick(footstep_sounds[source_loc.footstep][1]),
+ footstep_sounds[source_loc.footstep][2] * volume * volume_multiplier,
TRUE,
- GLOB.footstep[source_loc.footstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary)
+ footstep_sounds[source_loc.footstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary)
else
if(source.dna.species.special_step_sounds)
- playsound(source_loc, pick(source.dna.species.special_step_sounds), 50, TRUE, falloff_distance = 1, vary = sound_vary)
+ heard_clients = playsound(source_loc, pick(source.dna.species.special_step_sounds), 50, TRUE, falloff_distance = 1, vary = sound_vary)
else
- playsound(source_loc, pick(GLOB.barefootstep[source_loc.barefootstep][1]),
- GLOB.barefootstep[source_loc.barefootstep][2] * volume * volume_multiplier,
+ var/static/list/bare_footstep_sounds = GLOB.barefootstep
+
+ heard_clients = playsound(source_loc, pick(bare_footstep_sounds[source_loc.barefootstep][1]),
+ bare_footstep_sounds[source_loc.barefootstep][2] * volume * volume_multiplier,
TRUE,
- GLOB.barefootstep[source_loc.barefootstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary)
+ bare_footstep_sounds[source_loc.barefootstep][3] + e_range + range_adjustment, falloff_distance = 1, vary = sound_vary)
+
+ if(heard_clients)
+ play_fov_effect(source, 5, "footstep", direction, ignore_self = TRUE, override_list = heard_clients)
///Prepares a footstep for machine walking
diff --git a/code/datums/elements/forced_gravity.dm b/code/datums/elements/forced_gravity.dm
index ada16943b86a6..96889194f2d9f 100644
--- a/code/datums/elements/forced_gravity.dm
+++ b/code/datums/elements/forced_gravity.dm
@@ -1,16 +1,18 @@
/datum/element/forced_gravity
element_flags = ELEMENT_BESPOKE
id_arg_index = 2
+ ///the level of gravity we force unto our target
var/gravity
- var/ignore_space
+ ///whether we will override the turf if it forces no gravity
+ var/ignore_turf_gravity
-/datum/element/forced_gravity/Attach(datum/target, gravity=1, ignore_space=FALSE)
+/datum/element/forced_gravity/Attach(datum/target, gravity=1, ignore_turf_gravity = FALSE)
. = ..()
if(!isatom(target))
return ELEMENT_INCOMPATIBLE
src.gravity = gravity
- src.ignore_space = ignore_space
+ src.ignore_turf_gravity = ignore_turf_gravity
RegisterSignal(target, COMSIG_ATOM_HAS_GRAVITY, .proc/gravity_check)
if(isturf(target))
@@ -24,10 +26,12 @@
/datum/element/forced_gravity/proc/gravity_check(datum/source, turf/location, list/gravs)
SIGNAL_HANDLER
- if(!ignore_space && isspaceturf(location))
- return
+ if(!ignore_turf_gravity && location.force_no_gravity)
+ return FALSE
gravs += gravity
+ return TRUE
+
/datum/element/forced_gravity/proc/turf_gravity_check(datum/source, atom/checker, list/gravs)
SIGNAL_HANDLER
diff --git a/code/datums/elements/frozen.dm b/code/datums/elements/frozen.dm
new file mode 100644
index 0000000000000..c4393a960ebce
--- /dev/null
+++ b/code/datums/elements/frozen.dm
@@ -0,0 +1,52 @@
+///simple element to handle frozen obj's
+/datum/element/frozen
+ element_flags = ELEMENT_DETACH
+
+/datum/element/frozen/Attach(datum/target)
+ . = ..()
+ if(!isobj(target))
+ return ELEMENT_INCOMPATIBLE
+
+ var/obj/target_obj = target
+ if(target_obj.obj_flags & FREEZE_PROOF)
+ return ELEMENT_INCOMPATIBLE
+
+ if(HAS_TRAIT(target_obj, TRAIT_FROZEN))
+ return ELEMENT_INCOMPATIBLE
+
+ ADD_TRAIT(target_obj, TRAIT_FROZEN, ELEMENT_TRAIT(type))
+ target_obj.name = "frozen [target_obj.name]"
+ target_obj.add_atom_colour(GLOB.freon_color_matrix, TEMPORARY_COLOUR_PRIORITY)
+ target_obj.alpha -= 25
+
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, .proc/on_moved)
+ RegisterSignal(target, COMSIG_MOVABLE_POST_THROW, .proc/shatter_on_throw)
+ RegisterSignal(target, COMSIG_OBJ_UNFREEZE, .proc/on_unfrozen)
+
+/datum/element/frozen/Detach(datum/source, ...)
+ var/obj/obj_source = source
+ REMOVE_TRAIT(obj_source, TRAIT_FROZEN, ELEMENT_TRAIT(type))
+ obj_source.name = replacetext(obj_source.name, "frozen ", "")
+ obj_source.remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, GLOB.freon_color_matrix)
+ obj_source.alpha += 25
+ . = ..()
+
+/datum/element/frozen/proc/on_unfrozen(datum/source)
+ SIGNAL_HANDLER
+ Detach(source)
+
+/datum/element/frozen/proc/shatter_on_throw(datum/target)
+ SIGNAL_HANDLER
+ var/obj/obj_target = target
+ obj_target.visible_message(span_danger("[obj_target] shatters into a million pieces!"))
+ qdel(obj_target)
+
+/datum/element/frozen/proc/on_moved(datum/target)
+ SIGNAL_HANDLER
+ var/obj/obj_target = target
+ if(!isopenturf(obj_target.loc))
+ return
+
+ var/turf/open/turf_loc = obj_target.loc
+ if(turf_loc.air?.temperature >= T0C)//unfreezes target
+ Detach(target)
diff --git a/code/datums/elements/lazy_fishing_spot.dm b/code/datums/elements/lazy_fishing_spot.dm
new file mode 100644
index 0000000000000..603cd56e22fb4
--- /dev/null
+++ b/code/datums/elements/lazy_fishing_spot.dm
@@ -0,0 +1,25 @@
+// Lazy fishing spot element so fisheable turfs do not have a component each since they're usually pretty common on their respective maps (lava/water/etc)
+/datum/element/lazy_fishing_spot
+ element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
+ id_arg_index = 2
+ var/configuration
+
+/datum/element/lazy_fishing_spot/Attach(datum/target, configuration)
+ . = ..()
+ if(!isatom(target))
+ return ELEMENT_INCOMPATIBLE
+ if(!configuration)
+ CRASH("Lazy fishing spot had no configuration passed in.")
+ src.configuration = configuration
+
+ RegisterSignal(target, COMSIG_PRE_FISHING, .proc/create_fishing_spot)
+
+/datum/element/lazy_fishing_spot/Detach(datum/target)
+ UnregisterSignal(target, COMSIG_PRE_FISHING)
+ return ..()
+
+/datum/element/lazy_fishing_spot/proc/create_fishing_spot(datum/source)
+ SIGNAL_HANDLER
+
+ source.AddComponent(/datum/component/fishing_spot, configuration)
+ Detach(source)
diff --git a/code/datums/elements/light_eater.dm b/code/datums/elements/light_eater.dm
index 026258122a7bd..0eb59bbc850f2 100644
--- a/code/datums/elements/light_eater.dm
+++ b/code/datums/elements/light_eater.dm
@@ -67,8 +67,7 @@
/datum/element/light_eater/proc/table_buffet(atom/commisary, datum/devourer)
. = list()
SEND_SIGNAL(commisary, COMSIG_LIGHT_EATER_QUEUE, ., devourer)
- for(var/nom in commisary.light_sources)
- var/datum/light_source/morsel = nom
+ for(var/datum/light_source/morsel as anything in commisary.light_sources)
.[morsel.source_atom] = TRUE
/**
diff --git a/code/datums/elements/obj_regen.dm b/code/datums/elements/obj_regen.dm
index ef55457c28c6a..4830d420043ec 100644
--- a/code/datums/elements/obj_regen.dm
+++ b/code/datums/elements/obj_regen.dm
@@ -28,6 +28,7 @@
processing |= target
/datum/element/obj_regen/Detach(obj/target)
+ . = ..()
UnregisterSignal(target, COMSIG_ATOM_TAKE_DAMAGE)
processing -= target
if(!length(processing))
diff --git a/code/datums/elements/plant_backfire.dm b/code/datums/elements/plant_backfire.dm
index f753947e9ea34..b9a2000ffe487 100644
--- a/code/datums/elements/plant_backfire.dm
+++ b/code/datums/elements/plant_backfire.dm
@@ -3,7 +3,7 @@
/// If a user is protected with something like leather gloves, they can handle them normally.
/// If they're not protected properly, we invoke a callback on the user, harming or inconveniencing them.
/datum/element/plant_backfire
- element_flags = ELEMENT_BESPOKE | ELEMENT_DETACH
+ element_flags = ELEMENT_BESPOKE
id_arg_index = 2
/// Whether we stop the current action if backfire is triggered (EX: returning CANCEL_ATTACK_CHAIN)
var/cancel_action = FALSE
@@ -29,52 +29,67 @@
. = ..()
UnregisterSignal(target, list(COMSIG_ITEM_PRE_ATTACK, COMSIG_ITEM_PICKUP, COMSIG_MOVABLE_PRE_THROW))
-/*
+/**
* Checks before we attack if we're okay to continue.
*
* source - our plant
* user - the mob wielding our [source]
*/
-/datum/element/plant_backfire/proc/attack_safety_check(datum/source, atom/target, mob/user)
+/datum/element/plant_backfire/proc/attack_safety_check(obj/item/source, atom/target, mob/user)
SIGNAL_HANDLER
- if(plant_safety_check(source, user))
+ // Covers stuff like tk, since we aren't actually touching the plant.
+ if(!user.is_holding(source))
return
-
- SEND_SIGNAL(source, COMSIG_PLANT_ON_BACKFIRE, user)
- if(cancel_action)
- return COMPONENT_CANCEL_ATTACK_CHAIN
+ if(!backfire(source, user))
+ return
+
+ return cancel_action ? COMPONENT_CANCEL_ATTACK_CHAIN : NONE
-/*
+/**
* Checks before we pick up the plant if we're okay to continue.
*
* source - our plant
* user - the mob picking our [source]
*/
-/datum/element/plant_backfire/proc/pickup_safety_check(datum/source, mob/user)
+/datum/element/plant_backfire/proc/pickup_safety_check(obj/item/source, mob/user)
SIGNAL_HANDLER
- if(plant_safety_check(source, user))
- return
- SEND_SIGNAL(source, COMSIG_PLANT_ON_BACKFIRE, user)
+ backfire(source, user)
-/*
+/**
* Checks before we throw the plant if we're okay to continue.
*
* source - our plant
* thrower - the mob throwing our [source]
*/
-/datum/element/plant_backfire/proc/throw_safety_check(datum/source, list/arguments)
+/datum/element/plant_backfire/proc/throw_safety_check(obj/item/source, list/arguments)
SIGNAL_HANDLER
- var/mob/living/thrower = arguments[4] // 4th arg = mob/thrower
- if(plant_safety_check(source, thrower))
+ var/mob/living/thrower = arguments[4] // the 4th arg = the mob throwing our item
+ if(!thrower.is_holding(source))
return
- SEND_SIGNAL(source, COMSIG_PLANT_ON_BACKFIRE, thrower)
- if(cancel_action)
- return COMPONENT_CANCEL_THROW
+ if(!backfire(source, thrower))
+ return
+
+ return cancel_action ? COMPONENT_CANCEL_ATTACK_CHAIN : NONE
-/*
+/**
+ * The actual backfire occurs here.
+ * Checks if the user is able to safely handle the plant.
+ * If not, sends the backfire signal (meaning backfire will occur and be handled by one or multiple genes).
+ *
+ * Returns FALSE if the user was safe and no backfire occured.
+ * Returns TRUE if the user was not safe and a backfire actually happened.
+ */
+/datum/element/plant_backfire/proc/backfire(obj/item/plant, mob/user)
+ if(plant_safety_check(plant, user))
+ return FALSE
+
+ SEND_SIGNAL(plant, COMSIG_PLANT_ON_BACKFIRE, user)
+ return TRUE
+
+/**
* Actually checks if our user is safely handling our plant.
*
* Checks for TRAIT_PLANT_SAFE, and returns TRUE if we have it.
@@ -86,13 +101,10 @@
*
* returns FALSE if none of the checks are successful.
*/
-/datum/element/plant_backfire/proc/plant_safety_check(datum/source, mob/living/carbon/user)
+/datum/element/plant_backfire/proc/plant_safety_check(obj/item/plant, mob/living/carbon/user)
if(!istype(user))
return TRUE
- if(istype(source, /obj/item/tk_grab)) // since we aren't actually touching the plant
- return TRUE
-
if(HAS_TRAIT(user, TRAIT_PLANT_SAFE))
return TRUE
@@ -100,8 +112,7 @@
if(HAS_TRAIT(user, checked_trait))
return TRUE
- var/obj/item/parent_item = source
- var/obj/item/seeds/our_seed = parent_item.get_plant_seed()
+ var/obj/item/seeds/our_seed = plant.get_plant_seed()
if(our_seed)
for(var/checked_gene in extra_genes)
if(!our_seed.get_gene(checked_gene))
diff --git a/code/datums/elements/ridable.dm b/code/datums/elements/ridable.dm
index 240333125be42..cd4d4714c1bd5 100644
--- a/code/datums/elements/ridable.dm
+++ b/code/datums/elements/ridable.dm
@@ -7,7 +7,7 @@
* just having the variables, behavior, and procs be standardized is still a big improvement.
*/
/datum/element/ridable
- element_flags = ELEMENT_BESPOKE
+ element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH
id_arg_index = 2
/// The specific riding component subtype we're loading our instructions from, don't leave this as default please!
@@ -24,6 +24,7 @@
stack_trace("Tried attaching a ridable element to [target] with basic/abstract /datum/component/riding component type. Please designate a specific riding component subtype when adding the ridable element.")
return COMPONENT_INCOMPATIBLE
+ target.can_buckle = TRUE
riding_component_type = component_type
potion_boosted = potion_boost
@@ -31,10 +32,11 @@
if(isvehicle(target))
RegisterSignal(target, COMSIG_SPEED_POTION_APPLIED, .proc/check_potion)
if(ismob(target))
- RegisterSignal(target, COMSIG_LIVING_DEATH, .proc/handle_removal)
+ RegisterSignal(target, COMSIG_MOB_STATCHANGE, .proc/on_stat_change)
-/datum/element/ridable/Detach(datum/target)
- UnregisterSignal(target, list(COMSIG_MOVABLE_PREBUCKLE, COMSIG_SPEED_POTION_APPLIED, COMSIG_LIVING_DEATH))
+/datum/element/ridable/Detach(atom/movable/target)
+ target.can_buckle = initial(target.can_buckle)
+ UnregisterSignal(target, list(COMSIG_MOVABLE_PREBUCKLE, COMSIG_SPEED_POTION_APPLIED, COMSIG_MOB_STATCHANGE))
return ..()
/// Someone is buckling to this movable, which is literally the only thing we care about (other than speed potions)
@@ -147,13 +149,17 @@
qdel(O)
return TRUE
-/datum/element/ridable/proc/handle_removal(datum/source)
+/datum/element/ridable/proc/on_stat_change(mob/source)
SIGNAL_HANDLER
- var/atom/movable/ridden = source
- ridden.unbuckle_all_mobs()
+ // If we're dead, don't let anyone buckle onto us
+ if(source.stat == DEAD)
+ source.can_buckle = FALSE
+ source.unbuckle_all_mobs()
- Detach(source)
+ // If we're alive, back to being buckle-able
+ else
+ source.can_buckle = TRUE
/obj/item/riding_offhand
name = "offhand"
diff --git a/code/datums/elements/venomous.dm b/code/datums/elements/venomous.dm
index f513e03bd6c98..0062c78d568c5 100644
--- a/code/datums/elements/venomous.dm
+++ b/code/datums/elements/venomous.dm
@@ -1,7 +1,7 @@
/**
* Venomous element; which makes the attacks of the simplemob attached poison the enemy.
*
- * Used for spiders and bees!
+ * Used for spiders, frogs, and bees!
*/
/datum/element/venomous
element_flags = ELEMENT_BESPOKE|ELEMENT_DETACH
diff --git a/code/datums/elements/weapon_description.dm b/code/datums/elements/weapon_description.dm
index 32f0e17282feb..c2a0249133613 100644
--- a/code/datums/elements/weapon_description.dm
+++ b/code/datums/elements/weapon_description.dm
@@ -62,7 +62,7 @@
SIGNAL_HANDLER
if(href_list["examine"])
- to_chat(user, span_notice("[build_label_text(source)]"))
+ to_chat(user, span_notice(examine_block("[build_label_text(source)]")))
/**
*
diff --git a/code/datums/id_trim/jobs.dm b/code/datums/id_trim/jobs.dm
index 7a21a8d0bb3f2..21c4916c3c27e 100644
--- a/code/datums/id_trim/jobs.dm
+++ b/code/datums/id_trim/jobs.dm
@@ -80,9 +80,15 @@
assignment = "Assistant"
trim_state = "trim_assistant"
sechud_icon_state = SECHUD_ASSISTANT
- extra_access = list(ACCESS_MAINT_TUNNELS)
minimal_access = list()
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ extra_access = list(
+ ACCESS_MAINT_TUNNELS,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ )
job = /datum/job/assistant
/datum/id_trim/job/assistant/refresh_trim_access()
@@ -93,34 +99,77 @@
// Config has assistant maint access set.
if(CONFIG_GET(flag/assistants_have_maint_access))
- access |= list(ACCESS_MAINT_TUNNELS)
+ access |= list(
+ ACCESS_MAINT_TUNNELS)
/datum/id_trim/job/atmospheric_technician
assignment = "Atmospheric Technician"
trim_state = "trim_atmospherictechnician"
sechud_icon_state = SECHUD_ATMOSPHERIC_TECHNICIAN
- extra_access = list(ACCESS_ENGINE_EQUIP, ACCESS_MINERAL_STOREROOM, ACCESS_TECH_STORAGE)
- minimal_access = list(ACCESS_ENGINEERING, ACCESS_ATMOSPHERICS, ACCESS_AUX_BASE, ACCESS_CONSTRUCTION, ACCESS_EXTERNAL_AIRLOCKS, ACCESS_MAINT_TUNNELS, ACCESS_MECH_ENGINE,
- ACCESS_MINERAL_STOREROOM)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CE, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_ATMOSPHERICS,
+ ACCESS_AUX_BASE,
+ ACCESS_CONSTRUCTION,
+ ACCESS_ENGINEERING,
+ ACCESS_EXTERNAL_AIRLOCKS,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_ENGINE,
+ ACCESS_MINERAL_STOREROOM,
+ )
+ extra_access = list(
+ ACCESS_ENGINE_EQUIP,
+ ACCESS_MINISAT,
+ ACCESS_TCOMMS,
+ ACCESS_TECH_STORAGE,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ )
job = /datum/job/atmospheric_technician
/datum/id_trim/job/bartender
assignment = "Bartender"
trim_state = "trim_bartender"
sechud_icon_state = SECHUD_BARTENDER
- extra_access = list(ACCESS_HYDROPONICS, ACCESS_KITCHEN, ACCESS_MORGUE)
- minimal_access = list(ACCESS_BAR, ACCESS_MINERAL_STOREROOM, ACCESS_THEATRE, ACCESS_WEAPONS, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_BAR,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_SERVICE,
+ ACCESS_THEATRE,
+ ACCESS_WEAPONS,
+ )
+ extra_access = list(
+ ACCESS_HYDROPONICS,
+ ACCESS_KITCHEN,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ )
job = /datum/job/bartender
/datum/id_trim/job/botanist
assignment = "Botanist"
trim_state = "trim_botanist"
sechud_icon_state = SECHUD_BOTANIST
- extra_access = list(ACCESS_BAR, ACCESS_KITCHEN)
- minimal_access = list(ACCESS_HYDROPONICS, ACCESS_MINERAL_STOREROOM, ACCESS_MORGUE, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_HYDROPONICS,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_SERVICE,
+ )
+ extra_access = list(
+ ACCESS_BAR,
+ ACCESS_KITCHEN,
+ ACCESS_MORGUE,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ )
job = /datum/job/botanist
/datum/id_trim/job/captain
@@ -128,7 +177,10 @@
intern_alt_name = "Captain-in-Training"
trim_state = "trim_captain"
sechud_icon_state = SECHUD_CAPTAIN
- template_access = list(ACCESS_CAPTAIN, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ )
job = /datum/job/captain
/// Captain gets all station accesses hardcoded in because it's the Captain.
@@ -144,27 +196,64 @@
assignment = "Cargo Technician"
trim_state = "trim_cargotechnician"
sechud_icon_state = SECHUD_CARGO_TECHNICIAN
- extra_access = list(ACCESS_QM, ACCESS_MINING, ACCESS_MINING_STATION)
- minimal_access = list(ACCESS_CARGO, ACCESS_MAIL_SORTING, ACCESS_MAINT_TUNNELS, ACCESS_MECH_MINING, ACCESS_MINERAL_STOREROOM)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_CARGO,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_MINING,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_SHIPPING,
+ )
+ extra_access = list(
+ ACCESS_MINING,
+ ACCESS_MINING_STATION,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_QM,
+ )
job = /datum/job/cargo_technician
/datum/id_trim/job/chaplain
assignment = "Chaplain"
trim_state = "trim_chaplain"
sechud_icon_state = SECHUD_CHAPLAIN
+ minimal_access = list(
+ ACCESS_CHAPEL_OFFICE,
+ ACCESS_CREMATORIUM,
+ ACCESS_MORGUE,
+ ACCESS_SERVICE,
+ ACCESS_THEATRE,
+ )
extra_access = list()
- minimal_access = list(ACCESS_CHAPEL_OFFICE, ACCESS_CREMATORIUM, ACCESS_MORGUE, ACCESS_THEATRE, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ )
job = /datum/job/chaplain
/datum/id_trim/job/chemist
assignment = "Chemist"
trim_state = "trim_chemist"
sechud_icon_state = SECHUD_CHEMIST
- extra_access = list(ACCESS_SURGERY, ACCESS_VIROLOGY)
- minimal_access = list(ACCESS_PLUMBING, ACCESS_MECH_MEDICAL, ACCESS_MEDICAL, ACCESS_MINERAL_STOREROOM, ACCESS_MORGUE, ACCESS_PHARMACY)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CMO, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_MECH_MEDICAL,
+ ACCESS_MEDICAL,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_PHARMACY,
+ ACCESS_PLUMBING,
+ )
+ extra_access = list(
+ ACCESS_MORGUE,
+ ACCESS_SURGERY,
+ ACCESS_VIROLOGY,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_CMO,
+ )
job = /datum/job/chemist
/datum/id_trim/job/chief_engineer
@@ -172,13 +261,37 @@
intern_alt_name = "Chief Engineer-in-Training"
trim_state = "trim_chiefengineer"
sechud_icon_state = SECHUD_CHIEF_ENGINEER
- extra_access = list(ACCESS_TELEPORTER)
extra_wildcard_access = list()
- minimal_access = list(ACCESS_ATMOSPHERICS, ACCESS_AUX_BASE, ACCESS_CE, ACCESS_CONSTRUCTION, ACCESS_ENGINEERING, ACCESS_ENGINE_EQUIP, ACCESS_EVA,
- ACCESS_EXTERNAL_AIRLOCKS, ACCESS_COMMAND, ACCESS_KEYCARD_AUTH, ACCESS_MAINT_TUNNELS, ACCESS_MECH_ENGINE,
- ACCESS_MINERAL_STOREROOM, ACCESS_MINISAT, ACCESS_RC_ANNOUNCE, ACCESS_BRIG_ENTRANCE, ACCESS_TCOMMS, ACCESS_TECH_STORAGE)
- minimal_wildcard_access = list(ACCESS_CE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_ATMOSPHERICS,
+ ACCESS_AUX_BASE,
+ ACCESS_BRIG_ENTRANCE,
+ ACCESS_CE,
+ ACCESS_COMMAND,
+ ACCESS_CONSTRUCTION,
+ ACCESS_ENGINEERING,
+ ACCESS_ENGINE_EQUIP,
+ ACCESS_EVA,
+ ACCESS_EXTERNAL_AIRLOCKS,
+ ACCESS_KEYCARD_AUTH,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_ENGINE,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MINISAT,
+ ACCESS_RC_ANNOUNCE,
+ ACCESS_TCOMMS,
+ ACCESS_TECH_STORAGE,
+ )
+ minimal_wildcard_access = list(
+ ACCESS_CE,
+ )
+ extra_access = list(
+ ACCESS_TELEPORTER,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ )
job = /datum/job/chief_engineer
/datum/id_trim/job/chief_medical_officer
@@ -186,31 +299,71 @@
intern_alt_name = "Chief Medical Officer-in-Training"
trim_state = "trim_chiefmedicalofficer"
sechud_icon_state = SECHUD_CHIEF_MEDICAL_OFFICER
- extra_access = list(ACCESS_TELEPORTER)
extra_wildcard_access = list()
- minimal_access = list(ACCESS_PLUMBING, ACCESS_EVA, ACCESS_COMMAND, ACCESS_KEYCARD_AUTH, ACCESS_MAINT_TUNNELS, ACCESS_MECH_MEDICAL,
- ACCESS_MEDICAL, ACCESS_MINERAL_STOREROOM, ACCESS_MORGUE, ACCESS_PHARMACY, ACCESS_PSYCHOLOGY, ACCESS_RC_ANNOUNCE,
- ACCESS_BRIG_ENTRANCE, ACCESS_SURGERY, ACCESS_VIROLOGY)
- minimal_wildcard_access = list(ACCESS_CMO)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_BRIG_ENTRANCE,
+ ACCESS_COMMAND,
+ ACCESS_KEYCARD_AUTH,
+ ACCESS_PLUMBING,
+ ACCESS_EVA,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_MEDICAL,
+ ACCESS_MEDICAL,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MORGUE,
+ ACCESS_PHARMACY,
+ ACCESS_PSYCHOLOGY,
+ ACCESS_RC_ANNOUNCE,
+ ACCESS_SURGERY,
+ ACCESS_VIROLOGY,
+ )
+ minimal_wildcard_access = list(
+ ACCESS_CMO,
+ )
+ extra_access = list(
+ ACCESS_TELEPORTER,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ )
job = /datum/job/chief_medical_officer
/datum/id_trim/job/clown
assignment = "Clown"
trim_state = "trim_clown"
sechud_icon_state = SECHUD_CLOWN
+ minimal_access = list(
+ ACCESS_SERVICE,
+ ACCESS_THEATRE,
+ )
extra_access = list()
- minimal_access = list(ACCESS_THEATRE, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ )
job = /datum/job/clown
/datum/id_trim/job/cook
assignment = "Cook"
trim_state = "trim_cook"
sechud_icon_state = SECHUD_COOK
- extra_access = list(ACCESS_BAR, ACCESS_HYDROPONICS)
- minimal_access = list(ACCESS_KITCHEN, ACCESS_MINERAL_STOREROOM, ACCESS_MORGUE, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_KITCHEN,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MORGUE,
+ ACCESS_SERVICE,
+ )
+ extra_access = list(
+ ACCESS_BAR,
+ ACCESS_HYDROPONICS,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ )
job = /datum/job/cook
/datum/id_trim/job/cook/chef
@@ -221,19 +374,43 @@
assignment = "Curator"
trim_state = "trim_curator"
sechud_icon_state = SECHUD_CURATOR
+ minimal_access = list(
+ ACCESS_AUX_BASE,
+ ACCESS_LIBRARY,
+ ACCESS_MINING_STATION,
+ ACCESS_SERVICE,
+ )
extra_access = list()
- minimal_access = list(ACCESS_AUX_BASE, ACCESS_LIBRARY, ACCESS_MINING_STATION, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ )
job = /datum/job/curator
/datum/id_trim/job/detective
assignment = "Detective"
trim_state = "trim_detective"
sechud_icon_state = SECHUD_DETECTIVE
- extra_access = list(ACCESS_BRIG)
- minimal_access = list(ACCESS_SECURITY, ACCESS_COURT, ACCESS_DETECTIVE, ACCESS_BRIG_ENTRANCE,ACCESS_MAINT_TUNNELS, ACCESS_MORGUE,
- ACCESS_MECH_SECURITY, ACCESS_MINERAL_STOREROOM, ACCESS_WEAPONS)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOS, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_BRIG_ENTRANCE,
+ ACCESS_COURT,
+ ACCESS_DETECTIVE,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_SECURITY,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MORGUE,
+ ACCESS_SECURITY,
+ ACCESS_WEAPONS,
+ )
+ extra_access = list(
+ ACCESS_BRIG,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOS,
+ )
job = /datum/job/detective
/datum/id_trim/job/detective/refresh_trim_access()
@@ -250,9 +427,24 @@
assignment = "Geneticist"
trim_state = "trim_geneticist"
sechud_icon_state = SECHUD_GENETICIST
- extra_access = list(ACCESS_ROBOTICS, ACCESS_TECH_STORAGE, ACCESS_XENOBIOLOGY)
- minimal_access = list(ACCESS_GENETICS, ACCESS_MECH_SCIENCE, ACCESS_MINERAL_STOREROOM, ACCESS_MORGUE, ACCESS_RESEARCH, ACCESS_SCIENCE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_RD, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_GENETICS,
+ ACCESS_MECH_SCIENCE,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MORGUE,
+ ACCESS_RESEARCH,
+ ACCESS_SCIENCE,
+ )
+ extra_access = list(
+ ACCESS_ROBOTICS,
+ ACCESS_TECH_STORAGE,
+ ACCESS_XENOBIOLOGY,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_RD,
+ )
job = /datum/job/geneticist
/datum/id_trim/job/head_of_personnel
@@ -260,16 +452,46 @@
intern_alt_name = "Head of Personnel-in-Training"
trim_state = "trim_headofpersonnel"
sechud_icon_state = SECHUD_HEAD_OF_PERSONNEL
+ minimal_access = list(
+ ACCESS_AI_UPLOAD,
+ ACCESS_ALL_PERSONAL_LOCKERS,
+ ACCESS_AUX_BASE,
+ ACCESS_BAR,
+ ACCESS_BRIG_ENTRANCE,
+ ACCESS_CHAPEL_OFFICE,
+ ACCESS_CHANGE_IDS,
+ ACCESS_CREMATORIUM,
+ ACCESS_COMMAND,
+ ACCESS_COURT,
+ ACCESS_ENGINEERING,
+ ACCESS_EVA,
+ ACCESS_GATEWAY,
+ ACCESS_HYDROPONICS,
+ ACCESS_JANITOR,
+ ACCESS_KEYCARD_AUTH,
+ ACCESS_KITCHEN,
+ ACCESS_LAWYER,
+ ACCESS_LIBRARY,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MEDICAL,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_PSYCHOLOGY,
+ ACCESS_RC_ANNOUNCE,
+ ACCESS_SCIENCE,
+ ACCESS_SERVICE,
+ ACCESS_TELEPORTER,
+ ACCESS_THEATRE,
+ ACCESS_WEAPONS,
+ )
+ minimal_wildcard_access = list(
+ ACCESS_HOP,
+ )
extra_access = list()
extra_wildcard_access = list()
- minimal_access = list(ACCESS_AI_UPLOAD, ACCESS_ALL_PERSONAL_LOCKERS, ACCESS_AUX_BASE, ACCESS_BAR, ACCESS_CARGO, ACCESS_CHAPEL_OFFICE,
- ACCESS_CHANGE_IDS, ACCESS_CONSTRUCTION, ACCESS_COURT, ACCESS_CREMATORIUM, ACCESS_ENGINEERING, ACCESS_EVA, ACCESS_GATEWAY,
- ACCESS_COMMAND, ACCESS_HYDROPONICS, ACCESS_JANITOR, ACCESS_KEYCARD_AUTH, ACCESS_KITCHEN, ACCESS_LAWYER, ACCESS_LIBRARY,
- ACCESS_MAIL_SORTING, ACCESS_MAINT_TUNNELS, ACCESS_MECH_MINING, ACCESS_MEDICAL, ACCESS_MINERAL_STOREROOM,
- ACCESS_MINING, ACCESS_MINING_STATION, ACCESS_MORGUE, ACCESS_PSYCHOLOGY, ACCESS_QM, ACCESS_RC_ANNOUNCE,
- ACCESS_RESEARCH, ACCESS_BRIG_ENTRANCE, ACCESS_TELEPORTER, ACCESS_THEATRE, ACCESS_VAULT, ACCESS_WEAPONS, ACCESS_SERVICE)
- minimal_wildcard_access = list(ACCESS_HOP)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ )
job = /datum/job/head_of_personnel
/datum/id_trim/job/head_of_security
@@ -279,12 +501,41 @@
sechud_icon_state = SECHUD_HEAD_OF_SECURITY
extra_access = list(ACCESS_TELEPORTER)
extra_wildcard_access = list()
- minimal_access = list(ACCESS_ALL_PERSONAL_LOCKERS, ACCESS_ARMORY, ACCESS_AUX_BASE, ACCESS_BRIG, ACCESS_CONSTRUCTION, ACCESS_COURT,
- ACCESS_ENGINEERING, ACCESS_EVA, ACCESS_DETECTIVE, ACCESS_GATEWAY, ACCESS_COMMAND, ACCESS_KEYCARD_AUTH,
- ACCESS_MAIL_SORTING, ACCESS_MAINT_TUNNELS, ACCESS_MECH_SECURITY, ACCESS_MEDICAL, ACCESS_MINERAL_STOREROOM,
- ACCESS_MINING, ACCESS_MORGUE, ACCESS_RC_ANNOUNCE, ACCESS_RESEARCH, ACCESS_SECURITY, ACCESS_BRIG_ENTRANCE, ACCESS_WEAPONS)
- minimal_wildcard_access = list(ACCESS_HOS)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_ALL_PERSONAL_LOCKERS,
+ ACCESS_ARMORY,
+ ACCESS_AUX_BASE,
+ ACCESS_BRIG,
+ ACCESS_BRIG_ENTRANCE,
+ ACCESS_CARGO,
+ ACCESS_COMMAND,
+ ACCESS_CONSTRUCTION,
+ ACCESS_COURT,
+ ACCESS_DETECTIVE,
+ ACCESS_ENGINEERING,
+ ACCESS_EVA,
+ ACCESS_GATEWAY,
+ ACCESS_KEYCARD_AUTH,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_SECURITY,
+ ACCESS_MEDICAL,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MINING,
+ ACCESS_MORGUE,
+ ACCESS_RC_ANNOUNCE,
+ ACCESS_SCIENCE,
+ ACCESS_SECURITY,
+ ACCESS_SERVICE,
+ ACCESS_SHIPPING,
+ ACCESS_WEAPONS,
+ )
+ minimal_wildcard_access = list(
+ ACCESS_HOS,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ )
job = /datum/job/head_of_security
/datum/id_trim/job/head_of_security/refresh_trim_access()
@@ -301,53 +552,115 @@
assignment = "Janitor"
trim_state = "trim_janitor"
sechud_icon_state = SECHUD_JANITOR
+ minimal_access = list(
+ ACCESS_JANITOR,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_SERVICE,
+ )
extra_access = list()
- minimal_access = list(ACCESS_JANITOR, ACCESS_MAINT_TUNNELS, ACCESS_MINERAL_STOREROOM, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_HOP,
+ ACCESS_CHANGE_IDS,
+ )
job = /datum/job/janitor
/datum/id_trim/job/lawyer
assignment = "Lawyer"
trim_state = "trim_lawyer"
sechud_icon_state = SECHUD_LAWYER
+ minimal_access = list(
+ ACCESS_BRIG_ENTRANCE,
+ ACCESS_COURT,
+ ACCESS_LAWYER,
+ ACCESS_SERVICE,
+ )
extra_access = list()
- minimal_access = list(ACCESS_COURT, ACCESS_LAWYER, ACCESS_BRIG_ENTRANCE, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_HOS, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ ACCESS_HOS,
+ )
job = /datum/job/lawyer
/datum/id_trim/job/medical_doctor
assignment = "Medical Doctor"
trim_state = "trim_medicaldoctor"
sechud_icon_state = SECHUD_MEDICAL_DOCTOR
- extra_access = list(ACCESS_PLUMBING, ACCESS_VIROLOGY)
- minimal_access = list(ACCESS_MECH_MEDICAL, ACCESS_MEDICAL, ACCESS_MINERAL_STOREROOM, ACCESS_MORGUE, ACCESS_PHARMACY, ACCESS_SURGERY)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CMO, ACCESS_CHANGE_IDS)
+ extra_access = list(
+ ACCESS_PLUMBING,
+ ACCESS_VIROLOGY,
+ )
+ minimal_access = list(
+ ACCESS_MECH_MEDICAL,
+ ACCESS_MEDICAL,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MORGUE,
+ ACCESS_PHARMACY,
+ ACCESS_SURGERY,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_CMO,
+ )
job = /datum/job/doctor
/datum/id_trim/job/mime
assignment = "Mime"
trim_state = "trim_mime"
sechud_icon_state = SECHUD_MIME
+ minimal_access = list(
+ ACCESS_SERVICE,
+ ACCESS_THEATRE,
+ )
extra_access = list()
- minimal_access = list(ACCESS_THEATRE, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ )
job = /datum/job/mime
/datum/id_trim/job/paramedic
assignment = "Paramedic"
trim_state = "trim_paramedic"
sechud_icon_state = SECHUD_PARAMEDIC
- extra_access = list(ACCESS_SURGERY)
- minimal_access = list(ACCESS_CONSTRUCTION, ACCESS_HYDROPONICS, ACCESS_MAINT_TUNNELS, ACCESS_MECH_MEDICAL,
- ACCESS_MEDICAL, ACCESS_MINERAL_STOREROOM, ACCESS_MINING, ACCESS_MORGUE, ACCESS_RESEARCH)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CMO, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_CARGO,
+ ACCESS_CONSTRUCTION,
+ ACCESS_HYDROPONICS,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_MEDICAL,
+ ACCESS_MEDICAL,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MINING,
+ ACCESS_MORGUE,
+ ACCESS_SCIENCE,
+ ACCESS_SERVICE,
+ )
+ extra_access = list(
+ ACCESS_SURGERY,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_CMO,
+ )
job = /datum/job/paramedic
/datum/id_trim/job/prisoner
assignment = "Prisoner"
trim_state = "trim_prisoner"
sechud_icon_state = SECHUD_PRISONER
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_HOS, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOP,
+ ACCESS_HOS,
+ )
job = /datum/job/prisoner
/datum/id_trim/job/prisoner/one
@@ -382,19 +695,49 @@
assignment = "Psychologist"
trim_state = "trim_psychologist"
sechud_icon_state = SECHUD_PSYCHOLOGIST
+ minimal_access = list(
+ ACCESS_MEDICAL,
+ ACCESS_PSYCHOLOGY,
+ ACCESS_SERVICE,
+ )
extra_access = list()
- minimal_access = list(ACCESS_MEDICAL, ACCESS_PSYCHOLOGY, ACCESS_SERVICE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CMO, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_CMO,
+ ACCESS_HOP,
+ )
job = /datum/job/psychologist
/datum/id_trim/job/quartermaster
assignment = "Quartermaster"
trim_state = "trim_quartermaster"
sechud_icon_state = SECHUD_QUARTERMASTER
+ minimal_access = list(
+ ACCESS_AUX_BASE,
+ ACCESS_CARGO,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_MINING,
+ ACCESS_MINING_STATION,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MINING,
+ ACCESS_QM,
+ ACCESS_RC_ANNOUNCE,
+ ACCESS_SHIPPING,
+ ACCESS_VAULT,
+ ACCESS_KEYCARD_AUTH,
+ ACCESS_COMMAND,
+ ACCESS_EVA,
+ ACCESS_BRIG_ENTRANCE,
+ )
extra_access = list()
- minimal_access = list(ACCESS_AUX_BASE, ACCESS_CARGO, ACCESS_MAIL_SORTING, ACCESS_MAINT_TUNNELS, ACCESS_MECH_MINING, ACCESS_MINING_STATION,
- ACCESS_MINERAL_STOREROOM, ACCESS_MINING, ACCESS_QM, ACCESS_RC_ANNOUNCE, ACCESS_VAULT)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ minimal_wildcard_access = list(
+ ACCESS_QM,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ )
job = /datum/job/quartermaster
/datum/id_trim/job/research_director
@@ -402,35 +745,94 @@
intern_alt_name = "Research Director-in-Training"
trim_state = "trim_researchdirector"
sechud_icon_state = SECHUD_RESEARCH_DIRECTOR
+ minimal_access = list(
+ ACCESS_AI_UPLOAD,
+ ACCESS_AUX_BASE,
+ ACCESS_BRIG_ENTRANCE,
+ ACCESS_COMMAND,
+ ACCESS_CONSTRUCTION,
+ ACCESS_EVA,
+ ACCESS_GATEWAY,
+ ACCESS_GENETICS,
+ ACCESS_KEYCARD_AUTH,
+ ACCESS_NETWORK,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_ENGINE,
+ ACCESS_MECH_MINING,
+ ACCESS_MECH_SECURITY,
+ ACCESS_MECH_SCIENCE,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MINISAT,
+ ACCESS_MORGUE,
+ ACCESS_ORDNANCE,
+ ACCESS_ORDNANCE_STORAGE,
+ ACCESS_RC_ANNOUNCE,
+ ACCESS_RESEARCH,
+ ACCESS_ROBOTICS,
+ ACCESS_SCIENCE,
+ ACCESS_TECH_STORAGE,
+ ACCESS_TELEPORTER,
+ ACCESS_XENOBIOLOGY,
+ )
+ minimal_wildcard_access = list(
+ ACCESS_RD,
+ )
extra_access = list()
extra_wildcard_access = list()
- minimal_access = list(ACCESS_AI_UPLOAD, ACCESS_AUX_BASE, ACCESS_EVA, ACCESS_GATEWAY, ACCESS_GENETICS, ACCESS_COMMAND, ACCESS_KEYCARD_AUTH,
- ACCESS_NETWORK, ACCESS_MAINT_TUNNELS, ACCESS_MECH_ENGINE, ACCESS_MECH_MINING, ACCESS_MECH_SECURITY, ACCESS_MECH_SCIENCE,
- ACCESS_MEDICAL, ACCESS_MINERAL_STOREROOM, ACCESS_MINING, ACCESS_MINING_STATION, ACCESS_MINISAT, ACCESS_MORGUE,
- ACCESS_ORDNANCE, ACCESS_ORDNANCE_STORAGE, ACCESS_RC_ANNOUNCE, ACCESS_RESEARCH, ACCESS_SCIENCE, ACCESS_ROBOTICS,
- ACCESS_BRIG_ENTRANCE, ACCESS_TECH_STORAGE, ACCESS_TELEPORTER, ACCESS_XENOBIOLOGY)
- minimal_wildcard_access = list(ACCESS_RD)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CHANGE_IDS)
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ )
job = /datum/job/research_director
/datum/id_trim/job/roboticist
assignment = "Roboticist"
trim_state = "trim_roboticist"
sechud_icon_state = SECHUD_ROBOTICIST
- extra_access = list(ACCESS_GENETICS, ACCESS_ORDNANCE, ACCESS_ORDNANCE_STORAGE, ACCESS_XENOBIOLOGY)
- minimal_access = list(ACCESS_AUX_BASE, ACCESS_MECH_SCIENCE, ACCESS_MINERAL_STOREROOM, ACCESS_MORGUE, ACCESS_RESEARCH, ACCESS_SCIENCE,
- ACCESS_ROBOTICS, ACCESS_TECH_STORAGE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_RD, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_AUX_BASE,
+ ACCESS_MECH_SCIENCE,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MORGUE,
+ ACCESS_RESEARCH,
+ ACCESS_ROBOTICS,
+ ACCESS_SCIENCE,
+ ACCESS_TECH_STORAGE,
+ )
+ extra_access = list(
+ ACCESS_GENETICS,
+ ACCESS_XENOBIOLOGY,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_RD,
+ )
job = /datum/job/roboticist
/datum/id_trim/job/scientist
assignment = "Scientist"
trim_state = "trim_scientist"
sechud_icon_state = SECHUD_SCIENTIST
- extra_access = list(ACCESS_GENETICS, ACCESS_ROBOTICS)
- minimal_access = list(ACCESS_AUX_BASE, ACCESS_MECH_SCIENCE, ACCESS_MINERAL_STOREROOM, ACCESS_ORDNANCE, ACCESS_ORDNANCE_STORAGE,
- ACCESS_RESEARCH, ACCESS_SCIENCE, ACCESS_XENOBIOLOGY)
- template_access = list(ACCESS_CAPTAIN, ACCESS_RD, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_AUX_BASE,
+ ACCESS_MECH_SCIENCE,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_ORDNANCE,
+ ACCESS_ORDNANCE_STORAGE,
+ ACCESS_RESEARCH,
+ ACCESS_SCIENCE,
+ ACCESS_XENOBIOLOGY,
+ )
+ extra_access = list(
+ ACCESS_GENETICS,
+ ACCESS_ROBOTICS,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_RD,
+ )
job = /datum/job/scientist
/// Sec officers have departmental variants. They each have their own trims with bonus departmental accesses.
@@ -438,10 +840,25 @@
assignment = "Security Officer"
trim_state = "trim_securityofficer"
sechud_icon_state = SECHUD_SECURITY_OFFICER
- extra_access = list(ACCESS_DETECTIVE, ACCESS_MAINT_TUNNELS, ACCESS_MORGUE)
- minimal_access = list(ACCESS_BRIG, ACCESS_COURT, ACCESS_SECURITY, ACCESS_BRIG_ENTRANCE, ACCESS_MECH_SECURITY,
- ACCESS_MINERAL_STOREROOM, ACCESS_WEAPONS)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOS, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_BRIG,
+ ACCESS_BRIG_ENTRANCE,
+ ACCESS_COURT,
+ ACCESS_MECH_SECURITY,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_SECURITY,
+ ACCESS_WEAPONS,
+ )
+ extra_access = list(
+ ACCESS_DETECTIVE,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MORGUE,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOS,
+ )
job = /datum/job/security_officer
/// List of bonus departmental accesses that departmental sec officers get.
var/department_access = list()
@@ -461,66 +878,159 @@
/datum/id_trim/job/security_officer/supply
assignment = "Security Officer (Cargo)"
trim_state = "trim_securityofficer_car"
- department_access = list(ACCESS_AUX_BASE, ACCESS_CARGO, ACCESS_MAIL_SORTING, ACCESS_MINING, ACCESS_MINING_STATION)
+ department_access = list(
+ ACCESS_AUX_BASE,
+ ACCESS_CARGO,
+ ACCESS_MINING,
+ ACCESS_MINING_STATION,
+ ACCESS_SHIPPING,
+ )
/datum/id_trim/job/security_officer/engineering
assignment = "Security Officer (Engineering)"
trim_state = "trim_securityofficer_engi"
- department_access = list(ACCESS_ATMOSPHERICS, ACCESS_AUX_BASE, ACCESS_CONSTRUCTION, ACCESS_ENGINEERING)
+ department_access = list(
+ ACCESS_ATMOSPHERICS,
+ ACCESS_AUX_BASE,
+ ACCESS_CONSTRUCTION,
+ ACCESS_ENGINEERING,
+ ACCESS_ENGINE_EQUIP,
+ ACCESS_TCOMMS,
+ )
/datum/id_trim/job/security_officer/medical
assignment = "Security Officer (Medical)"
trim_state = "trim_securityofficer_med"
- department_access = list(ACCESS_MEDICAL, ACCESS_MORGUE, ACCESS_SURGERY)
+ department_access = list(
+ ACCESS_MEDICAL,
+ ACCESS_MORGUE,
+ ACCESS_PHARMACY,
+ ACCESS_PLUMBING,
+ ACCESS_SURGERY,
+ ACCESS_VIROLOGY,
+ )
/datum/id_trim/job/security_officer/science
assignment = "Security Officer (Science)"
trim_state = "trim_securityofficer_sci"
- department_access = list(ACCESS_AUX_BASE, ACCESS_RESEARCH, ACCESS_SCIENCE)
+ department_access = list(
+ ACCESS_AUX_BASE,
+ ACCESS_GENETICS,
+ ACCESS_ORDNANCE,
+ ACCESS_ORDNANCE_STORAGE,
+ ACCESS_RESEARCH,
+ ACCESS_ROBOTICS,
+ ACCESS_SCIENCE,
+ ACCESS_XENOBIOLOGY,
+ )
/datum/id_trim/job/shaft_miner
assignment = "Shaft Miner"
trim_state = "trim_shaftminer"
sechud_icon_state = SECHUD_SHAFT_MINER
- extra_access = list(ACCESS_MAINT_TUNNELS, ACCESS_QM)
- minimal_access = list(ACCESS_CARGO, ACCESS_AUX_BASE, ACCESS_MAIL_SORTING, ACCESS_MECH_MINING, ACCESS_MINERAL_STOREROOM, ACCESS_MINING,
- ACCESS_MINING_STATION)
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOP, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_AUX_BASE,
+ ACCESS_CARGO,
+ ACCESS_MECH_MINING,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MINING,
+ ACCESS_MINING_STATION,
+ )
+ extra_access = list(
+ ACCESS_MAINT_TUNNELS,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_QM,
+ )
job = /datum/job/shaft_miner
/// ID card obtained from the mining Disney dollar points vending machine.
/datum/id_trim/job/shaft_miner/spare
+ minimal_access = list(
+ ACCESS_CARGO,
+ ACCESS_MECH_MINING,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MINING,
+ ACCESS_MINING_STATION,
+ )
extra_access = list()
- minimal_access = list(ACCESS_CARGO, ACCESS_MAIL_SORTING, ACCESS_MECH_MINING, ACCESS_MINERAL_STOREROOM, ACCESS_MINING, ACCESS_MINING_STATION)
template_access = null
/datum/id_trim/job/station_engineer
assignment = "Station Engineer"
trim_state = "trim_stationengineer"
sechud_icon_state = SECHUD_STATION_ENGINEER
- extra_access = list(ACCESS_ATMOSPHERICS)
- minimal_access = list(ACCESS_AUX_BASE, ACCESS_CONSTRUCTION, ACCESS_ENGINEERING, ACCESS_ENGINE_EQUIP, ACCESS_EXTERNAL_AIRLOCKS,
- ACCESS_MAINT_TUNNELS, ACCESS_MECH_ENGINE, ACCESS_MINERAL_STOREROOM, ACCESS_TCOMMS, ACCESS_TECH_STORAGE)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CE, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_AUX_BASE,
+ ACCESS_CONSTRUCTION,
+ ACCESS_ENGINEERING,
+ ACCESS_ENGINE_EQUIP,
+ ACCESS_EXTERNAL_AIRLOCKS,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MECH_ENGINE,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_MINISAT,
+ ACCESS_TCOMMS,
+ ACCESS_TECH_STORAGE,
+ )
+ extra_access = list(
+ ACCESS_ATMOSPHERICS,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_CE,
+ )
job = /datum/job/station_engineer
/datum/id_trim/job/virologist
assignment = "Virologist"
trim_state = "trim_virologist"
sechud_icon_state = SECHUD_VIROLOGIST
- extra_access = list(ACCESS_PLUMBING, ACCESS_MORGUE, ACCESS_SURGERY)
- minimal_access = list(ACCESS_MEDICAL, ACCESS_MECH_MEDICAL, ACCESS_MINERAL_STOREROOM, ACCESS_VIROLOGY)
- template_access = list(ACCESS_CAPTAIN, ACCESS_CMO, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_MECH_MEDICAL,
+ ACCESS_MEDICAL,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_VIROLOGY,
+ )
+ extra_access = list(
+ ACCESS_PLUMBING,
+ ACCESS_MORGUE,
+ ACCESS_SURGERY,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_CMO,
+ )
job = /datum/job/virologist
/datum/id_trim/job/warden
assignment = "Warden"
trim_state = "trim_warden"
sechud_icon_state = SECHUD_WARDEN
- extra_access = list(ACCESS_DETECTIVE, ACCESS_MAINT_TUNNELS, ACCESS_MORGUE)
- minimal_access = list(ACCESS_ARMORY, ACCESS_BRIG, ACCESS_COURT, ACCESS_MECH_SECURITY, ACCESS_MINERAL_STOREROOM,
- ACCESS_SECURITY, ACCESS_BRIG_ENTRANCE, ACCESS_WEAPONS) // See /datum/job/warden/get_access()
- template_access = list(ACCESS_CAPTAIN, ACCESS_HOS, ACCESS_CHANGE_IDS)
+ minimal_access = list(
+ ACCESS_ARMORY,
+ ACCESS_BRIG,
+ ACCESS_BRIG_ENTRANCE,
+ ACCESS_COURT,
+ ACCESS_MECH_SECURITY,
+ ACCESS_MINERAL_STOREROOM,
+ ACCESS_SECURITY,
+ ACCESS_WEAPONS,
+ ) // See /datum/job/warden/get_access()
+ extra_access = list(
+ ACCESS_DETECTIVE,
+ ACCESS_MAINT_TUNNELS,
+ ACCESS_MORGUE,
+ )
+ template_access = list(
+ ACCESS_CAPTAIN,
+ ACCESS_CHANGE_IDS,
+ ACCESS_HOS,
+ )
job = /datum/job/warden
/datum/id_trim/job/warden/refresh_trim_access()
diff --git a/code/datums/id_trim/ruins.dm b/code/datums/id_trim/ruins.dm
index 3d44cc2d7cb19..b6b199bd7e855 100644
--- a/code/datums/id_trim/ruins.dm
+++ b/code/datums/id_trim/ruins.dm
@@ -17,7 +17,7 @@
/// Trim for the oldstation ruin/Charlie station
/datum/id_trim/away/old/sci
- access = list(ACCESS_AWAY_GENERAL)
+ access = list(ACCESS_AWAY_GENERAL, ACCESS_AWAY_SCIENCE)
assignment = "Charlie Station Scientist"
/// Trim for the oldstation ruin/Charlie station
@@ -30,9 +30,9 @@
access = list(ACCESS_ENGINEERING, ACCESS_ENGINE_EQUIP)
assignment = "Engineering Equipment Access"
-/// Trim for the oldstation ruin/Charlie station to access robots
+/// Trim for the oldstation ruin/Charlie station to access robots, and downloading of paper publishing software for experiments
/datum/id_trim/away/old/robo
- access = list(ACCESS_AWAY_GENERAL, ACCESS_ROBOTICS)
+ access = list(ACCESS_AWAY_GENERAL, ACCESS_ROBOTICS, ACCESS_ORDNANCE)
/// Trim for the cat surgeon ruin.
/datum/id_trim/away/cat_surgeon
diff --git a/code/datums/keybinding/communication.dm b/code/datums/keybinding/communication.dm
index 36301da7ab65d..0c96919651cce 100644
--- a/code/datums/keybinding/communication.dm
+++ b/code/datums/keybinding/communication.dm
@@ -3,18 +3,24 @@
/datum/keybinding/client/communication/say
hotkey_keys = list("T")
- name = "Say"
+ name = SAY_CHANNEL
full_name = "IC Say"
keybind_signal = COMSIG_KB_CLIENT_SAY_DOWN
+/datum/keybinding/client/communication/radio
+ hotkey_keys = list("Y")
+ name = RADIO_CHANNEL
+ full_name = "IC Radio (;)"
+ keybind_signal = COMSIG_KB_CLIENT_RADIO_DOWN
+
/datum/keybinding/client/communication/ooc
hotkey_keys = list("O")
- name = "OOC"
+ name = OOC_CHANNEL
full_name = "Out Of Character Say (OOC)"
keybind_signal = COMSIG_KB_CLIENT_OOC_DOWN
/datum/keybinding/client/communication/me
hotkey_keys = list("M")
- name = "Me"
+ name = ME_CHANNEL
full_name = "Custom Emote (/Me)"
keybind_signal = COMSIG_KB_CLIENT_ME_DOWN
diff --git a/code/datums/map_config.dm b/code/datums/map_config.dm
index 745f0fdbba645..c570e8be70624 100644
--- a/code/datums/map_config.dm
+++ b/code/datums/map_config.dm
@@ -35,6 +35,8 @@
var/job_changes = list()
/// List of additional areas that count as a part of the library
var/library_areas = list()
+ /// What message shows up when the orbit is shifted.
+ var/orbit_shift_replacement = "Attention crew, it appears that someone on your station has shifted your orbit into more dangerous territory."
/**
* Proc that simply loads the default map config, which should always be functional.
@@ -166,6 +168,9 @@
log_world("map_config space_empty_levels is not a number!")
return
+ if("orbit_shift_replacement" in json)
+ orbit_shift_replacement = json["orbit_shift_replacement"]
+
if ("minetype" in json)
minetype = json["minetype"]
diff --git a/code/datums/martial/cqc.dm b/code/datums/martial/cqc.dm
index 273b0b0dc902e..4c3b4c689b73a 100644
--- a/code/datums/martial/cqc.dm
+++ b/code/datums/martial/cqc.dm
@@ -22,19 +22,19 @@
if(!can_use(A))
return FALSE
if(findtext(streak,SLAM_COMBO))
- streak = ""
+ reset_streak(A)
return Slam(A,D)
if(findtext(streak,KICK_COMBO))
- streak = ""
+ reset_streak(A)
return Kick(A,D)
if(findtext(streak,RESTRAIN_COMBO))
- streak = ""
+ reset_streak(A)
return Restrain(A,D)
if(findtext(streak,PRESSURE_COMBO))
- streak = ""
+ reset_streak(A)
return Pressure(A,D)
if(findtext(streak,CONSECUTIVE_COMBO))
- streak = ""
+ reset_streak(A)
return Consecutive(A,D)
return FALSE
diff --git a/code/datums/martial/plasma_fist.dm b/code/datums/martial/plasma_fist.dm
index 6f5c2acad3963..af5640b84a5fd 100644
--- a/code/datums/martial/plasma_fist.dm
+++ b/code/datums/martial/plasma_fist.dm
@@ -16,17 +16,17 @@
if(findtext(streak,TORNADO_COMBO))
if(A == D)//helps using apotheosis
return FALSE
- streak = ""
+ reset_streak(A)
Tornado(A,D)
return TRUE
if(findtext(streak,THROWBACK_COMBO))
if(A == D)//helps using apotheosis
return FALSE
- streak = ""
+ reset_streak(A)
Throwback(A,D)
return TRUE
if(findtext(streak,PLASMA_COMBO))
- streak = ""
+ reset_streak(A)
if(A == D && !nobomb)
Apotheosis(A,D)
else
@@ -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/martial/sleeping_carp.dm b/code/datums/martial/sleeping_carp.dm
index 767cf869ecc65..3838305caf1ea 100644
--- a/code/datums/martial/sleeping_carp.dm
+++ b/code/datums/martial/sleeping_carp.dm
@@ -11,15 +11,15 @@
/datum/martial_art/the_sleeping_carp/proc/check_streak(mob/living/A, mob/living/D)
if(findtext(streak,STRONG_PUNCH_COMBO))
- streak = ""
+ reset_streak(A)
strongPunch(A,D)
return TRUE
if(findtext(streak,LAUNCH_KICK_COMBO))
- streak = ""
+ reset_streak(A)
launchKick(A,D)
return TRUE
if(findtext(streak,DROP_KICK_COMBO))
- streak = ""
+ reset_streak(A)
dropKick(A,D)
return TRUE
return FALSE
diff --git a/code/datums/memory/memory.dm b/code/datums/memory/memory.dm
index f2cbdd3bb3122..e1fe64e8f9f91 100644
--- a/code/datums/memory/memory.dm
+++ b/code/datums/memory/memory.dm
@@ -48,7 +48,7 @@
/mob/living/simple_animal/hostile/carp,
/mob/living/simple_animal/hostile/bear,
/mob/living/simple_animal/hostile/mushroom,
- /mob/living/simple_animal/hostile/statue,
+ /mob/living/simple_animal/hostile/netherworld/statue,
/mob/living/simple_animal/hostile/retaliate/bat,
/mob/living/simple_animal/hostile/retaliate/goat,
/mob/living/simple_animal/hostile/killertomato,
diff --git a/code/datums/mind.dm b/code/datums/mind.dm
index 9c5da09c43eff..106eb891bd645 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)
@@ -263,12 +260,12 @@
if(!length(shown_skills))
to_chat(user, span_notice("You don't seem to have any particularly outstanding skills."))
return
- var/msg = "[span_info("*---------*\nYour skills")]\n"
+ var/msg = "[span_info("Your skills")]\n"
for(var/i in shown_skills)
var/datum/skill/the_skill = i
msg += "[initial(the_skill.name)] - [get_skill_level_name(the_skill)]\n"
msg += ""
- to_chat(user, msg)
+ to_chat(user, examine_block(msg))
/datum/mind/proc/set_death_time()
SIGNAL_HANDLER
@@ -764,8 +761,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())
@@ -797,25 +795,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
@@ -825,27 +804,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/mood_events/generic_negative_events.dm b/code/datums/mood_events/generic_negative_events.dm
index 050ff5ca16923..150e94336d0cf 100644
--- a/code/datums/mood_events/generic_negative_events.dm
+++ b/code/datums/mood_events/generic_negative_events.dm
@@ -47,6 +47,11 @@
mood_change = -2
timeout = 4 MINUTES
+/datum/mood_event/cascade // Big boi delamination
+ description = "The engineers have finally done it, we are all going to die..."
+ mood_change = -8
+ timeout = 5 MINUTES
+
/datum/mood_event/depression_minimal
description = "I feel a bit down."
mood_change = -10
@@ -374,3 +379,8 @@
description = "This is really embarrassing! I'm ashamed to pick up all these cards off the floor..."
mood_change = -3
timeout = 3 MINUTES
+
+/datum/mood_event/russian_roulette_lose
+ description = "I gambled my life and lost! I guess this is the end..."
+ mood_change = -20
+ timeout = 10 MINUTES
diff --git a/code/datums/mood_events/generic_positive_events.dm b/code/datums/mood_events/generic_positive_events.dm
index 44acc5a322807..ad3b54efce725 100644
--- a/code/datums/mood_events/generic_positive_events.dm
+++ b/code/datums/mood_events/generic_positive_events.dm
@@ -282,6 +282,10 @@
mood_change = 2
timeout = 3 MINUTES
+/datum/mood_event/garland
+ description = "These flowers are rather soothing."
+ mood_change = 1
+
/datum/mood_event/playing_cards/add_effects(param)
var/card_players = 1
for(var/mob/living/carbon/player in viewers(COMBAT_MESSAGE_RANGE, owner))
@@ -293,3 +297,16 @@
mood_change *= card_players
return ..()
+
+/datum/mood_event/russian_roulette_win
+ description = "I gambled my life and won! I'm lucky to be alive..."
+ mood_change = 2
+ timeout = 5 MINUTES
+
+/datum/mood_event/russian_roulette_win/add_effects(loaded_rounds)
+ mood_change = 2 ** loaded_rounds
+
+/datum/mood_event/fishing
+ description = "Fishing is relaxing."
+ mood_change = 5
+ timeout = 3 MINUTES
diff --git a/code/datums/mutations/_mutations.dm b/code/datums/mutations/_mutations.dm
index 20956b76be2f5..72f9087fbb5da 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
@@ -114,7 +114,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
@@ -138,8 +138,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()
@@ -164,12 +166,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)
@@ -198,15 +209,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 48e8c41b078b7..0000000000000
--- 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 9b71063a54f72..a5b220abde633 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
+
+ if(cast_on == owner)
+ to_chat(owner, span_warning("You plunge into your mind... Yep, it's your mind."))
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]!"))
+ 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 0000000000000..8f7b66f0b6c34
--- /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 0abb77a6a2db3..6e9a9afa53f40 100644
--- a/code/datums/mutations/body.dm
+++ b/code/datums/mutations/body.dm
@@ -218,13 +218,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/cold.dm b/code/datums/mutations/cold.dm
index 1dd531490acc3..a49dfa2616083 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."
+ button_icon_state = "snow"
+
+ cooldown_time = 5 SECONDS
+ spell_requirements = NONE
+
item_type = /obj/item/stack/sheet/mineral/snow
- charge_max = 50
delete_old = FALSE
- action_icon_state = "snow"
-
/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 0000000000000..9869d41283e0f
--- /dev/null
+++ b/code/datums/mutations/fire_breath.dm
@@ -0,0 +1,96 @@
+/datum/mutation/human/firebreath
+ name = "Fire Breath"
+ desc = "An ancient mutation that gives lizards breath of fire."
+ quality = POSITIVE
+ difficulty = 12
+ locked = TRUE
+ text_gain_indication = "Your throat is burning!"
+ text_lose_indication = "Your throat is cooling down."
+ power_path = /datum/action/cooldown/spell/cone/staggered/fire_breath
+ instability = 30
+ energy_coeff = 1
+ power_coeff = 1
+
+/datum/mutation/human/firebreath/modify()
+ . = ..()
+ var/datum/action/cooldown/spell/cone/staggered/fire_breath/to_modify = .
+ if(!istype(to_modify)) // null or invalid
+ return
+
+ if(GET_MUTATION_POWER(src) <= 1) // we only care about power from here on
+ return
+
+ to_modify.cone_levels += 2 // Cone fwooshes further, and...
+ to_modify.self_throw_range += 1 // the breath throws the user back more
+
+/datum/action/cooldown/spell/cone/staggered/fire_breath
+ name = "Fire Breath"
+ desc = "You breathe a cone of fire directly in front of you."
+ button_icon_state = "fireball0"
+ sound = 'sound/magic/demon_dies.ogg' //horrifying lizard noises
+
+ school = SCHOOL_EVOCATION
+ cooldown_time = 40 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+ antimagic_flags = NONE
+
+ cone_levels = 3
+ respect_density = TRUE
+ /// The range our user is thrown backwards after casting the spell
+ var/self_throw_range = 1
+
+/datum/action/cooldown/spell/cone/staggered/fire_breath/before_cast(atom/cast_on)
+ . = ..()
+ if(. & SPELL_CANCEL_CAST)
+ return
+
+ if(!iscarbon(cast_on))
+ return
+
+ var/mob/living/carbon/our_lizard = cast_on
+ if(!our_lizard.is_mouth_covered())
+ return
+
+ our_lizard.adjust_fire_stacks(cone_levels)
+ our_lizard.ignite_mob()
+ to_chat(our_lizard, span_warning("Something in front of your mouth catches fire!"))
+
+/datum/action/cooldown/spell/cone/staggered/fire_breath/after_cast(atom/cast_on)
+ . = ..()
+ if(!isliving(cast_on))
+ return
+
+ var/mob/living/living_cast_on = cast_on
+ // When casting, throw the caster backwards a few tiles.
+ var/original_dir = living_cast_on.dir
+ living_cast_on.throw_at(
+ get_edge_target_turf(living_cast_on, turn(living_cast_on.dir, 180)),
+ range = self_throw_range,
+ speed = 2,
+ gentle = TRUE,
+ )
+ // Try to set us to our original direction after, so we don't end up backwards.
+ living_cast_on.setDir(original_dir)
+
+/datum/action/cooldown/spell/cone/staggered/fire_breath/calculate_cone_shape(current_level)
+ // This makes the cone shoot out into a 3 wide column of flames.
+ // You may be wondering, "that equation doesn't seem like it'd make a 3 wide column"
+ // well it does, and that's all that matters.
+ return (2 * current_level) - 1
+
+/datum/action/cooldown/spell/cone/staggered/fire_breath/do_turf_cone_effect(turf/target_turf, atom/caster, level)
+ // Further turfs experience less exposed_temperature and exposed_volume
+ new /obj/effect/hotspot(target_turf) // for style
+ target_turf.hotspot_expose(max(500, 900 - (100 * level)), max(50, 200 - (50 * level)), 1)
+
+/datum/action/cooldown/spell/cone/staggered/fire_breath/do_mob_cone_effect(mob/living/target_mob, atom/caster, level)
+ // Further out targets take less immediate burn damage and get less fire stacks.
+ // The actual burn damage application is not blocked by fireproofing, like space dragons.
+ target_mob.apply_damage(max(10, 40 - (5 * level)), BURN, spread_damage = TRUE)
+ target_mob.adjust_fire_stacks(max(2, 5 - level))
+ target_mob.ignite_mob()
+
+/datum/action/cooldown/spell/cone/staggered/firebreath/do_obj_cone_effect(obj/target_obj, atom/caster, level)
+ // Further out objects experience less exposed_temperature and exposed_volume
+ target_obj.fire_act(max(500, 900 - (100 * level)), max(50, 200 - (50 * level)))
diff --git a/code/datums/mutations/holy_mutation/honorbound.dm b/code/datums/mutations/holy_mutation/honorbound.dm
index 6b7eac5cb4c80..46de73bea5e60 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)
+ /// 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!"
+
+/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
- if(!targets.len)
- if(!silent)
- to_chat(user, span_warning("Nobody to declare evil here!"))
+
+ 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
- if(targets.len > 1)
- if(!silent)
- to_chat(user, span_warning("Too many people to declare! Pick ONE!"))
+
+ 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
- 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!"))
+
+ // This shouldn't technically be a possible state, but you never know
+ if(!honor_mutation)
return FALSE
- if(!can_target(targets[1], user, silent))
+ 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
- GLOB.religious_sect.adjust_favor(-150, user)
- user.say(declaration_message)
- honormut.guilty(targets[1], declaration = TRUE)
+
return TRUE
-/obj/effect/proc_holder/spell/pointed/declare_evil/can_target(atom/target, mob/user, silent)
+/datum/action/cooldown/spell/pointed/declare_evil/is_valid_target(atom/cast_on)
. = ..()
if(!.)
return FALSE
- if(!isliving(target))
- if(!silent)
- to_chat(user, span_warning("You can only declare living beings evil!"))
+ if(!isliving(cast_on))
+ to_chat(owner, span_warning("You can only declare living beings evil!"))
return FALSE
- var/mob/living/victim = target
- if(victim.stat == DEAD)
- if(!silent)
- to_chat(user, span_warning("Declaration on the dead? Really?"))
+
+ 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
- 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."))
+
+ // 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
- 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!"))
+
+ // 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
- 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!"))
+
+ // 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 0000000000000..e014806233a7b
--- /dev/null
+++ b/code/datums/mutations/olfaction.dm
@@ -0,0 +1,139 @@
+/datum/mutation/human/olfaction
+ name = "Transcendent Olfaction"
+ desc = "Your sense of smell is comparable to that of a canine."
+ quality = POSITIVE
+ difficulty = 12
+ text_gain_indication = "Smells begin to make more sense..."
+ text_lose_indication = "Your sense of smell goes back to normal."
+ power_path = /datum/action/cooldown/spell/olfaction
+ instability = 30
+ synchronizer_coeff = 1
+
+/datum/mutation/human/olfaction/modify()
+ . = ..()
+ var/datum/action/cooldown/spell/olfaction/to_modify = .
+ if(!istype(to_modify)) // null or invalid
+ return
+
+ to_modify.sensitivity = GET_MUTATION_SYNCHRONIZER(src)
+
+/datum/action/cooldown/spell/olfaction
+ name = "Remember the Scent"
+ desc = "Get a scent off of the item you're currently holding to track it. \
+ With an empty hand, you'll track the scent you've remembered."
+ button_icon_state = "nose"
+
+ cooldown_time = 10 SECONDS
+ spell_requirements = NONE
+
+ /// Weakref to the mob we're tracking
+ var/datum/weakref/tracking_ref
+ /// Our nose's sensitivity
+ var/sensitivity = 1
+
+/datum/action/cooldown/spell/olfaction/is_valid_target(atom/cast_on)
+ if(!isliving(cast_on))
+ return FALSE
+
+ var/mob/living/living_cast_on = cast_on
+ if(ishuman(living_cast_on) && !living_cast_on.get_bodypart(BODY_ZONE_HEAD))
+ to_chat(owner, span_warning("You have no nose!"))
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/spell/olfaction/cast(mob/living/cast_on)
+ . = ..()
+ // Can we sniff? is there miasma in the air?
+ var/datum/gas_mixture/air = cast_on.loc.return_air()
+ var/list/cached_gases = air.gases
+
+ if(cached_gases[/datum/gas/miasma])
+ cast_on.adjust_disgust(sensitivity * 45)
+ to_chat(cast_on, span_warning("With your overly sensitive nose, \
+ you get a whiff of stench and feel sick! Try moving to a cleaner area!"))
+ return
+
+ var/atom/sniffed = cast_on.get_active_held_item()
+ if(sniffed)
+ pick_up_target(cast_on, sniffed)
+ else
+ follow_target(cast_on)
+
+/// Attempt to pick up a new target based on the fingerprints on [sniffed].
+/datum/action/cooldown/spell/olfaction/proc/pick_up_target(mob/living/caster, atom/sniffed)
+ var/mob/living/carbon/old_target = tracking_ref?.resolve()
+ var/list/possibles = list()
+ var/list/prints = GET_ATOM_FINGERPRINTS(sniffed)
+ if(prints)
+ for(var/mob/living/carbon/to_check as anything in GLOB.carbon_list)
+ if(prints[md5(to_check.dna?.unique_identity)])
+ possibles |= to_check
+
+ // There are no finger prints on the atom, so nothing to track
+ if(!length(possibles))
+ to_chat(caster, span_warning("Despite your best efforts, there are no scents to be found on [sniffed]..."))
+ return
+
+ var/mob/living/carbon/new_target = tgui_input_list(caster, "Scent to remember", "Scent Tracking", sort_names(possibles))
+ if(QDELETED(src) || QDELETED(caster))
+ return
+
+ if(QDELETED(new_target))
+ // We don't have a new target OR an old target
+ if(QDELETED(old_target))
+ to_chat(caster, span_warning("You decide against remembering any scents. \
+ Instead, you notice your own nose in your peripheral vision. \
+ This goes on to remind you of that one time you started breathing manually and couldn't stop. \
+ What an awful day that was."))
+ tracking_ref = null
+
+ // We don't have a new target, but we have an old target to fall back on
+ else
+ to_chat(caster, span_notice("You return to tracking [old_target]. The hunt continues."))
+ on_the_trail(caster)
+ return
+
+ // We have a new target to track
+ to_chat(caster, span_notice("You pick up the scent of [new_target]. The hunt begins."))
+ tracking_ref = WEAKREF(new_target)
+ on_the_trail(caster)
+
+/// Attempt to follow our current tracking target.
+/datum/action/cooldown/spell/olfaction/proc/follow_target(mob/living/caster)
+ var/mob/living/carbon/current_target = tracking_ref?.resolve()
+ // Either our weakref failed to resolve (our target's gone),
+ // or we never had a target in the first place
+ if(QDELETED(current_target))
+ to_chat(caster, span_warning("You're not holding anything to smell, \
+ and you haven't smelled anything you can track. You smell your skin instead; it's kinda salty."))
+ tracking_ref = null
+ return
+
+ on_the_trail(caster)
+
+/// Actually go through and give the user a hint of the direction our target is.
+/datum/action/cooldown/spell/olfaction/proc/on_the_trail(mob/living/caster)
+ var/mob/living/carbon/current_target = tracking_ref?.resolve()
+ if(!current_target)
+ to_chat(caster, span_warning("You're not tracking a scent, but the game thought you were. \
+ Something's gone wrong! Report this as a bug."))
+ stack_trace("[type] - on_the_trail was called when no tracking target was set.")
+ tracking_ref = null
+ return
+
+ if(current_target == caster)
+ to_chat(caster, span_warning("You smell out the trail to yourself. Yep, it's you."))
+ return
+
+ if(caster.z < current_target.z)
+ to_chat(caster, span_warning("The trail leads... way up above you? Huh. They must be really, really far away."))
+ return
+
+ else if(caster.z > current_target.z)
+ to_chat(caster, span_warning("The trail leads... way down below you? Huh. They must be really, really far away."))
+ return
+
+ var/direction_text = span_bold("[dir2text(get_dir(caster, current_target))]")
+ if(direction_text)
+ to_chat(caster, span_notice("You consider [current_target]'s scent. The trail leads [direction_text]."))
diff --git a/code/datums/mutations/passive.dm b/code/datums/mutations/passive.dm
index 6024d212be00c..1c6d130b9e574 100644
--- a/code/datums/mutations/passive.dm
+++ b/code/datums/mutations/passive.dm
@@ -24,8 +24,10 @@
if(..())
return
ADD_TRAIT(owner, TRAIT_ADVANCEDTOOLUSER, GENETIC_MUTATION)
+ ADD_TRAIT(owner, TRAIT_LITERATE, GENETIC_MUTATION)
/datum/mutation/human/clever/on_losing(mob/living/carbon/human/owner)
if(..())
return
REMOVE_TRAIT(owner, TRAIT_ADVANCEDTOOLUSER, GENETIC_MUTATION)
+ REMOVE_TRAIT(owner, TRAIT_LITERATE, GENETIC_MUTATION)
diff --git a/code/datums/mutations/sight.dm b/code/datums/mutations/sight.dm
index bc4995cb48588..eb652125c2a61 100644
--- a/code/datums/mutations/sight.dm
+++ b/code/datums/mutations/sight.dm
@@ -15,7 +15,6 @@
return
owner.cure_nearsighted(GENETIC_MUTATION)
-
///Blind makes you blind. Who knew?
/datum/mutation/human/blind
name = "Blindness"
@@ -45,61 +44,67 @@
synchronizer_coeff = 1
power_coeff = 1
energy_coeff = 1
- power = /obj/effect/proc_holder/spell/self/thermal_vision_activate
+ power_path = /datum/action/cooldown/spell/thermal_vision
+
+/datum/mutation/human/thermal/on_losing(mob/living/carbon/human/owner)
+ if(..())
+ return
+ // 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()
- 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
+ . = ..()
+ 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)
-/obj/effect/proc_holder/spell/self/thermal_vision_activate
+/datum/action/cooldown/spell/thermal_vision
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)
- . = ..()
+ icon_icon = 'icons/mob/actions/actions_changeling.dmi'
+ button_icon_state = "augmented_eyesight"
- if(HAS_TRAIT(user,TRAIT_THERMAL_VISION))
- return
+ cooldown_time = 25 SECONDS
+ spell_requirements = NONE
- 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)
+ /// How much eye damage is given on cast
+ var/eye_damage = 10
+ /// The duration of the thermal vision
+ var/thermal_duration = 10 SECONDS
-/obj/effect/proc_holder/spell/self/thermal_vision_activate/proc/thermal_vision_deactivate(mob/user = usr)
+/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)
- 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."))
+/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)
- var/mob/living/carbon/user_mob = user
- if(!istype(user_mob))
+/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
- user_mob.adjustOrganLoss(ORGAN_SLOT_EYES, eye_damage)
+ 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."))
-/datum/mutation/human/thermal/on_losing(mob/living/carbon/human/owner)
- if(..())
- return
- REMOVE_TRAIT(owner, TRAIT_THERMAL_VISION, GENETIC_MUTATION)
- owner.update_sight()
+ 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
@@ -174,3 +179,20 @@
name = "beam"
icon = 'icons/effects/genetics.dmi'
icon_state = "eyelasers"
+
+/datum/mutation/human/illiterate
+ name = "Illiterate"
+ desc = "Causes a severe case of Aphasia that prevents reading or writing."
+ quality = NEGATIVE
+ text_gain_indication = "You feel unable to read or write."
+ text_lose_indication = "You feel able to read and write again."
+
+/datum/mutation/human/illiterate/on_acquiring(mob/living/carbon/human/owner)
+ if(..())
+ return
+ ADD_TRAIT(owner, TRAIT_ILLITERATE, GENETIC_MUTATION)
+
+/datum/mutation/human/illiterate/on_losing(mob/living/carbon/human/owner)
+ if(..())
+ return
+ REMOVE_TRAIT(owner, TRAIT_ILLITERATE, GENETIC_MUTATION)
diff --git a/code/datums/mutations/telepathy.dm b/code/datums/mutations/telepathy.dm
new file mode 100644
index 0000000000000..8619c2bddc476
--- /dev/null
+++ b/code/datums/mutations/telepathy.dm
@@ -0,0 +1,10 @@
+/datum/mutation/human/telepathy
+ name = "Telepathy"
+ desc = "A rare mutation that allows the user to telepathically communicate to others."
+ quality = POSITIVE
+ text_gain_indication = "You can hear your own voice echoing in your mind!"
+ text_lose_indication = "You don't hear your mind echo anymore."
+ difficulty = 12
+ power_path = /datum/action/cooldown/spell/list_target/telepathy
+ instability = 10
+ energy_coeff = 1
diff --git a/code/datums/mutations/tongue_spike.dm b/code/datums/mutations/tongue_spike.dm
new file mode 100644
index 0000000000000..1bd02df0b3e2b
--- /dev/null
+++ b/code/datums/mutations/tongue_spike.dm
@@ -0,0 +1,181 @@
+/datum/mutation/human/tongue_spike
+ name = "Tongue Spike"
+ desc = "Allows a creature to voluntary shoot their tongue out as a deadly weapon."
+ quality = POSITIVE
+ text_gain_indication = span_notice("Your feel like you can throw your voice.")
+ instability = 15
+ power_path = /datum/action/cooldown/spell/tongue_spike
+
+ energy_coeff = 1
+ synchronizer_coeff = 1
+
+/datum/action/cooldown/spell/tongue_spike
+ name = "Launch spike"
+ desc = "Shoot your tongue out in the direction you're facing, embedding it and dealing damage until they remove it."
+ icon_icon = 'icons/mob/actions/actions_genetic.dmi'
+ button_icon_state = "spike"
+
+ cooldown_time = 10 SECONDS
+ spell_requirements = SPELL_REQUIRES_HUMAN
+
+ /// The type-path to what projectile we spawn to throw at someone.
+ var/spike_path = /obj/item/hardened_spike
+
+/datum/action/cooldown/spell/tongue_spike/is_valid_target(atom/cast_on)
+ return iscarbon(cast_on)
+
+/datum/action/cooldown/spell/tongue_spike/cast(mob/living/carbon/cast_on)
+ . = ..()
+ if(HAS_TRAIT(cast_on, TRAIT_NODISMEMBER))
+ to_chat(cast_on, span_notice("You concentrate really hard, but nothing happens."))
+ return
+
+ var/obj/item/organ/internal/tongue/to_fire = locate() in cast_on.internal_organs
+ if(!to_fire)
+ to_chat(cast_on, span_notice("You don't have a tongue to shoot!"))
+ return
+
+ to_fire.Remove(cast_on, special = TRUE)
+ var/obj/item/hardened_spike/spike = new spike_path(get_turf(cast_on), cast_on)
+ to_fire.forceMove(spike)
+ spike.throw_at(get_edge_target_turf(cast_on, cast_on.dir), 14, 4, cast_on)
+
+/obj/item/hardened_spike
+ name = "biomass spike"
+ desc = "Hardened biomass, shaped into a spike. Very pointy!"
+ icon_state = "tonguespike"
+ force = 2
+ throwforce = 15 //15 + 2 (WEIGHT_CLASS_SMALL) * 4 (EMBEDDED_IMPACT_PAIN_MULTIPLIER) = i didnt do the math
+ throw_speed = 4
+ embedding = list(
+ "embedded_pain_multiplier" = 4,
+ "embed_chance" = 100,
+ "embedded_fall_chance" = 0,
+ "embedded_ignore_throwspeed_threshold" = TRUE,
+ )
+ w_class = WEIGHT_CLASS_SMALL
+ sharpness = SHARP_POINTY
+ custom_materials = list(/datum/material/biomass = 500)
+ /// What mob "fired" our tongue
+ var/datum/weakref/fired_by_ref
+ /// if we missed our target
+ var/missed = TRUE
+
+/obj/item/hardened_spike/Initialize(mapload, mob/living/carbon/source)
+ . = ..()
+ src.fired_by_ref = WEAKREF(source)
+ addtimer(CALLBACK(src, .proc/check_embedded), 5 SECONDS)
+
+/obj/item/hardened_spike/proc/check_embedded()
+ if(missed)
+ unembedded()
+
+/obj/item/hardened_spike/embedded(atom/target)
+ if(isbodypart(target))
+ missed = FALSE
+
+/obj/item/hardened_spike/unembedded()
+ visible_message(span_warning("[src] cracks and twists, changing shape!"))
+ for(var/obj/tongue as anything in contents)
+ tongue.forceMove(get_turf(src))
+
+ qdel(src)
+
+/datum/mutation/human/tongue_spike/chem
+ name = "Chem Spike"
+ desc = "Allows a creature to voluntary shoot their tongue out as biomass, allowing a long range transfer of chemicals."
+ quality = POSITIVE
+ text_gain_indication = span_notice("Your feel like you can really connect with people by throwing your voice.")
+ instability = 15
+ locked = TRUE
+ power_path = /datum/action/cooldown/spell/tongue_spike/chem
+ energy_coeff = 1
+ synchronizer_coeff = 1
+
+/datum/action/cooldown/spell/tongue_spike/chem
+ name = "Launch chem spike"
+ desc = "Shoot your tongue out in the direction you're facing, \
+ embedding it for a very small amount of damage. \
+ While the other person has the spike embedded, \
+ you can transfer your chemicals to them."
+ button_icon_state = "spikechem"
+
+ spike_path = /obj/item/hardened_spike/chem
+
+/obj/item/hardened_spike/chem
+ name = "chem spike"
+ desc = "Hardened biomass, shaped into... something."
+ icon_state = "tonguespikechem"
+ throwforce = 2 //2 + 2 (WEIGHT_CLASS_SMALL) * 0 (EMBEDDED_IMPACT_PAIN_MULTIPLIER) = i didnt do the math again but very low or smthin
+ embedding = list(
+ "embedded_pain_multiplier" = 0,
+ "embed_chance" = 100,
+ "embedded_fall_chance" = 0,
+ "embedded_pain_chance" = 0,
+ "embedded_ignore_throwspeed_threshold" = TRUE, //never hurts once it's in you
+ )
+ /// Whether the tongue's already embedded in a target once before
+ var/embedded_once_alread = FALSE
+
+/obj/item/hardened_spike/chem/embedded(mob/living/carbon/human/embedded_mob)
+ if(embedded_once_alread)
+ return
+ embedded_once_alread = TRUE
+
+ var/mob/living/carbon/fired_by = fired_by_ref?.resolve()
+ if(!fired_by)
+ return
+
+ var/datum/action/send_chems/chem_action = new(src)
+ chem_action.transfered_ref = WEAKREF(embedded_mob)
+ chem_action.Grant(fired_by)
+
+ to_chat(fired_by, span_notice("Link established! Use the \"Transfer Chemicals\" ability \
+ to send your chemicals to the linked target!"))
+
+/obj/item/hardened_spike/chem/unembedded()
+ var/mob/living/carbon/fired_by = fired_by_ref?.resolve()
+ if(fired_by)
+ to_chat(fired_by, span_warning("Link lost!"))
+ var/datum/action/send_chems/chem_action = locate() in fired_by.actions
+ QDEL_NULL(chem_action)
+
+ return ..()
+
+/datum/action/send_chems
+ name = "Transfer Chemicals"
+ desc = "Send all of your reagents into whomever the chem spike is embedded in. One use."
+ background_icon_state = "bg_spell"
+ icon_icon = 'icons/mob/actions/actions_genetic.dmi'
+ button_icon_state = "spikechemswap"
+ check_flags = AB_CHECK_CONSCIOUS
+
+ /// Weakref to the mob target that we transfer chemicals to on activation
+ var/datum/weakref/transfered_ref
+
+/datum/action/send_chems/New(Target)
+ . = ..()
+ if(!istype(target, /obj/item/hardened_spike/chem))
+ qdel(src)
+
+/datum/action/send_chems/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!ishuman(owner) || !owner.reagents)
+ return FALSE
+ var/mob/living/carbon/human/transferer = owner
+ var/mob/living/carbon/human/transfered = transfered_ref?.resolve()
+ if(!ishuman(transfered))
+ return FALSE
+
+ to_chat(transfered, span_warning("You feel a tiny prick!"))
+ transferer.reagents.trans_to(transfered, transferer.reagents.total_volume, 1, 1, 0, transfered_by = transferer)
+
+ var/obj/item/hardened_spike/chem/chem_spike = target
+ var/obj/item/bodypart/spike_location = chem_spike.check_embedded()
+
+ //this is where it would deal damage, if it transfers chems it removes itself so no damage
+ chem_spike.forceMove(get_turf(spike_location))
+ chem_spike.visible_message(span_notice("[chem_spike] falls out of [spike_location]!"))
+ return TRUE
diff --git a/code/datums/mutations/touch.dm b/code/datums/mutations/touch.dm
index 951d6edc6a70a..4328e397c6a8b 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 0000000000000..7900b4c099f17
--- /dev/null
+++ b/code/datums/mutations/void_magnet.dm
@@ -0,0 +1,43 @@
+/datum/mutation/human/void
+ name = "Void Magnet"
+ desc = "A rare genome that attracts odd forces not usually observed."
+ quality = MINOR_NEGATIVE //upsides and downsides
+ text_gain_indication = "You feel a heavy, dull force just beyond the walls watching you."
+ instability = 30
+ power_path = /datum/action/cooldown/spell/void
+ energy_coeff = 1
+ synchronizer_coeff = 1
+
+/datum/mutation/human/void/on_life(delta_time, times_fired)
+ // Move this onto the spell itself at some point?
+ var/datum/action/cooldown/spell/void/curse = locate(power_path) in owner
+ if(!curse)
+ remove()
+ return
+
+ if(!curse.is_valid_target(owner))
+ return
+
+ //very rare, but enough to annoy you hopefully. + 0.5 probability for every 10 points lost in stability
+ if(DT_PROB((0.25 + ((100 - dna.stability) / 40)) * GET_MUTATION_SYNCHRONIZER(src), delta_time))
+ curse.cast(owner)
+
+/datum/action/cooldown/spell/void
+ name = "Convoke Void" //magic the gathering joke here
+ desc = "A rare genome that attracts odd forces not usually observed. May sometimes pull you in randomly."
+ button_icon_state = "void_magnet"
+
+ school = SCHOOL_EVOCATION
+ cooldown_time = 1 MINUTES
+
+ invocation = "DOOOOOOOOOOOOOOOOOOOOM!!!"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = NONE
+ antimagic_flags = NONE
+
+/datum/action/cooldown/spell/void/is_valid_target(atom/cast_on)
+ return isturf(cast_on.loc)
+
+/datum/action/cooldown/spell/void/cast(atom/cast_on)
+ . = ..()
+ new /obj/effect/immortality_talisman/void(get_turf(cast_on), cast_on)
diff --git a/code/datums/mutations/webbing.dm b/code/datums/mutations/webbing.dm
new file mode 100644
index 0000000000000..2d696938e6ca5
--- /dev/null
+++ b/code/datums/mutations/webbing.dm
@@ -0,0 +1,52 @@
+//spider webs
+/datum/mutation/human/webbing
+ name = "Webbing Production"
+ desc = "Allows the user to lay webbing, and travel through it."
+ quality = POSITIVE
+ text_gain_indication = "Your skin feels webby."
+ instability = 15
+ power_path = /datum/action/cooldown/spell/lay_genetic_web
+
+/datum/mutation/human/webbing/on_acquiring(mob/living/carbon/human/owner)
+ if(..())
+ return
+ ADD_TRAIT(owner, TRAIT_WEB_WEAVER, GENETIC_MUTATION)
+
+/datum/mutation/human/webbing/on_losing(mob/living/carbon/human/owner)
+ if(..())
+ return
+ REMOVE_TRAIT(owner, TRAIT_WEB_WEAVER, GENETIC_MUTATION)
+
+// In the future this could be unified with the spider's web action
+/datum/action/cooldown/spell/lay_genetic_web
+ name = "Lay Web"
+ desc = "Drops a web. Only you will be able to traverse your web easily, making it pretty good for keeping you safe."
+ icon_icon = 'icons/mob/actions/actions_genetic.dmi'
+ button_icon_state = "lay_web"
+
+ cooldown_time = 4 SECONDS //the same time to lay a web
+ spell_requirements = NONE
+
+ /// How long it takes to lay a web
+ var/webbing_time = 4 SECONDS
+ /// The path of web that we create
+ var/web_path = /obj/structure/spider/stickyweb/genetic
+
+/datum/action/cooldown/spell/lay_genetic_web/cast(atom/cast_on)
+ var/turf/web_spot = cast_on.loc
+ if(!isturf(web_spot) || (locate(web_path) in web_spot))
+ to_chat(cast_on, span_warning("You can't lay webs here!"))
+ reset_spell_cooldown()
+ return FALSE
+
+ cast_on.visible_message(
+ span_notice("[cast_on] begins to secrete a sticky substance."),
+ span_notice("You begin to lay a web."),
+ )
+
+ if(!do_after(cast_on, webbing_time, target = web_spot))
+ to_chat(cast_on, span_warning("Your web spinning was interrupted!"))
+ return
+
+ new web_path(web_spot, cast_on)
+ return ..()
diff --git a/code/datums/proximity_monitor/fields/timestop.dm b/code/datums/proximity_monitor/fields/timestop.dm
index c9c544dff0d97..bde85c6f4d9e8 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/ruins/space.dm b/code/datums/ruins/space.dm
index c9c814fd1e79f..c5f5b72f2f012 100644
--- a/code/datums/ruins/space.dm
+++ b/code/datums/ruins/space.dm
@@ -66,12 +66,12 @@
description = "A once-bustling tradestation that handled imports and exports from nearby stations now lays eerily dormant. \
The last received message was a distress call from one of the on-board officers, but we had no success in making contact again."
-/datum/map_template/ruin/space/derelict1
- id = "derelict1"
- suffix = "derelict1.dmm"
- name = "Derelict 1"
- description = "Nothing to see here citizen, move along, certainly no xeno outbreaks on this piece of station debris. That purple stuff? It's uh... station nectar. \
- It's a top secret research installation."
+/datum/map_template/ruin/space/derelict_sulaco
+ id = "derelict_sulaco"
+ suffix = "derelict_sulaco.dmm"
+ name = "Derelict Sulaco"
+ description = "Nothing to see here citizen, move along, certainly no xeno outbreaks here. That purple stuff? It's uh... space nectar... but don't eat it! \
+ It's the bridge of a top secret military ship."
/datum/map_template/ruin/space/derelict2
id = "derelict2"
diff --git a/code/datums/status_effects/debuffs/fire_stacks.dm b/code/datums/status_effects/debuffs/fire_stacks.dm
index 598f38d3adfd3..bc601ef167a06 100644
--- a/code/datums/status_effects/debuffs/fire_stacks.dm
+++ b/code/datums/status_effects/debuffs/fire_stacks.dm
@@ -220,6 +220,7 @@
SEND_SIGNAL(owner, COMSIG_LIVING_IGNITED, owner)
cache_stacks()
update_overlay()
+ return TRUE
/**
* Handles mob extinguishing, should be the only way to set on_fire to FALSE
diff --git a/code/datums/status_effects/debuffs/jitteriness.dm b/code/datums/status_effects/debuffs/jitteriness.dm
index d97748b43fc1f..dea467e68c30c 100644
--- a/code/datums/status_effects/debuffs/jitteriness.dm
+++ b/code/datums/status_effects/debuffs/jitteriness.dm
@@ -8,6 +8,12 @@
return ..()
/datum/status_effect/jitter/on_apply()
+ // If we're being applied to a dead person, don't make the status effect.
+ // Just do a bit of jitter animation and be done.
+ if(owner.stat == DEAD)
+ owner.do_jitter_animation(duration / 10)
+ return FALSE
+
RegisterSignal(owner, list(COMSIG_LIVING_POST_FULLY_HEAL, COMSIG_LIVING_DEATH), .proc/remove_jitter)
SEND_SIGNAL(owner, COMSIG_ADD_MOOD_EVENT, id, /datum/mood_event/jittery)
return TRUE
diff --git a/code/datums/status_effects/gas.dm b/code/datums/status_effects/gas.dm
index c4e671cdbfe04..0ef72ce2b8a6f 100644
--- a/code/datums/status_effects/gas.dm
+++ b/code/datums/status_effects/gas.dm
@@ -51,3 +51,33 @@
/datum/status_effect/freon/watcher
duration = 8
can_melt = FALSE
+
+/datum/status_effect/hypernob_protection
+ id = "hypernob_protection"
+ duration = 10 SECONDS
+ alert_type = /atom/movable/screen/alert/status_effect/hypernob_protection
+
+/datum/status_effect/hypernob_protection/on_creation(mob/living/new_owner, duration = 10 SECONDS)
+ src.duration = duration
+ return ..()
+
+/atom/movable/screen/alert/status_effect/hypernob_protection
+ name = "Hyper-Noblium Protection"
+ desc = "The Hyper-Noblium around your body is protecting it from self-combustion and fires, but you feel sluggish..."
+ icon_state = "hypernob_protection"
+
+/datum/status_effect/hypernob_protection/on_apply()
+ if(!ishuman(owner))
+ CRASH("[type] status effect added to non-human owner: [owner ? owner.type : "null owner"]")
+ var/mob/living/carbon/human/human_owner = owner
+ human_owner.add_movespeed_modifier(/datum/movespeed_modifier/reagent/hypernoblium) //small slowdown as a tradeoff
+ ADD_TRAIT(human_owner, TRAIT_NOFIRE, type)
+ return TRUE
+
+/datum/status_effect/hypernob_protection/on_remove()
+ if(!ishuman(owner))
+ stack_trace("[type] status effect being removed from non-human owner: [owner ? owner.type : "null owner"]")
+ var/mob/living/carbon/human/human_owner = owner
+ human_owner.remove_movespeed_modifier(/datum/movespeed_modifier/reagent/hypernoblium)
+ REMOVE_TRAIT(human_owner, TRAIT_NOFIRE, type)
+
diff --git a/code/datums/status_effects/neutral.dm b/code/datums/status_effects/neutral.dm
index e188b31c8d016..06861e64a9c0e 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/datums/status_effects/wound_effects.dm b/code/datums/status_effects/wound_effects.dm
index e29f093296717..9ce7af7fc4bd7 100644
--- a/code/datums/status_effects/wound_effects.dm
+++ b/code/datums/status_effects/wound_effects.dm
@@ -142,9 +142,9 @@
alert_type = NONE
/datum/status_effect/wound/on_creation(mob/living/new_owner, incoming_wound)
- . = ..()
linked_wound = incoming_wound
linked_limb = linked_wound.limb
+ return ..()
/datum/status_effect/wound/on_remove()
linked_wound = null
diff --git a/code/datums/weather/weather.dm b/code/datums/weather/weather.dm
index d665138ee2158..0cc4c9c91ba28 100644
--- a/code/datums/weather/weather.dm
+++ b/code/datums/weather/weather.dm
@@ -59,11 +59,15 @@
/// Since it's above everything else, this is the layer used by default. TURF_LAYER is below mobs and walls if you need to use that.
var/overlay_layer = AREA_LAYER
/// Plane for the overlay
- var/overlay_plane = ABOVE_LIGHTING_PLANE
+ var/overlay_plane = AREA_PLANE
/// If the weather has no purpose other than looks
var/aesthetic = FALSE
/// Used by mobs (or movables containing mobs, such as enviro bags) to prevent them from being affected by the weather.
var/immunity_type
+ /// If this bit of weather should also draw an overlay that's uneffected by lighting onto the area
+ /// Taken from weather_glow.dmi
+ var/use_glow = TRUE
+ var/mutable_appearance/current_glow
/// The stage of the weather, from 1-4
var/stage = END_STAGE
@@ -224,23 +228,37 @@
*
*/
/datum/weather/proc/update_areas()
+ var/using_icon_state = ""
+ switch(stage)
+ if(STARTUP_STAGE)
+ using_icon_state = telegraph_overlay
+ if(MAIN_STAGE)
+ using_icon_state = weather_overlay
+ if(WIND_DOWN_STAGE)
+ using_icon_state = end_overlay
+ if(END_STAGE)
+ using_icon_state = ""
+
+ var/mutable_appearance/glow_overlay = mutable_appearance('icons/effects/glow_weather.dmi', using_icon_state, overlay_layer, ABOVE_LIGHTING_PLANE, 100)
for(var/V in impacted_areas)
var/area/N = V
- N.layer = overlay_layer
- N.plane = overlay_plane
- N.icon = 'icons/effects/weather_effects.dmi'
- N.color = weather_color
- switch(stage)
- if(STARTUP_STAGE)
- N.icon_state = telegraph_overlay
- if(MAIN_STAGE)
- N.icon_state = weather_overlay
- if(WIND_DOWN_STAGE)
- N.icon_state = end_overlay
- if(END_STAGE)
- N.color = null
- N.icon_state = ""
- N.icon = 'icons/area/areas_misc.dmi'
- N.layer = initial(N.layer)
- N.plane = initial(N.plane)
- N.set_opacity(FALSE)
+ if(current_glow)
+ N.overlays -= current_glow
+ if(stage == END_STAGE)
+ N.color = null
+ N.icon_state = using_icon_state
+ N.icon = 'icons/area/areas_misc.dmi'
+ N.layer = initial(N.layer)
+ N.plane = initial(N.plane)
+ N.set_opacity(FALSE)
+ else
+ N.layer = overlay_layer
+ N.plane = overlay_plane
+ N.icon = 'icons/effects/weather_effects.dmi'
+ N.icon_state = using_icon_state
+ N.color = weather_color
+ if(use_glow)
+ N.overlays += glow_overlay
+
+ current_glow = glow_overlay
+
diff --git a/code/datums/weather/weather_types/floor_is_lava.dm b/code/datums/weather/weather_types/floor_is_lava.dm
index 1ee58c68fa669..90a8d6fd55c31 100644
--- a/code/datums/weather/weather_types/floor_is_lava.dm
+++ b/code/datums/weather/weather_types/floor_is_lava.dm
@@ -21,6 +21,10 @@
overlay_layer = ABOVE_OPEN_TURF_LAYER //Covers floors only
overlay_plane = FLOOR_PLANE
immunity_type = TRAIT_LAVA_IMMUNE
+ /// We don't draw on walls, so this ends up lookin weird
+ /// Can't really use like, the emissive system here because I am not about to make
+ /// all walls block emissive
+ use_glow = FALSE
/datum/weather/floor_is_lava/can_weather_act(mob/living/mob_to_check)
diff --git a/code/datums/wires/robot.dm b/code/datums/wires/robot.dm
index 944399f6d4b66..42ae3a18b3468 100644
--- a/code/datums/wires/robot.dm
+++ b/code/datums/wires/robot.dm
@@ -42,7 +42,7 @@
R.notify_ai(AI_NOTIFICATION_CYBORG_DISCONNECTED)
if(new_ai && (new_ai != R.connected_ai))
R.set_connected_ai(new_ai)
- log_combat(usr, R, "synced cyborg [R.connected_ai ? "from [ADMIN_LOOKUP(R.connected_ai)]": ""] to [ADMIN_LOOKUP(new_ai)]")
+ log_silicon("[key_name(usr)] synced [key_name(R)] [R.connected_ai ? "from [ADMIN_LOOKUP(R.connected_ai)]": ""] to [ADMIN_LOOKUP(new_ai)]")
if(R.shell)
R.undeploy() //If this borg is an AI shell, disconnect the controlling AI and assign ti to a new AI
R.notify_ai(AI_NOTIFICATION_AI_SHELL)
@@ -52,17 +52,17 @@
if(!QDELETED(R.builtInCamera) && !R.scrambledcodes)
R.builtInCamera.toggle_cam(usr, FALSE)
R.visible_message(span_notice("[R]'s camera lens focuses loudly."), span_notice("Your camera lens focuses loudly."))
- log_combat(usr, R, "toggled cyborg camera to [R.builtInCamera.status ? "on" : "off"] via pulse")
+ log_silicon("[key_name(usr)] toggled [key_name(R)]'s camera to [R.builtInCamera.status ? "on" : "off"] via pulse")
if(WIRE_LAWSYNC) // Forces a law update if possible.
if(R.lawupdate)
R.visible_message(span_notice("[R] gently chimes."), span_notice("LawSync protocol engaged."))
- log_combat(usr, R, "forcibly synced cyborg laws via pulse")
+ log_silicon("[key_name(usr)] forcibly synced [key_name(R)]'s laws via pulse")
// TODO, log the laws they gained here
R.lawsync()
R.show_laws()
if(WIRE_LOCKDOWN)
R.SetLockdown(!R.lockcharge) // Toggle
- log_combat(usr, R, "[!R.lockcharge ? "locked down" : "released"] via pulse")
+ log_silicon("[key_name(usr)] [!R.lockcharge ? "locked down" : "released"] [key_name(R)] via pulse")
if(WIRE_RESET_MODEL)
if(R.has_model())
@@ -74,7 +74,7 @@
if(WIRE_AI) // Cut the AI wire to reset AI control.
if(!mend)
R.notify_ai(AI_NOTIFICATION_CYBORG_DISCONNECTED)
- log_combat(usr, R, "cut AI wire on cyborg[R.connected_ai ? " and disconnected from [ADMIN_LOOKUP(R.connected_ai)]": ""]")
+ log_silicon("[key_name(usr)] cut AI wire on [key_name(R)][R.connected_ai ? " and disconnected from [ADMIN_LOOKUP(R.connected_ai)]": ""]")
if(R.shell)
R.undeploy()
R.set_connected_ai(null)
@@ -83,26 +83,26 @@
if(mend)
if(!R.emagged)
R.lawupdate = TRUE
- log_combat(usr, R, "enabled lawsync via wire")
+ log_silicon("[key_name(usr)] enabled [key_name(R)]'s lawsync via wire")
else if(!R.deployed) //AI shells must always have the same laws as the AI
R.lawupdate = FALSE
- log_combat(usr, R, "disabled lawsync via wire")
- R.logevent("Lawsync Module fault [mend?"cleared":"detected"]")
+ log_silicon("[key_name(usr)] disabled [key_name(R)]'s lawsync via wire")
+ R.logevent("Lawsync Module fault [mend ? "cleared" : "detected"]")
if (WIRE_CAMERA) // Disable the camera.
if(!QDELETED(R.builtInCamera) && !R.scrambledcodes)
R.builtInCamera.status = mend
R.builtInCamera.toggle_cam(usr, 0)
R.visible_message(span_notice("[R]'s camera lens focuses loudly."), span_notice("Your camera lens focuses loudly."))
R.logevent("Camera Module fault [mend?"cleared":"detected"]")
- log_combat(usr, R, "[mend ? "enabled" : "disabled"] cyborg camera via wire")
+ log_silicon("[key_name(usr)] [mend ? "enabled" : "disabled"] [key_name(R)]'s camera via wire")
if(WIRE_LOCKDOWN) // Simple lockdown.
R.SetLockdown(!mend)
R.logevent("Motor Controller fault [mend?"cleared":"detected"]")
- log_combat(usr, R, "[!R.lockcharge ? "locked down" : "released"] via wire")
+ log_silicon("[key_name(usr)] [!R.lockcharge ? "locked down" : "released"] [key_name(R)] via wire")
if(WIRE_RESET_MODEL)
if(R.has_model() && !mend)
R.ResetModel()
- log_combat(usr, R, "reset the cyborg module via wire")
+ log_silicon("[key_name(usr)] reset [key_name(R)]'s module via wire")
/datum/wires/robot/can_reveal_wires(mob/user)
if(HAS_TRAIT(user, TRAIT_KNOW_CYBORG_WIRES))
diff --git a/code/datums/world_topic.dm b/code/datums/world_topic.dm
index b6e96dd4b16b6..bb22827a8e5da 100644
--- a/code/datums/world_topic.dm
+++ b/code/datums/world_topic.dm
@@ -213,7 +213,7 @@
if(key_valid)
.["active_players"] = get_active_player_count()
- .["security_level"] = get_security_level()
+ .["security_level"] = SSsecurity_level.get_current_level_as_text()
.["round_duration"] = SSticker ? round((world.time-SSticker.round_start_time)/10) : 0
// Amount of world's ticks in seconds, useful for calculating round duration
diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm
index 5970fc4851cd5..67c6deb77a1f5 100644
--- a/code/game/area/areas.dm
+++ b/code/game/area/areas.dm
@@ -403,7 +403,8 @@ GLOBAL_LIST_EMPTY(teleportlocs)
/area/Entered(atom/movable/arrived, area/old_area)
set waitfor = FALSE
SEND_SIGNAL(src, COMSIG_AREA_ENTERED, arrived, old_area)
- if(!LAZYACCESS(arrived.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE))
+
+ if(!arrived.important_recursive_contents?[RECURSIVE_CONTENTS_AREA_SENSITIVE])
return
for(var/atom/movable/recipient as anything in arrived.important_recursive_contents[RECURSIVE_CONTENTS_AREA_SENSITIVE])
SEND_SIGNAL(recipient, COMSIG_ENTER_AREA, src)
@@ -419,19 +420,6 @@ GLOBAL_LIST_EMPTY(teleportlocs)
if(L.client?.prefs.toggles & SOUND_SHIP_AMBIENCE)
SEND_SOUND(L, sound('sound/ambience/shipambience.ogg', repeat = 1, wait = 0, volume = 35, channel = CHANNEL_BUZZ))
-
-
-///Divides total beauty in the room by roomsize to allow us to get an average beauty per tile.
-/area/proc/update_beauty()
- if(!areasize)
- beauty = 0
- return FALSE
- if(areasize >= beauty_threshold)
- beauty = 0
- return FALSE //Too big
- beauty = totalbeauty / areasize
-
-
/**
* Called when an atom exits an area
*
@@ -439,11 +427,21 @@ GLOBAL_LIST_EMPTY(teleportlocs)
*/
/area/Exited(atom/movable/gone, direction)
SEND_SIGNAL(src, COMSIG_AREA_EXITED, gone, direction)
- if(!LAZYACCESS(gone.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE))
+
+ if(!gone.important_recursive_contents?[RECURSIVE_CONTENTS_AREA_SENSITIVE])
return
for(var/atom/movable/recipient as anything in gone.important_recursive_contents[RECURSIVE_CONTENTS_AREA_SENSITIVE])
SEND_SIGNAL(recipient, COMSIG_EXIT_AREA, src)
+///Divides total beauty in the room by roomsize to allow us to get an average beauty per tile.
+/area/proc/update_beauty()
+ if(!areasize)
+ beauty = 0
+ return FALSE
+ if(areasize >= beauty_threshold)
+ beauty = 0
+ return FALSE //Too big
+ beauty = totalbeauty / areasize
/**
* Setup an area (with the given name)
diff --git a/code/game/area/areas/mining.dm b/code/game/area/areas/mining.dm
index 57ed48b9ce279..d2dfa56a329c4 100644
--- a/code/game/area/areas/mining.dm
+++ b/code/game/area/areas/mining.dm
@@ -227,6 +227,10 @@
/area/icemoon/underground/unexplored/rivers/deep
map_generator = /datum/map_generator/cave_generator/icemoon/deep
+/area/icemoon/underground/unexplored/rivers/deep/shoreline //use this for when you don't want mobs to spawn in certain areas in the "deep" portions. Think adjacent to rivers or station structures.
+ icon_state = "shore"
+ area_flags = UNIQUE_AREA | CAVES_ALLOWED | FLORA_ALLOWED | NO_ALERTS
+
/area/icemoon/underground/explored // ruins can't spawn here
name = "Icemoon Underground"
area_flags = UNIQUE_AREA | NO_ALERTS
diff --git a/code/game/area/areas/shuttles.dm b/code/game/area/areas/shuttles.dm
index cefbb4c1945aa..cb8dd26894ec1 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/area/areas/station.dm b/code/game/area/areas/station.dm
index 803aec7a8c5f2..3ae23bfb262a7 100644
--- a/code/game/area/areas/station.dm
+++ b/code/game/area/areas/station.dm
@@ -1120,7 +1120,7 @@
/area/station/security/processing
name = "\improper Labor Shuttle Dock"
- icon_state = "sec_processing"
+ icon_state = "sec_labor_processing"
/area/station/security/processing/cremation
name = "\improper Security Crematorium"
@@ -1301,44 +1301,43 @@
name = "\improper Cytology Lab"
icon_state = "cytology"
-/area/station/science/storage
- name = "Ordnance Storage"
+// Use this for the main lab. If test equipment, storage, etc is also present use this one too.
+/area/station/science/ordnance
+ name = "\improper Ordnance Lab"
+ icon_state = "ord_main"
+
+/area/station/science/ordnance/office
+ name = "\improper Ordnance Office"
+ icon_state = "ord_office"
+
+/area/station/science/ordnance/storage
+ name = "\improper Ordnance Storage"
icon_state = "ord_storage"
-/area/station/science/test_area
- name = "\improper Ordnance Test Area"
- icon_state = "ord_test"
+/area/station/science/ordnance/burnchamber
+ name = "\improper Ordnance Burn Chamber"
+ icon_state = "ord_burn"
area_flags = BLOBS_ALLOWED | UNIQUE_AREA | CULT_PERMITTED
-/area/station/science/mixing
- name = "\improper Ordnance Mixing Lab"
- icon_state = "ord_mix"
-
-/area/station/science/mixing/chamber
- name = "\improper Ordnance Mixing Chamber"
- icon_state = "ord_mix_chamber"
+/area/station/science/ordnance/freezerchamber
+ name = "\improper Ordnance Freezer Chamber"
+ icon_state = "ord_freeze"
area_flags = BLOBS_ALLOWED | UNIQUE_AREA | CULT_PERMITTED
-/area/station/science/mixing/hallway
- name = "\improper Ordnance Mixing Hallway"
- icon_state = "ord_mix_hallway"
+// Room for equipments and such
+/area/station/science/ordnance/testlab
+ name = "\improper Ordnance Testing Lab"
+ icon_state = "ord_test"
+ area_flags = BLOBS_ALLOWED | UNIQUE_AREA | CULT_PERMITTED
-/area/station/science/mixing/launch
- name = "\improper Ordnance Mixing Launch Site"
- icon_state = "ord_mix_launch"
+/area/station/science/ordnance/bomb
+ name = "\improper Ordnance Bomb Site"
+ icon_state = "ord_boom"
/area/station/science/genetics
name = "\improper Genetics Lab"
icon_state = "geneticssci"
-/area/station/science/misc_lab
- name = "\improper Testing Lab"
- icon_state = "ord_misc"
-
-/area/station/science/misc_lab/range
- name = "\improper Research Testing Range"
- icon_state = "ord_range"
-
/area/station/science/server
name = "\improper Research Division Server Room"
icon_state = "server"
@@ -1347,6 +1346,11 @@
name = "\improper Experimentation Lab"
icon_state = "exp_lab"
+// Useless room
+/area/station/science/auxlab
+ name = "\improper Auxillary Lab"
+ icon_state = "aux_lab"
+
/area/station/science/robotics
name = "Robotics"
icon_state = "robotics"
diff --git a/code/game/atoms.dm b/code/game/atoms.dm
index ee9ff67cd46a6..5ef8b69bb0134 100644
--- a/code/game/atoms.dm
+++ b/code/game/atoms.dm
@@ -602,10 +602,10 @@
* [COMSIG_ATOM_GET_EXAMINE_NAME] signal
*/
/atom/proc/get_examine_name(mob/user)
- . = "\a [src]"
+ . = "\a [src]"
var/list/override = list(gender == PLURAL ? "some" : "a", " ", "[name]")
if(article)
- . = "[article] [src]"
+ . = "[article] [src]"
override[EXAMINE_POSITION_ARTICLE] = article
if(SEND_SIGNAL(src, COMSIG_ATOM_GET_EXAMINE_NAME, user, override) & COMPONENT_EXNAME_CHANGED)
. = override.Join("")
@@ -637,7 +637,11 @@
* Produces a signal [COMSIG_PARENT_EXAMINE]
*/
/atom/proc/examine(mob/user)
- . = list("[get_examine_string(user, TRUE)].")
+ var/examine_string = get_examine_string(user, thats = TRUE)
+ if(examine_string)
+ . = list("[examine_string].")
+ else
+ . = list()
. += get_name_chaser(user)
if(desc)
@@ -654,7 +658,7 @@
if(length(reagents.reagent_list))
if(user.can_see_reagents()) //Show each individual reagent
for(var/datum/reagent/current_reagent as anything in reagents.reagent_list)
- . += "[round(current_reagent.volume, 0.01)] units of [current_reagent.name]"
+ . += "• [round(current_reagent.volume, 0.01)] units of [current_reagent.name]"
if(reagents.is_reacting)
. += span_warning("It is currently reacting!")
. += span_notice("The solution's pH is [round(reagents.ph, 0.01)] and has a temperature of [reagents.chem_temp]K.")
@@ -1781,7 +1785,9 @@
* Returns true if this atom has gravity for the passed in turf
*
* Sends signals [COMSIG_ATOM_HAS_GRAVITY] and [COMSIG_TURF_HAS_GRAVITY], both can force gravity with
- * the forced gravity var
+ * the forced gravity var.
+ *
+ * micro-optimized to hell because this proc is very hot, being called several times per movement every movement.
*
* Gravity situations:
* * No gravity if you're not in a turf
@@ -1792,39 +1798,28 @@
* * otherwise no gravity
*/
/atom/proc/has_gravity(turf/gravity_turf)
- if(!gravity_turf || !isturf(gravity_turf))
+ if(!isturf(gravity_turf))
gravity_turf = get_turf(src)
- if(!gravity_turf)
- return 0
+ if(!gravity_turf)//no gravity in nullspace
+ return 0
+
+ //the list isnt created every time as this proc is very hot, its only accessed if anything is actually listening to the signal too
+ var/static/list/forced_gravity = list()
+ if(SEND_SIGNAL(src, COMSIG_ATOM_HAS_GRAVITY, gravity_turf, forced_gravity))
+ if(!length(forced_gravity))
+ SEND_SIGNAL(gravity_turf, COMSIG_TURF_HAS_GRAVITY, src, forced_gravity)
- var/list/forced_gravity = list()
- SEND_SIGNAL(src, COMSIG_ATOM_HAS_GRAVITY, gravity_turf, forced_gravity)
- if(!forced_gravity.len)
- SEND_SIGNAL(gravity_turf, COMSIG_TURF_HAS_GRAVITY, src, forced_gravity)
- if(forced_gravity.len)
- var/max_grav = forced_gravity[1]
- for(var/i in forced_gravity)
+ var/max_grav = 0
+ for(var/i in forced_gravity)//our gravity is the strongest return forced gravity we get
max_grav = max(max_grav, i)
+ forced_gravity.Cut()
+ //cut so we can reuse the list, this is ok since forced gravity movers are exceedingly rare compared to all other movement
return max_grav
- if(isspaceturf(gravity_turf)) // Turf never has gravity
- return 0
- if(istype(gravity_turf, /turf/open/openspace)) //openspace in a space area doesn't get gravity
- if(istype(get_area(gravity_turf), /area/space))
- return 0
+ var/area/turf_area = gravity_turf.loc
- var/area/turf_area = get_area(gravity_turf)
- if(turf_area.has_gravity) // Areas which always has gravity
- return turf_area.has_gravity
- else
- // There's a gravity generator on our z level
- if(GLOB.gravity_generators["[gravity_turf.z]"])
- var/max_grav = 0
- for(var/obj/machinery/gravity_generator/main/main_grav_gen as anything in GLOB.gravity_generators["[gravity_turf.z]"])
- max_grav = max(main_grav_gen.setting,max_grav)
- return max_grav
- return SSmapping.level_trait(gravity_turf.z, ZTRAIT_GRAVITY)
+ return !gravity_turf.force_no_gravity && (SSmapping.gravity_by_z_level["[gravity_turf.z]"] || turf_area.has_gravity)
/**
* Causes effects when the atom gets hit by a rust effect from heretics
diff --git a/code/game/atoms_movable.dm b/code/game/atoms_movable.dm
index ce8539a8dd09d..ffc89ecbe6939 100644
--- a/code/game/atoms_movable.dm
+++ b/code/game/atoms_movable.dm
@@ -3,8 +3,6 @@
glide_size = 8
appearance_flags = TILE_BOUND|PIXEL_SCALE|LONG_GLIDE
- ///how many times a this movable had movement procs called on it since Moved() was last called
- var/move_stacks = 0
var/last_move = null
var/anchored = FALSE
var/move_resist = MOVE_RESIST_DEFAULT
@@ -36,8 +34,10 @@
var/pass_flags = NONE
/// If false makes [CanPass][/atom/proc/CanPass] call [CanPassThrough][/atom/movable/proc/CanPassThrough] on this type instead of using default behaviour
var/generic_canpass = TRUE
- var/moving_diagonally = 0 //0: not doing a diagonal move. 1 and 2: doing the first/second step of the diagonal move
- var/atom/movable/moving_from_pull //attempt to resume grab after moving instead of before.
+ ///0: not doing a diagonal move. 1 and 2: doing the first/second step of the diagonal move
+ var/moving_diagonally = 0
+ ///attempt to resume grab after moving instead of before.
+ var/atom/movable/moving_from_pull
///Holds information about any movement loops currently running/waiting to run on the movable. Lazy, will be null if nothing's going on
var/datum/movement_packet/move_packet
var/datum/forced_movement/force_moving = null //handled soley by forced_movement.dm
@@ -441,8 +441,7 @@
SEND_SIGNAL(src, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, target)
glide_size = target
- for(var/m in buckled_mobs)
- var/mob/buckled_mob = m
+ for(var/mob/buckled_mob as anything in buckled_mobs)
buckled_mob.set_glide_size(target)
/**
@@ -452,9 +451,9 @@
*/
/atom/movable/proc/abstract_move(atom/new_loc)
var/atom/old_loc = loc
- move_stacks++
+ var/direction = get_dir(old_loc, new_loc)
loc = new_loc
- Moved(old_loc)
+ Moved(old_loc, direction)
////////////////////////////////////////
// Here's where we rewrite how byond handles movement except slightly different
@@ -468,7 +467,7 @@
if(!direction)
direction = get_dir(src, newloc)
- if(set_dir_on_move)
+ if(set_dir_on_move && dir != direction)
setDir(direction)
var/is_multi_tile_object = bound_width > 32 || bound_height > 32
@@ -508,7 +507,6 @@
var/atom/oldloc = loc
var/area/oldarea = get_area(oldloc)
var/area/newarea = get_area(newloc)
- move_stacks++
loc = newloc
@@ -543,7 +541,7 @@
return FALSE
var/atom/oldloc = loc
//Early override for some cases like diagonal movement
- if(glide_size_override)
+ if(glide_size_override && glide_size != glide_size_override)
set_glide_size(glide_size_override)
if(loc != newloc)
@@ -597,10 +595,6 @@
if(moving_diagonally == SECOND_DIAG_STEP)
if(!. && set_dir_on_move)
setDir(first_step_dir)
- else if (!inertia_moving)
- newtonian_move(direct)
- if(client_mobs_in_contents) // We're done moving, update our parallax now
- update_parallax_contents()
moving_diagonally = 0
return
@@ -631,12 +625,12 @@
//glide_size strangely enough can change mid movement animation and update correctly while the animation is playing
//This means that if you don't override it late like this, it will just be set back by the movement update that's called when you move turfs.
- if(glide_size_override)
+ if(glide_size_override && glide_size != glide_size_override)
set_glide_size(glide_size_override)
last_move = direct
- if(set_dir_on_move)
+ if(set_dir_on_move && dir != direct)
setDir(direct)
if(. && has_buckled_mobs() && !handle_buckled_mob_movement(loc, direct, glide_size_override)) //movement failed due to buckled mob(s)
. = FALSE
@@ -661,11 +655,12 @@
* * movement_dir is the direction the movement took place. Can be NONE if it was some sort of teleport.
* * The forced flag indicates whether this was a forced move, which skips many checks of regular movement.
* * The old_locs is an optional argument, in case the moved movable was present in multiple locations before the movement.
+ * * momentum_change represents whether this movement is due to a "new" force if TRUE or an already "existing" force if FALSE
**/
-/atom/movable/proc/Moved(atom/old_loc, movement_dir, forced = FALSE, list/old_locs)
+/atom/movable/proc/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
SHOULD_CALL_PARENT(TRUE)
- if (!inertia_moving)
+ if (!inertia_moving && momentum_change)
newtonian_move(movement_dir)
// If we ain't moving diagonally right now, update our parallax
// We don't do this all the time because diag movements should trigger one call to this, not two
@@ -673,14 +668,12 @@
if (!moving_diagonally && client_mobs_in_contents)
update_parallax_contents()
- move_stacks--
- if(move_stacks > 0) //we want only the first Moved() call in the stack to send this signal, all the other ones have an incorrect old_loc
- return
- if(move_stacks < 0)
- stack_trace("move_stacks is negative in Moved()!")
- move_stacks = 0 //setting it to 0 so that we dont get every movable with negative move_stacks runtiming on every movement
+ SEND_SIGNAL(src, COMSIG_MOVABLE_MOVED, old_loc, movement_dir, forced, old_locs, momentum_change)
- SEND_SIGNAL(src, COMSIG_MOVABLE_MOVED, old_loc, movement_dir, forced, old_locs)
+ if(old_loc)
+ SEND_SIGNAL(old_loc, COMSIG_ATOM_ABSTRACT_EXITED, src, movement_dir)
+ if(loc)
+ SEND_SIGNAL(loc, COMSIG_ATOM_ABSTRACT_ENTERED, src, old_loc, old_locs)
var/turf/old_turf = get_turf(old_loc)
var/turf/new_turf = get_turf(src)
@@ -843,7 +836,6 @@
///allows this movable to know when it has "entered" another area no matter how many movable atoms its stuffed into, uses important_recursive_contents
/atom/movable/proc/become_area_sensitive(trait_source = TRAIT_GENERIC)
if(!HAS_TRAIT(src, TRAIT_AREA_SENSITIVE))
- //RegisterSignal(src, SIGNAL_REMOVETRAIT(TRAIT_AREA_SENSITIVE), .proc/on_area_sensitive_trait_loss)
for(var/atom/movable/location as anything in get_nested_locs(src) + src)
LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_AREA_SENSITIVE, src)
ADD_TRAIT(src, TRAIT_AREA_SENSITIVE, trait_source)
@@ -887,6 +879,24 @@
ASSOC_UNSETEMPTY(recursive_contents, RECURSIVE_CONTENTS_CLIENT_MOBS)
UNSETEMPTY(movable_loc.important_recursive_contents)
+///called when this movable becomes the parent of a storage component that is currently being viewed by a player. uses important_recursive_contents
+/atom/movable/proc/become_active_storage(datum/component/storage/component_source)
+ if(!HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE))
+ for(var/atom/movable/location as anything in get_nested_locs(src) + src)
+ LAZYADDASSOCLIST(location.important_recursive_contents, RECURSIVE_CONTENTS_ACTIVE_STORAGE, src)
+ ADD_TRAIT(src, TRAIT_ACTIVE_STORAGE, component_source)
+
+///called when this movable's storage component is no longer viewed by any players, unsets important_recursive_contents
+/atom/movable/proc/lose_active_storage(datum/component/storage/component_source)
+ if(!HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE))
+ return
+ REMOVE_TRAIT(src, TRAIT_ACTIVE_STORAGE, component_source)
+ if(HAS_TRAIT(src, TRAIT_ACTIVE_STORAGE))
+ return
+
+ for(var/atom/movable/location as anything in get_nested_locs(src) + src)
+ LAZYREMOVEASSOC(location.important_recursive_contents, RECURSIVE_CONTENTS_ACTIVE_STORAGE, src)
+
///Sets the anchored var and returns if it was sucessfully changed or not.
/atom/movable/proc/set_anchored(anchorvalue)
SHOULD_CALL_PARENT(TRUE)
@@ -917,12 +927,13 @@
/atom/movable/proc/doMove(atom/destination)
. = FALSE
- move_stacks++
var/atom/oldloc = loc
+ var/is_multi_tile = bound_width > world.icon_size || bound_height > world.icon_size
if(destination)
///zMove already handles whether a pull from another movable should be broken.
if(pulledby && !currently_z_moving)
pulledby.stop_pulling()
+
var/same_loc = oldloc == destination
var/area/old_area = get_area(oldloc)
var/area/destarea = get_area(destination)
@@ -933,23 +944,49 @@
loc = destination
if(!same_loc)
- if(oldloc)
- oldloc.Exited(src, movement_dir)
+ if(is_multi_tile && isturf(destination))
+ var/list/new_locs = block(
+ destination,
+ locate(
+ min(world.maxx, destination.x + ROUND_UP(bound_width / 32)),
+ min(world.maxy, destination.y + ROUND_UP(bound_height / 32)),
+ destination.z
+ )
+ )
if(old_area && old_area != destarea)
old_area.Exited(src, movement_dir)
- destination.Entered(src, oldloc)
- if(destarea && old_area != destarea)
- destarea.Entered(src, old_area)
+ for(var/atom/left_loc as anything in locs - new_locs)
+ left_loc.Exited(src, movement_dir)
+
+ for(var/atom/entering_loc as anything in new_locs - locs)
+ entering_loc.Entered(src, movement_dir)
+
+ if(old_area && old_area != destarea)
+ destarea.Entered(src, movement_dir)
+ else
+ if(oldloc)
+ oldloc.Exited(src, movement_dir)
+ if(old_area && old_area != destarea)
+ old_area.Exited(src, movement_dir)
+ destination.Entered(src, oldloc)
+ if(destarea && old_area != destarea)
+ destarea.Entered(src, old_area)
. = TRUE
//If no destination, move the atom into nullspace (don't do this unless you know what you're doing)
else
. = TRUE
- loc = null
+
if (oldloc)
+ loc = null
var/area/old_area = get_area(oldloc)
- oldloc.Exited(src, NONE)
+ if(is_multi_tile && isturf(oldloc))
+ for(var/atom/old_loc as anything in locs)
+ old_loc.Exited(src, NONE)
+ else
+ oldloc.Exited(src, NONE)
+
if(old_area)
old_area.Exited(src, NONE)
@@ -975,7 +1012,7 @@
* Called whenever an object moves and by mobs when they attempt to move themselves through space
* And when an object or action applies a force on src, see [newtonian_move][/atom/movable/proc/newtonian_move]
*
- * Return 0 to have src start/keep drifting in a no-grav area and 1 to stop/not start drifting
+ * Return FALSE to have src start/keep drifting in a no-grav area and TRUE to stop/not start drifting
*
* Mobs should return 1 if they should be able to move of their own volition, see [/client/proc/Move]
*
@@ -984,10 +1021,10 @@
* * continuous_move - If this check is coming from something in the context of already drifting
*/
/atom/movable/proc/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
- if(SEND_SIGNAL(src, COMSIG_MOVABLE_SPACEMOVE, movement_dir, continuous_move) & COMSIG_MOVABLE_STOP_SPACEMOVE)
+ if(has_gravity())
return TRUE
- if(has_gravity(src))
+ if(SEND_SIGNAL(src, COMSIG_MOVABLE_SPACEMOVE, movement_dir, continuous_move) & COMSIG_MOVABLE_STOP_SPACEMOVE)
return TRUE
if(pulledby && (pulledby.pulledby != src || moving_from_pull))
@@ -1393,7 +1430,6 @@
log_admin("[key_name(usr)] has added deadchat control to [src]")
message_admins(span_notice("[key_name(usr)] has added deadchat control to [src]"))
-
/**
* A wrapper for setDir that should only be able to fail by living mobs.
*
diff --git a/code/game/gamemodes/dynamic/dynamic.dm b/code/game/gamemodes/dynamic/dynamic.dm
index b05802b536269..b085c587bad4c 100644
--- a/code/game/gamemodes/dynamic/dynamic.dm
+++ b/code/game/gamemodes/dynamic/dynamic.dm
@@ -100,7 +100,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// Basically, if this is set to 5, then for every 5 threat, one midround roll will be added.
/// The equation this is used in rounds up, meaning that if this is set to 5, and you have 6
/// threat, then you will get 2 midround rolls.
- var/threat_per_midround_roll = 6.5
+ var/threat_per_midround_roll = 7
/// A number between -5 and +5.
/// A negative value will give a more peaceful round and
@@ -157,10 +157,10 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
/// The maximum threat that can roll with *zero* players.
/// As the number of players approaches `low_pop_player_threshold`, the maximum
/// threat level will increase.
- /// For example, if `low_pop_minimum_threat` is 50, `low_pop_player_threshold` is 20,
+ /// For example, if `low_pop_maximum_threat` is 50, `low_pop_player_threshold` is 20,
/// and the number of readied players is 10, then the highest threat that can roll is
/// lerp(50, 100, 10 / 20), AKA 75.
- var/low_pop_minimum_threat = 50
+ var/low_pop_maximum_threat = 40
/// The chance for latejoins to roll when ready
var/latejoin_roll_chance = 50
@@ -340,8 +340,8 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
priority_announce("Thanks to the tireless efforts of our security and intelligence divisions, there are currently no credible threats to [station_name()]. All station construction projects have been authorized. Have a secure shift!", "Security Report", SSstation.announcer.get_rand_report_sound())
else
priority_announce("A summary has been copied and printed to all communications consoles.", "Security level elevated.", ANNOUNCER_INTERCEPT)
- if(SSsecurity_level.current_level < SEC_LEVEL_BLUE)
- set_security_level(SEC_LEVEL_BLUE)
+ if(SSsecurity_level.get_current_level_as_number() < SEC_LEVEL_BLUE)
+ SSsecurity_level.set_level(SEC_LEVEL_BLUE)
/datum/game_mode/dynamic/proc/show_threatlog(mob/admin)
if(!SSticker.HasRoundStarted())
@@ -367,7 +367,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
threat_level = clamp(round(lorentz_to_amount(relative_threat), 0.1), 0, max_threat_level)
if (SSticker.totalPlayersReady < low_pop_player_threshold)
- threat_level = min(threat_level, LERP(low_pop_minimum_threat, max_threat_level, SSticker.totalPlayersReady / low_pop_player_threshold))
+ threat_level = min(threat_level, LERP(low_pop_maximum_threat, max_threat_level, SSticker.totalPlayersReady / low_pop_player_threshold))
peaceful_percentage = round(LORENTZ_CUMULATIVE_DISTRIBUTION(relative_threat, threat_curve_centre, threat_curve_width), 0.01)*100
@@ -592,7 +592,7 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
return FALSE
/// An experimental proc to allow admins to call rules on the fly or have rules call other rules.
-/datum/game_mode/dynamic/proc/picking_specific_rule(ruletype, forced = FALSE)
+/datum/game_mode/dynamic/proc/picking_specific_rule(ruletype, forced = FALSE, ignore_cost = FALSE)
var/datum/dynamic_ruleset/midround/new_rule
if(ispath(ruletype))
new_rule = new ruletype() // You should only use it to call midround rules though.
@@ -618,10 +618,11 @@ GLOBAL_VAR_INIT(dynamic_forced_threat_level, -1)
return FALSE
var/population = GLOB.alive_player_list.len
- if((new_rule.acceptable(population, threat_level) && new_rule.cost <= mid_round_budget) || forced)
+ if((new_rule.acceptable(population, threat_level) && (ignore_cost || new_rule.cost <= mid_round_budget)) || forced)
new_rule.trim_candidates()
if (new_rule.ready(forced))
- spend_midround_budget(new_rule.cost, threat_log, "[worldtime2text()]: Forced rule [new_rule.name]")
+ if (!ignore_cost)
+ spend_midround_budget(new_rule.cost, threat_log, "[worldtime2text()]: Forced rule [new_rule.name]")
new_rule.pre_execute(population)
if (new_rule.execute()) // This should never fail since ready() returned 1
if(new_rule.flags & HIGH_IMPACT_RULESET)
diff --git a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
index 96fb0d68413ac..0d1a4f8646c94 100644
--- a/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
+++ b/code/game/gamemodes/dynamic/dynamic_midround_rolling.dm
@@ -26,11 +26,12 @@
if (EMERGENCY_PAST_POINT_OF_NO_RETURN)
return
+ var/spawn_heavy = prob(get_heavy_midround_injection_chance())
+
last_midround_injection_attempt = world.time
next_midround_injection = null
forced_injection = FALSE
- var/spawn_heavy = prob(get_heavy_midround_injection_chance())
dynamic_log("A midround ruleset is rolling, and will be [spawn_heavy ? "HEAVY" : "LIGHT"].")
random_event_hijacked = HIJACKED_NOTHING
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
index 2e682d7ef7227..5f23fc74f3290 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_latejoin.dm
@@ -90,6 +90,7 @@
JOB_HEAD_OF_PERSONNEL,
JOB_HEAD_OF_SECURITY,
JOB_PRISONER,
+ JOB_QUARTERMASTER,
JOB_RESEARCH_DIRECTOR,
JOB_SECURITY_OFFICER,
JOB_WARDEN,
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index 10edebaefcb56..1b20add4b8e07 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -365,7 +365,7 @@
if(prob(MALF_ION_PROB))
priority_announce("Ion storm detected near the station. Please check all AI-controlled equipment for errors.", "Anomaly Alert", ANNOUNCER_IONSTORM)
if(prob(REPLACE_LAW_WITH_ION_PROB))
- new_malf_ai.replace_random_law(generate_ion_law(), list(LAW_INHERENT, LAW_SUPPLIED, LAW_ION))
+ new_malf_ai.replace_random_law(generate_ion_law(), list(LAW_INHERENT, LAW_SUPPLIED, LAW_ION), LAW_ION)
else
new_malf_ai.add_ion_law(generate_ion_law())
return TRUE
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
index d632a1b56bc3e..1d46d68bce6c0 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_roundstart.dm
@@ -27,7 +27,7 @@
cost = 8 // Avoid raising traitor threat above this, as it is the default low cost ruleset.
scaling_cost = 9
requirements = list(8,8,8,8,8,8,8,8,8,8)
- antag_cap = list("denominator" = 24)
+ antag_cap = list("denominator" = 38)
var/autotraitor_cooldown = (15 MINUTES)
/datum/dynamic_ruleset/roundstart/traitor/pre_execute(population)
@@ -477,6 +477,7 @@
JOB_HEAD_OF_PERSONNEL,
JOB_HEAD_OF_SECURITY,
JOB_PRISONER,
+ JOB_QUARTERMASTER,
JOB_RESEARCH_DIRECTOR,
JOB_SECURITY_OFFICER,
JOB_WARDEN,
diff --git a/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm b/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm
new file mode 100644
index 0000000000000..a74e03739ea1d
--- /dev/null
+++ b/code/game/gamemodes/dynamic/dynamic_unfavorable_situation.dm
@@ -0,0 +1,57 @@
+/// An easy interface to make...*waves hands* bad things happen.
+/// This is used for impactful events like traitors hacking and creating more threat, or a revolutions victory.
+/// It tries to spawn a heavy midround if possible, otherwise it will trigger a "bad" random event after a short period.
+/// Calling this function will not use up any threat.
+/datum/game_mode/dynamic/proc/unfavorable_situation()
+ SHOULD_NOT_SLEEP(TRUE)
+
+ INVOKE_ASYNC(src, .proc/_unfavorable_situation)
+
+/datum/game_mode/dynamic/proc/_unfavorable_situation()
+ var/static/list/unfavorable_random_events = list(
+ /datum/round_event_control/immovable_rod,
+ /datum/round_event_control/meteor_wave,
+ /datum/round_event_control/portal_storm_syndicate,
+ )
+
+ var/list/possible_heavies = list()
+
+ // Ignored factors: threat cost, minimum round time
+ for (var/datum/dynamic_ruleset/midround/ruleset as anything in midround_rules)
+ if (ruleset.midround_ruleset_style != MIDROUND_RULESET_STYLE_HEAVY)
+ continue
+
+ if (ruleset.weight == 0)
+ continue
+
+ if (ruleset.cost > max_threat_level)
+ continue
+
+ if (!ruleset.acceptable(GLOB.alive_player_list.len, threat_level))
+ continue
+
+ if (ruleset.minimum_round_time > world.time - SSticker.round_start_time)
+ continue
+
+ if(istype(ruleset, /datum/dynamic_ruleset/midround/from_ghosts) && !(GLOB.ghost_role_flags & GHOSTROLE_MIDROUND_EVENT))
+ continue
+
+ ruleset.trim_candidates()
+
+ if (!ruleset.ready())
+ continue
+
+ possible_heavies[ruleset] = ruleset.get_weight()
+
+ if (possible_heavies.len == 0)
+ var/datum/round_event_control/round_event_control_type = pick(unfavorable_random_events)
+ var/delay = rand(20 SECONDS, 1 MINUTES)
+
+ dynamic_log("An unfavorable situation was requested, but no heavy rulesets could be drafted. Spawning [initial(round_event_control_type.name)] in [DisplayTimeText(delay)] instead.")
+
+ var/datum/round_event_control/round_event_control = new round_event_control_type
+ addtimer(CALLBACK(round_event_control, /datum/round_event_control.proc/runEvent), delay)
+ else
+ var/datum/dynamic_ruleset/midround/heavy_ruleset = pick_weight(possible_heavies)
+ dynamic_log("An unfavorable situation was requested, spawning [initial(heavy_ruleset.name)]")
+ picking_specific_rule(heavy_ruleset, forced = TRUE, ignore_cost = TRUE)
diff --git a/code/game/gamemodes/game_mode.dm b/code/game/gamemodes/game_mode.dm
index 470aa6f455bbe..388f56311a675 100644
--- a/code/game/gamemodes/game_mode.dm
+++ b/code/game/gamemodes/game_mode.dm
@@ -200,6 +200,8 @@
SSticker.news_report = STATION_EVACUATED
if(SSshuttle.emergency.is_hijacked())
SSticker.news_report = SHUTTLE_HIJACK
+ if(SSsupermatter_cascade.cascade_initiated)
+ SSticker.news_report = SUPERMATTER_CASCADE
/// Mode specific admin panel.
/datum/game_mode/proc/admin_panel()
diff --git a/code/game/gamemodes/objective.dm b/code/game/gamemodes/objective.dm
index 93e787e150319..33b95158b6417 100644
--- a/code/game/gamemodes/objective.dm
+++ b/code/game/gamemodes/objective.dm
@@ -157,30 +157,32 @@ GLOBAL_LIST(admin_objective_list) //Prefilled admin assignable objective list
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
-
-/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)
+ icon_icon = 'icons/obj/device.dmi'
+ button_icon_state = "beacon"
+
+/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"
@@ -875,12 +877,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/PDApainter.dm b/code/game/machinery/PDApainter.dm
index cca8d6cf79f80..cf172a269655f 100644
--- a/code/game/machinery/PDApainter.dm
+++ b/code/game/machinery/PDApainter.dm
@@ -377,3 +377,8 @@
/obj/machinery/pdapainter/engineering
name = "\improper Engineering PDA & ID Painter"
target_dept = REGION_ENGINEERING
+
+/// Supply departmental variant. Limited to PDAs defined in the SSid_access.sub_department_managers_tgui data structure.
+/obj/machinery/pdapainter/supply
+ name = "\improper Supply PDA & ID Painter"
+ target_dept = REGION_SUPPLY
diff --git a/code/game/machinery/_machinery.dm b/code/game/machinery/_machinery.dm
index 97b382e010db3..c57ba341b3b91 100644
--- a/code/game/machinery/_machinery.dm
+++ b/code/game/machinery/_machinery.dm
@@ -745,6 +745,7 @@
LAZYCLEARLIST(component_parts)
return ..()
+
/**
* Spawns a frame where this machine is. If the machine was not disassmbled, the
* frame is spawned damaged. If the frame couldn't exist on this turf, it's smashed
diff --git a/code/game/machinery/airlock_control.dm b/code/game/machinery/airlock_control.dm
index e8aab954059d9..d40539ab17b01 100644
--- a/code/game/machinery/airlock_control.dm
+++ b/code/game/machinery/airlock_control.dm
@@ -7,7 +7,6 @@
var/frequency
var/datum/radio_frequency/radio_connection
-
/obj/machinery/door/airlock/receive_signal(datum/signal/signal)
if(!signal)
return
@@ -50,7 +49,6 @@
send_status()
-
/obj/machinery/door/airlock/proc/send_status()
if(radio_connection)
var/datum/signal/signal = new(list(
@@ -61,25 +59,27 @@
))
radio_connection.post_signal(src, signal, range = AIRLOCK_CONTROL_RANGE, filter = RADIO_AIRLOCK)
-
/obj/machinery/door/airlock/open(surpress_send)
. = ..()
if(!surpress_send)
send_status()
-
/obj/machinery/door/airlock/close(surpress_send)
. = ..()
if(!surpress_send)
send_status()
-
/obj/machinery/door/airlock/proc/set_frequency(new_frequency)
SSradio.remove_object(src, frequency)
if(new_frequency)
frequency = new_frequency
radio_connection = SSradio.add_object(src, frequency, RADIO_AIRLOCK)
+/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 ..()
+
/obj/machinery/door/airlock/Destroy()
if(frequency)
SSradio.remove_object(src,frequency)
diff --git a/code/game/machinery/autolathe.dm b/code/game/machinery/autolathe.dm
index 8fdde994953fd..609b39ecc34a8 100644
--- a/code/game/machinery/autolathe.dm
+++ b/code/game/machinery/autolathe.dm
@@ -33,6 +33,7 @@
"Tools",
"Electronics",
"Construction",
+ "Material",
"T-Comm",
"Security",
"Machinery",
@@ -111,9 +112,9 @@
var/datum/component/material_container/mats = GetComponent(/datum/component/material_container)
for(var/datum/material/mat in D.materials)
max_multiplier = min(D.maxstack, round(mats.get_material_amount(mat)/D.materials[mat]))
- if (max_multiplier>10 && !disabled)
+ if (max_multiplier >= 10 && !disabled)
m10 = TRUE
- if (max_multiplier>25 && !disabled)
+ if (max_multiplier >= 25 && !disabled)
m25 = TRUE
else
if(!unbuildable)
diff --git a/code/game/machinery/computer/arcade/arcade.dm b/code/game/machinery/computer/arcade/arcade.dm
index 810bfd015aa83..8fa1a828f656d 100644
--- a/code/game/machinery/computer/arcade/arcade.dm
+++ b/code/game/machinery/computer/arcade/arcade.dm
@@ -56,6 +56,8 @@ GLOBAL_LIST_INIT(arcade_prize_pool, list(
/obj/item/toy/plush/moth = 2,
/obj/item/toy/plush/pkplush = 2,
/obj/item/toy/plush/rouny = 2,
+ /obj/item/toy/plush/abductor = 2,
+ /obj/item/toy/plush/abductor/agent = 2,
/obj/item/storage/belt/military/snack = 2,
/obj/item/toy/brokenradio = 2,
/obj/item/toy/braintoy = 2,
diff --git a/code/game/machinery/computer/arcade/orion_event.dm b/code/game/machinery/computer/arcade/orion_event.dm
index 07295d831475f..f9f893f9c2803 100644
--- a/code/game/machinery/computer/arcade/orion_event.dm
+++ b/code/game/machinery/computer/arcade/orion_event.dm
@@ -114,19 +114,19 @@
/datum/orion_event/electronic_part/emag_effect(obj/machinery/computer/arcade/orion_trail/game, mob/living/gamer)
playsound(game, 'sound/effects/empulse.ogg', 50, TRUE)
- game.visible_message(span_danger("[src] malfunctions, randomizing in-game stats!"))
+ game.visible_message(span_danger("[game] malfunctions, randomizing in-game stats!"))
var/oldfood = game.food
var/oldfuel = game.fuel
game.food = rand(10,80) / rand(1,2)
game.fuel = rand(10,60) / rand(1,2)
if(game.electronics)
- addtimer(CALLBACK(game, .proc/revert_random, game, oldfood, oldfuel), 1 SECONDS)
+ addtimer(CALLBACK(src, .proc/revert_random, game, oldfood, oldfuel), 1 SECONDS)
/datum/orion_event/electronic_part/proc/revert_random(obj/machinery/computer/arcade/orion_trail/game, oldfood, oldfuel)
if(oldfuel > game.fuel && oldfood > game.food)
- game.audible_message(span_danger("[src] lets out a somehow reassuring chime."))
+ game.audible_message(span_danger("[game] lets out a somehow reassuring chime."))
else if(oldfuel < game.fuel || oldfood < game.food)
- game.audible_message(span_danger("[src] lets out a somehow ominous chime."))
+ game.audible_message(span_danger("[game] lets out a somehow ominous chime."))
game.food = oldfood
game.fuel = oldfuel
playsound(game, 'sound/machines/chime.ogg', 50, TRUE)
@@ -158,20 +158,20 @@
/datum/orion_event/hull_part/emag_effect(obj/machinery/computer/arcade/orion_trail/game, mob/living/gamer)
if(prob(10+gamer_skill))
- game.say("Something slams into the floor around [src] - luckily, it didn't get through!")
+ game.say("Something slams into the floor around [game] - luckily, it didn't get through!")
playsound(game, 'sound/effects/bang.ogg', 50, TRUE)
return
playsound(game, 'sound/effects/bang.ogg', 100, TRUE)
- for(var/turf/open/floor/smashed in orange(1, src))
+ for(var/turf/open/floor/smashed in orange(1, game))
smashed.ScrapeAway()
- game.say("Something slams into the floor around [src], exposing it to space!")
+ game.say("Something slams into the floor around [game], exposing it to space!")
if(game.hull)
- addtimer(CALLBACK(game, .proc/fix_floor, game), 1 SECONDS)
+ addtimer(CALLBACK(src, .proc/fix_floor, game), 1 SECONDS)
/datum/orion_event/hull_part/proc/fix_floor(obj/machinery/computer/arcade/orion_trail/game)
- game.say("A new floor suddenly appears around [src]. What the hell?")
+ game.say("A new floor suddenly appears around [game]. What the hell?")
playsound(game, 'sound/weapons/genhit.ogg', 100, TRUE)
- for(var/turf/open/space/fixed in orange(1, src))
+ for(var/turf/open/space/fixed in orange(1, game))
fixed.PlaceOnTop(/turf/open/floor/plating)
#define BUTTON_EXPLORE_SHIP "Explore Ship"
@@ -412,7 +412,7 @@
game.turns += 1
return ..()
if(prob(75-gamer_skill))
- game.encounter_event(/datum/orion_event/black_hole_death)
+ game.encounter_event(/datum/orion_event/black_hole_death, usr)
return
game.turns += 1
..()
@@ -525,7 +525,7 @@
game.say("WEEWOO! WEEWOO! Spaceport security en route!")
playsound(game, 'sound/items/weeoo1.ogg', 100, FALSE)
for(var/i in 1 to 3)
- var/mob/living/simple_animal/hostile/syndicate/ranged/smg/orion/spaceport_security = new(get_turf(src))
+ var/mob/living/simple_animal/hostile/syndicate/ranged/smg/orion/spaceport_security = new(get_turf(game))
spaceport_security.GiveTarget(usr)
game.fuel += fuel
game.food += food
diff --git a/code/game/machinery/computer/atmos_computers/__identifiers.dm b/code/game/machinery/computer/atmos_computers/__identifiers.dm
index 3a8bfa5322c8a..c8096472b8e62 100644
--- a/code/game/machinery/computer/atmos_computers/__identifiers.dm
+++ b/code/game/machinery/computer/atmos_computers/__identifiers.dm
@@ -26,7 +26,8 @@
#define ATMOS_GAS_MONITOR_HELIUM "helium"
#define ATMOS_GAS_MONITOR_ANTINOBLIUM "antinoblium"
#define ATMOS_GAS_MONITOR_INCINERATOR "incinerator"
-#define ATMOS_GAS_MONITOR_ORDNANCE_LAB "ordnancelab"
+#define ATMOS_GAS_MONITOR_ORDNANCE_BURN "ordnanceburn"
+#define ATMOS_GAS_MONITOR_ORDNANCE_FREEZER "ordnancefreezer"
#define ATMOS_GAS_MONITOR_DISTRO "distro"
#define ATMOS_GAS_MONITOR_WASTE "waste"
@@ -55,7 +56,8 @@ GLOBAL_LIST_INIT(station_gas_chambers, list(
ATMOS_GAS_MONITOR_ANTINOBLIUM = "Antinoblium Supply",
ATMOS_GAS_MONITOR_MIX = "Mix Chamber",
ATMOS_GAS_MONITOR_INCINERATOR = "Incinerator Chamber",
- ATMOS_GAS_MONITOR_ORDNANCE_LAB = "Ordnance Chamber",
+ ATMOS_GAS_MONITOR_ORDNANCE_BURN = "Ordnance Burn Chamber",
+ ATMOS_GAS_MONITOR_ORDNANCE_FREEZER = "Ordnance Freezer Chamber",
ATMOS_GAS_MONITOR_DISTRO = "Distribution Loop",
ATMOS_GAS_MONITOR_WASTE = "Waste Loop",
))
diff --git a/code/game/machinery/computer/atmos_computers/air_sensors.dm b/code/game/machinery/computer/atmos_computers/air_sensors.dm
index 429389473eea4..bd28bcbae3a27 100644
--- a/code/game/machinery/computer/atmos_computers/air_sensors.dm
+++ b/code/game/machinery/computer/atmos_computers/air_sensors.dm
@@ -2,10 +2,6 @@
name = "plasma tank gas sensor"
chamber_id = ATMOS_GAS_MONITOR_PLAS
-/obj/machinery/air_sensor/ordnance_mixing_tank
- name = "ordnance mixing gas sensor"
- chamber_id = ATMOS_GAS_MONITOR_ORDNANCE_LAB
-
/obj/machinery/air_sensor/oxygen_tank
name = "oxygen tank gas sensor"
chamber_id = ATMOS_GAS_MONITOR_O2
@@ -93,3 +89,11 @@
/obj/machinery/air_sensor/incinerator_tank
name = "incinerator chamber gas sensor"
chamber_id = ATMOS_GAS_MONITOR_INCINERATOR
+
+/obj/machinery/air_sensor/ordnance_burn_chamber
+ name = "ordnance burn chamber gas sensor"
+ chamber_id = ATMOS_GAS_MONITOR_ORDNANCE_BURN
+
+/obj/machinery/air_sensor/ordnance_freezer_chamber
+ name = "ordnance freezer chamber gas sensor"
+ chamber_id = ATMOS_GAS_MONITOR_ORDNANCE_FREEZER
diff --git a/code/game/machinery/computer/atmos_computers/atmos_controls.dm b/code/game/machinery/computer/atmos_computers/atmos_controls.dm
index 6aa45f7c234a2..bf386c83a1c38 100644
--- a/code/game/machinery/computer/atmos_computers/atmos_controls.dm
+++ b/code/game/machinery/computer/atmos_computers/atmos_controls.dm
@@ -118,7 +118,10 @@
circuit = /obj/item/circuitboard/computer/atmos_control/nocontrol/incinerator
atmos_chambers = list(ATMOS_GAS_MONITOR_INCINERATOR = "Incinerator Chamber")
-/obj/machinery/computer/atmos_control/nocontrol/ordnancemix
- name = "Ordnance Chamber Monitor"
- circuit = /obj/item/circuitboard/computer/atmos_control/nocontrol/ordnancemix
- atmos_chambers = list(ATMOS_GAS_MONITOR_ORDNANCE_LAB = "Ordnance Chamber")
+/obj/machinery/computer/atmos_control/ordnancemix
+ name = "Ordnance Chamber Control"
+ circuit = /obj/item/circuitboard/computer/atmos_control/ordnancemix
+ atmos_chambers = list(
+ ATMOS_GAS_MONITOR_ORDNANCE_BURN = "Ordnance Burn Chamber",
+ ATMOS_GAS_MONITOR_ORDNANCE_FREEZER = "Ordnance Freezer Chamber",
+ )
diff --git a/code/game/machinery/computer/atmos_computers/inlets.dm b/code/game/machinery/computer/atmos_computers/inlets.dm
index 934d128f1aa85..4df5a9185f8fb 100644
--- a/code/game/machinery/computer/atmos_computers/inlets.dm
+++ b/code/game/machinery/computer/atmos_computers/inlets.dm
@@ -161,6 +161,12 @@
name = "incinerator chamber input injector"
chamber_id = ATMOS_GAS_MONITOR_INCINERATOR
-/obj/machinery/atmospherics/components/unary/outlet_injector/monitored/ordnance_mixing_input
- name = "ordnance mixing input injector"
- chamber_id = ATMOS_GAS_MONITOR_ORDNANCE_LAB
+/obj/machinery/atmospherics/components/unary/outlet_injector/monitored/ordnance_burn_chamber_input
+ on = FALSE
+ name = "ordnance burn chamber input injector"
+ chamber_id = ATMOS_GAS_MONITOR_ORDNANCE_BURN
+
+/obj/machinery/atmospherics/components/unary/outlet_injector/monitored/ordnance_freezer_chamber_input
+ on = FALSE
+ name = "ordnance freezer chamber input injector"
+ chamber_id = ATMOS_GAS_MONITOR_ORDNANCE_FREEZER
diff --git a/code/game/machinery/computer/atmos_computers/outlets.dm b/code/game/machinery/computer/atmos_computers/outlets.dm
index 5b7dac9a17651..c20a431dacc17 100644
--- a/code/game/machinery/computer/atmos_computers/outlets.dm
+++ b/code/game/machinery/computer/atmos_computers/outlets.dm
@@ -120,9 +120,13 @@
name = "incinerator chamber output inlet"
chamber_id = ATMOS_GAS_MONITOR_INCINERATOR
-/obj/machinery/atmospherics/components/unary/vent_pump/siphon/monitored/ordnance_mixing_output
- name = "ordnance mixing output inlet"
- chamber_id = ATMOS_GAS_MONITOR_ORDNANCE_LAB
+/obj/machinery/atmospherics/components/unary/vent_pump/siphon/monitored/ordnance_burn_chamber_output
+ name = "ordnance burn chamber output inlet"
+ chamber_id = ATMOS_GAS_MONITOR_ORDNANCE_BURN
+
+/obj/machinery/atmospherics/components/unary/vent_pump/siphon/monitored/ordnance_freezer_chamber_output
+ name = "ordnance freezer chamber output inlet"
+ chamber_id = ATMOS_GAS_MONITOR_ORDNANCE_FREEZER
/obj/machinery/atmospherics/components/unary/vent_pump/high_volume/siphon/monitored
frequency = FREQ_ATMOS_STORAGE
diff --git a/code/game/machinery/computer/chef_orders/order_datum.dm b/code/game/machinery/computer/chef_orders/order_datum.dm
index c0743f08074a6..5d6eb00690168 100644
--- a/code/game/machinery/computer/chef_orders/order_datum.dm
+++ b/code/game/machinery/computer/chef_orders/order_datum.dm
@@ -217,6 +217,36 @@
item_instance = /obj/item/food/ready_donk/donkhiladas
cost_per_order = 40
+/datum/orderable_item/tiziran_goods
+ name = "Tiziran Farm-Fresh Pack"
+ category_index = CATEGORY_MILK_EGGS
+ item_instance = /obj/item/storage/box/tiziran_goods
+ cost_per_order = 120
+
+/datum/orderable_item/tiziran_cans
+ name = "Tiziran Canned Goods Pack"
+ category_index = CATEGORY_MILK_EGGS
+ item_instance = /obj/item/storage/box/tiziran_goods
+ cost_per_order = 120
+
+/datum/orderable_item/tiziran_meats
+ name = "Tiziran Meatmarket Pack"
+ category_index = CATEGORY_MILK_EGGS
+ item_instance = /obj/item/storage/box/tiziran_meats
+ cost_per_order = 120
+
+/datum/orderable_item/mothic_goods
+ name = "Mothic Farm-Fresh Pack"
+ category_index = CATEGORY_MILK_EGGS
+ item_instance = /obj/item/storage/box/mothic_goods
+ cost_per_order = 120
+
+/datum/orderable_item/mothic_cans_sauces
+ name = "Mothic Pantry Pack"
+ category_index = CATEGORY_MILK_EGGS
+ item_instance = /obj/item/storage/box/mothic_cans_sauces
+ cost_per_order = 120
+
//Reagents
/datum/orderable_item/flour
@@ -283,4 +313,4 @@
name = "Quality Oil"
category_index = CATEGORY_SAUCES_REAGENTS
item_instance = /obj/item/reagent_containers/food/condiment/quality_oil
- cost_per_order = 120 //Extra Virgin, just like you, the reader
+ cost_per_order = 50 //Extra Virgin, just like you, the reader
diff --git a/code/game/machinery/computer/communications.dm b/code/game/machinery/computer/communications.dm
index e37a859cb97ec..f4d600e3caf22 100755
--- a/code/game/machinery/computer/communications.dm
+++ b/code/game/machinery/computer/communications.dm
@@ -197,13 +197,13 @@
playsound(src, 'sound/machines/terminal_prompt_deny.ogg', 50, FALSE)
return
- var/new_sec_level = seclevel2num(params["newSecurityLevel"])
+ var/new_sec_level = SSsecurity_level.text_level_to_number(params["newSecurityLevel"])
if (new_sec_level != SEC_LEVEL_GREEN && new_sec_level != SEC_LEVEL_BLUE)
return
- if (SSsecurity_level.current_level == new_sec_level)
+ if (SSsecurity_level.get_current_level_as_number() == new_sec_level)
return
- set_security_level(new_sec_level)
+ SSsecurity_level.set_level(new_sec_level)
to_chat(usr, span_notice("Authorization confirmed. Modifying security level."))
playsound(src, 'sound/machines/terminal_prompt_confirm.ogg', 50, FALSE)
@@ -517,7 +517,7 @@
data["shuttleCalled"] = FALSE
data["shuttleLastCalled"] = FALSE
data["aprilFools"] = SSevents.holidays && SSevents.holidays[APRIL_FOOLS]
- data["alertLevel"] = get_security_level()
+ data["alertLevel"] = SSsecurity_level.get_current_level_as_text()
data["authorizeName"] = authorize_name
data["canLogOut"] = !issilicon(user)
data["shuttleCanEvacOrFailReason"] = SSshuttle.canEvac(user)
@@ -776,10 +776,27 @@
#define MIN_GHOSTS_FOR_FUGITIVES 6
/// The maximum percentage of the population to be ghosts before we no longer have the chance of spawning Sleeper Agents.
#define MAX_PERCENT_GHOSTS_FOR_SLEEPER 0.2
-/// The amount of threat injected by a hack, if chosen.
-#define HACK_THREAT_INJECTION_AMOUNT 15
-/*
+/// Begin the process of hacking into the comms console to call in a threat.
+/obj/machinery/computer/communications/proc/try_hack_console(mob/living/hacker, duration = 30 SECONDS)
+ if(!can_hack())
+ return FALSE
+
+ AI_notify_hack()
+ if(!do_after(hacker, duration, src, extra_checks = CALLBACK(src, .proc/can_hack)))
+ return FALSE
+
+ hack_console(hacker)
+ return TRUE
+
+/// Checks if this console is hackable. Used as a callback during try_hack_console's doafter as well.
+/obj/machinery/computer/communications/proc/can_hack()
+ if(machine_stat & (NOPOWER|BROKEN))
+ return FALSE
+
+ return TRUE
+
+/**
* The communications console hack,
* called by certain antagonist actions.
*
@@ -811,8 +828,8 @@
if(HACK_PIRATE) // Triggers pirates, which the crew may be able to pay off to prevent
priority_announce(
"Attention crew, it appears that someone on your station has made unexpected communication with a Syndicate ship in nearby space.",
- "[command_name()] High-Priority Update"
- )
+ "[command_name()] High-Priority Update",
+ )
var/datum/round_event_control/pirates/pirate_event = locate() in SSevents.control
if(!pirate_event)
@@ -821,20 +838,20 @@
if(HACK_FUGITIVES) // Triggers fugitives, which can cause confusion / chaos as the crew decides which side help
priority_announce(
- "Attention crew, it appears that someone on your station has established an unexpected orbit with an unmarked ship in nearby space.",
- "[command_name()] High-Priority Update"
- )
+ "Attention crew, it appears that someone on your station has made unexpected communication with an unmarked ship in nearby space.",
+ "[command_name()] High-Priority Update",
+ )
var/datum/round_event_control/fugitives/fugitive_event = locate() in SSevents.control
if(!fugitive_event)
CRASH("hack_console() attempted to run fugitives, but could not find an event controller!")
addtimer(CALLBACK(fugitive_event, /datum/round_event_control.proc/runEvent), rand(20 SECONDS, 1 MINUTES))
- if(HACK_THREAT) // Adds a flat amount of threat to buy a (probably) more dangerous antag later
+ if(HACK_THREAT) // Force an unfavorable situation on the crew
priority_announce(
- "Attention crew, it appears that someone on your station has shifted your orbit into more dangerous territory.",
- "[command_name()] High-Priority Update"
- )
+ SSmapping.config.orbit_shift_replacement,
+ "[command_name()] High-Priority Update",
+ )
for(var/mob/crew_member as anything in GLOB.player_list)
if(!is_station_level(crew_member.z))
@@ -842,7 +859,7 @@
shake_camera(crew_member, 15, 1)
var/datum/game_mode/dynamic/dynamic = SSticker.mode
- dynamic.create_threat(HACK_THREAT_INJECTION_AMOUNT, list(dynamic.threat_log, dynamic.roundend_threat_log), "[worldtime2text()]: Communications console hacked by [hacker]")
+ dynamic.unfavorable_situation()
if(HACK_SLEEPER) // Trigger one or multiple sleeper agents with the crew (or for latejoining crew)
var/datum/dynamic_ruleset/midround/sleeper_agent_type = /datum/dynamic_ruleset/midround/autotraitor
@@ -850,23 +867,20 @@
var/max_number_of_sleepers = clamp(round(length(GLOB.alive_player_list) / 20), 1, 3)
var/num_agents_created = 0
for(var/num_agents in 1 to rand(1, max_number_of_sleepers))
- // Offset the threat cost of the sleeper agent(s) we're about to run...
- dynamic.create_threat(initial(sleeper_agent_type.cost))
- // ...Then try to actually trigger a sleeper agent.
- if(!dynamic.picking_specific_rule(sleeper_agent_type, TRUE))
+ if(!dynamic.picking_specific_rule(sleeper_agent_type, forced = TRUE, ignore_cost = TRUE))
break
num_agents_created++
if(num_agents_created <= 0)
// We failed to run any midround sleeper agents, so let's be patient and run latejoin traitor
- dynamic.picking_specific_rule(/datum/dynamic_ruleset/latejoin/infiltrator, TRUE)
+ dynamic.picking_specific_rule(/datum/dynamic_ruleset/latejoin/infiltrator, forced = TRUE, ignore_cost = TRUE)
else
// We spawned some sleeper agents, nice - give them a report to kickstart the paranoia
priority_announce(
"Attention crew, it appears that someone on your station has hijacked your telecommunications, broadcasting a Syndicate radio signal to your fellow employees.",
- "[command_name()] High-Priority Update"
- )
+ "[command_name()] High-Priority Update",
+ )
#undef HACK_PIRATE
#undef HACK_FUGITIVES
@@ -876,7 +890,6 @@
#undef MIN_GHOSTS_FOR_PIRATES
#undef MIN_GHOSTS_FOR_FUGITIVES
#undef MAX_PERCENT_GHOSTS_FOR_SLEEPER
-#undef HACK_THREAT_INJECTION_AMOUNT
/datum/comm_message
var/title
diff --git a/code/game/machinery/computer/crew.dm b/code/game/machinery/computer/crew.dm
index 67c2f076bc7e0..88398137c90c0 100644
--- a/code/game/machinery/computer/crew.dm
+++ b/code/game/machinery/computer/crew.dm
@@ -121,11 +121,11 @@ GLOBAL_DATUM_INIT(crewmonitor, /datum/crewmonitor, new)
JOB_STATION_ENGINEER = 41,
JOB_ATMOSPHERIC_TECHNICIAN = 42,
// 50-59: Cargo
- JOB_HEAD_OF_PERSONNEL = 50,
- JOB_QUARTERMASTER = 51,
- JOB_SHAFT_MINER = 52,
- JOB_CARGO_TECHNICIAN = 53,
+ JOB_QUARTERMASTER = 50,
+ JOB_SHAFT_MINER = 51,
+ JOB_CARGO_TECHNICIAN = 52,
// 60+: Civilian/other
+ JOB_HEAD_OF_PERSONNEL = 60,
JOB_BARTENDER = 61,
JOB_COOK = 62,
JOB_BOTANIST = 63,
diff --git a/code/game/machinery/computer/security.dm b/code/game/machinery/computer/security.dm
index 0d8c28203b6b2..99b21fd536c56 100644
--- a/code/game/machinery/computer/security.dm
+++ b/code/game/machinery/computer/security.dm
@@ -502,7 +502,7 @@ What a mess.*/
else if(I && check_access(I))
active1 = null
active2 = null
- authenticated = I.registered_name
+ authenticated = (I.registered_name ? I.registered_name : "Unknown")
rank = I.assignment
screen = 1
else
diff --git a/code/game/machinery/dance_machine.dm b/code/game/machinery/dance_machine.dm
index 2c6fdc8fcb29b..fc26394eb0449 100644
--- a/code/game/machinery/dance_machine.dm
+++ b/code/game/machinery/dance_machine.dm
@@ -458,7 +458,7 @@
continue
if(!(M in rangers))
rangers[M] = TRUE
- M.playsound_local(get_turf(M), null, volume, channel = CHANNEL_JUKEBOX, S = song_played, use_reverb = FALSE)
+ M.playsound_local(get_turf(M), null, volume, channel = CHANNEL_JUKEBOX, sound_to_use = song_played, use_reverb = FALSE)
for(var/mob/L in rangers)
if(get_dist(src,L) > 10)
rangers -= L
diff --git a/code/game/machinery/defibrillator_mount.dm b/code/game/machinery/defibrillator_mount.dm
index 2b9851033e0e7..648e5ae15d8c7 100644
--- a/code/game/machinery/defibrillator_mount.dm
+++ b/code/game/machinery/defibrillator_mount.dm
@@ -41,7 +41,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/defibrillator_mount, 28)
. = ..()
if(defib)
. += span_notice("There is a defib unit hooked up. Alt-click to remove it.")
- if(SSsecurity_level.current_level >= SEC_LEVEL_RED)
+ if(SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED)
. += span_notice("Due to a security situation, its locking clamps can be toggled by swiping any ID.")
else
. += span_notice("Its locking clamps can be [clamps_locked ? "dis" : ""]engaged by swiping an ID with access.")
@@ -107,7 +107,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/defibrillator_mount, 28)
return
var/obj/item/card/id = I.GetID()
if(id)
- if(check_access(id) || SSsecurity_level.current_level >= SEC_LEVEL_RED) //anyone can toggle the clamps in red alert!
+ if(check_access(id) || SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED) //anyone can toggle the clamps in red alert!
if(!defib)
to_chat(user, span_warning("You can't engage the clamps on a defibrillator that isn't there."))
return
diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm
index 26015873b55de..c471b26b6363f 100644
--- a/code/game/machinery/doors/door.dm
+++ b/code/game/machinery/doors/door.dm
@@ -65,10 +65,15 @@
explosion_block = EXPLOSION_BLOCK_PROC
RegisterSignal(SSsecurity_level, COMSIG_SECURITY_LEVEL_CHANGED, .proc/check_security_level)
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_MAGICALLY_UNLOCKED = .proc/on_magic_unlock,
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+
/obj/machinery/door/examine(mob/user)
. = ..()
if(red_alert_access)
- if(SSsecurity_level.current_level >= SEC_LEVEL_RED)
+ if(SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED)
. += span_notice("Due to a security threat, its access requirements have been lifted!")
else
. += span_notice("In the event of a red alert, its access requirements will automatically lift.")
@@ -88,7 +93,7 @@
return CONTEXTUAL_SCREENTIP_SET
/obj/machinery/door/check_access_list(list/access_list)
- if(red_alert_access && SSsecurity_level.current_level >= SEC_LEVEL_RED)
+ if(red_alert_access && SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED)
return TRUE
return ..()
@@ -483,5 +488,10 @@
zap_flags &= ~ZAP_OBJ_DAMAGE
. = ..()
+/// Signal proc for [COMSIG_ATOM_MAGICALLY_UNLOCKED]. Open up when someone casts knock.
+/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)
#undef DOOR_CLOSE_WAIT
diff --git a/code/game/machinery/doors/firedoor.dm b/code/game/machinery/doors/firedoor.dm
index 3f90440257f3d..dc4e834914867 100644
--- a/code/game/machinery/doors/firedoor.dm
+++ b/code/game/machinery/doors/firedoor.dm
@@ -82,7 +82,7 @@
RegisterSignal(src, COMSIG_MERGER_ADDING, .proc/merger_adding)
RegisterSignal(src, COMSIG_MERGER_REMOVING, .proc/merger_removing)
GetMergeGroup(merger_id, merger_typecache)
- register_adjacent_turfs(src)
+ register_adjacent_turfs()
if(alarm_type) // Fucking subtypes fucking mappers fucking hhhhhhhh
start_activation_process(alarm_type)
@@ -215,29 +215,35 @@
var/turf/checked_turf = get_step(get_turf(firelock), dir)
if(!checked_turf)
continue
+ if(isclosedturf(checked_turf))
+ continue
process_results(checked_turf)
-/obj/machinery/door/firedoor/proc/register_adjacent_turfs(atom/loc)
+/obj/machinery/door/firedoor/proc/register_adjacent_turfs()
if(!loc)
return
- RegisterSignal(loc, COMSIG_TURF_CALCULATED_ADJACENT_ATMOS, .proc/process_results)
+ var/turf/our_turf = get_turf(loc)
+ RegisterSignal(our_turf, COMSIG_TURF_CALCULATED_ADJACENT_ATMOS, .proc/process_results)
for(var/dir in GLOB.cardinals)
- var/turf/checked_turf = get_step(get_turf(loc), dir)
+ var/turf/checked_turf = get_step(our_turf, dir)
if(!checked_turf)
continue
+ if(isclosedturf(checked_turf))
+ continue
process_results(checked_turf)
RegisterSignal(checked_turf, COMSIG_TURF_EXPOSE, .proc/process_results)
-/obj/machinery/door/firedoor/proc/unregister_adjacent_turfs(atom/loc)
+/obj/machinery/door/firedoor/proc/unregister_adjacent_turfs(atom/old_loc)
if(!loc)
return
- UnregisterSignal(loc, COMSIG_TURF_CALCULATED_ADJACENT_ATMOS)
+ var/turf/our_turf = get_turf(old_loc)
+ UnregisterSignal(our_turf, COMSIG_TURF_CALCULATED_ADJACENT_ATMOS)
for(var/dir in GLOB.cardinals)
- var/turf/checked_turf = get_step(get_turf(loc), dir)
+ var/turf/checked_turf = get_step(our_turf, dir)
if(!checked_turf)
continue
@@ -647,7 +653,7 @@
/obj/machinery/door/firedoor/Moved(atom/oldloc)
. = ..()
unregister_adjacent_turfs(oldloc)
- register_adjacent_turfs(src)
+ register_adjacent_turfs()
/obj/machinery/door/firedoor/closed
icon_state = "door_closed"
diff --git a/code/game/machinery/firealarm.dm b/code/game/machinery/firealarm.dm
index 7a5fad78b1b18..31be707ea873a 100644
--- a/code/game/machinery/firealarm.dm
+++ b/code/game/machinery/firealarm.dm
@@ -128,9 +128,9 @@
. += "fire_overlay"
if(is_station_level(z))
- . += "fire_[SSsecurity_level.current_level]"
- . += mutable_appearance(icon, "fire_[SSsecurity_level.current_level]")
- . += emissive_appearance(icon, "fire_[SSsecurity_level.current_level]", alpha = src.alpha)
+ . += "fire_[SSsecurity_level.get_current_level_as_number()]"
+ . += mutable_appearance(icon, "fire_[SSsecurity_level.get_current_level_as_number()]")
+ . += emissive_appearance(icon, "fire_[SSsecurity_level.get_current_level_as_number()]", alpha = src.alpha)
else
. += "fire_[SEC_LEVEL_GREEN]"
. += mutable_appearance(icon, "fire_[SEC_LEVEL_GREEN]")
diff --git a/code/game/machinery/hologram.dm b/code/game/machinery/hologram.dm
index 88a3ad7e89ab8..ec538eb202f9d 100644
--- a/code/game/machinery/hologram.dm
+++ b/code/game/machinery/hologram.dm
@@ -39,7 +39,8 @@ Possible to do for anyone motivated enough:
icon_state = "holopad0"
base_icon_state = "holopad"
layer = LOW_OBJ_LAYER
- plane = FLOOR_PLANE
+ /// The plane is set such that it shows up without being covered by pipes/wires in a map editor, we change this on initialize.
+ plane = GAME_PLANE
req_access = list(ACCESS_KEYCARD_AUTH) //Used to allow for forced connecting to other (not secure) holopads. Anyone can make a call, though.
max_integrity = 300
armor = list(MELEE = 50, BULLET = 20, LASER = 20, ENERGY = 20, BOMB = 0, BIO = 0, FIRE = 50, ACID = 0)
@@ -86,6 +87,12 @@ Possible to do for anyone motivated enough:
///bitfield. used to turn on and off hearing sensitivity depending on if we can act on Hear() at all - meant for lowering the number of unessesary hearable atoms
var/can_hear_flags = NONE
+/obj/machinery/holopad/Initialize(mapload)
+ . = ..()
+ /// We set the plane on mapload such that we can see the holopad render over atmospherics pipe and cabling in a map editor (without initialization), but so it gets that "inset" look in the floor in-game.
+ plane = FLOOR_PLANE
+ update_overlays()
+
/obj/machinery/holopad/secure
name = "secure holopad"
desc = "It's a floor-mounted device for projecting holographic images. This one will refuse to auto-connect incoming calls."
diff --git a/code/game/machinery/limbgrower.dm b/code/game/machinery/limbgrower.dm
index 0e800a15365f4..217f80878e329 100644
--- a/code/game/machinery/limbgrower.dm
+++ b/code/game/machinery/limbgrower.dm
@@ -216,7 +216,7 @@
limb = new buildpath(loc)
limb.name = "\improper synthetic [selected_category] [limb.plaintext_zone]"
limb.limb_id = selected_category
- limb.mutation_color = "#62A262"
+ limb.species_color = "#62A262"
limb.update_icon_dropped()
///Returns a valid limb typepath based on the selected option
diff --git a/code/game/machinery/recycler.dm b/code/game/machinery/recycler.dm
index 33ea2c265daf6..8d088dfd5d81a 100644
--- a/code/game/machinery/recycler.dm
+++ b/code/game/machinery/recycler.dm
@@ -49,14 +49,8 @@
/obj/machinery/recycler/RefreshParts()
. = ..()
var/amt_made = 0
- var/mat_mod = 0
- for(var/obj/item/stock_parts/matter_bin/B in component_parts)
- mat_mod = 2 * B.rating
- mat_mod *= 50000
for(var/obj/item/stock_parts/manipulator/M in component_parts)
amt_made = 12.5 * M.rating //% of materials salvaged
- var/datum/component/material_container/materials = GetComponent(/datum/component/material_container)
- materials.max_amount = mat_mod
amount_produced = min(50, amt_made) + 50
var/datum/component/butchering/butchering = GetComponent(/datum/component/butchering/recycler)
butchering.effectiveness = amount_produced
@@ -167,22 +161,21 @@
qdel(content)
/obj/machinery/recycler/proc/recycle_item(obj/item/I)
-
var/obj/item/grown/log/L = I
if(istype(L))
var/seed_modifier = 0
if(L.seed)
seed_modifier = round(L.seed.potency / 25)
new L.plank_type(loc, 1 + seed_modifier)
+ qdel(I)
else
var/datum/component/material_container/materials = GetComponent(/datum/component/material_container)
var/material_amount = materials.get_item_material_amount(I, BREAKDOWN_FLAGS_RECYCLER)
if(!material_amount)
return
materials.insert_item(I, material_amount, multiplier = (amount_produced / 100), breakdown_flags=BREAKDOWN_FLAGS_RECYCLER)
+ qdel(I)
materials.retrieve_all()
- qdel(I)
-
/obj/machinery/recycler/proc/emergency_stop()
playsound(src, 'sound/machines/buzz-sigh.ogg', 50, FALSE)
@@ -228,6 +221,6 @@
/obj/item/paper/guides/recycler
name = "paper - 'garbage duty instructions'"
- info = "
New Assignment
You have been assigned to collect garbage from trash bins, located around the station. The crewmembers will put their trash into it and you will collect the said trash.
There is a recycling machine near your closet, inside maintenance; use it to recycle the trash for a small chance to get useful minerals. Then deliver these minerals to cargo or engineering. You are our last hope for a clean station, do not screw this up!"
+ info = "
New Assignment
You have been assigned to collect garbage from trash bins, located around the station. The crewmembers will put their trash into it and you will collect said trash.
There is a recycling machine near your closet, inside maintenance; use it to recycle the trash for a small chance to get useful minerals. Then, deliver these minerals to cargo or engineering. You are our last hope for a clean station. Do not screw this up!"
#undef SAFETY_COOLDOWN
diff --git a/code/game/machinery/suit_storage_unit.dm b/code/game/machinery/suit_storage_unit.dm
index 7bcda26b8b051..70d2df27f34b8 100644
--- a/code/game/machinery/suit_storage_unit.dm
+++ b/code/game/machinery/suit_storage_unit.dm
@@ -389,7 +389,7 @@
visible_message(span_warning("[src]'s door creaks open with a loud whining noise. A cloud of foul black smoke escapes from its chamber."))
playsound(src, 'sound/machines/airlock_alien_prying.ogg', 50, TRUE)
var/datum/effect_system/fluid_spread/smoke/bad/black/smoke = new
- smoke.set_up(0, location = src)
+ smoke.set_up(0, holder = src, location = src)
smoke.start()
QDEL_NULL(helmet)
QDEL_NULL(suit)
diff --git a/code/game/machinery/telecomms/computers/message.dm b/code/game/machinery/telecomms/computers/message.dm
index 5f9cd39505ae9..9d153abfaf019 100644
--- a/code/game/machinery/telecomms/computers/message.dm
+++ b/code/game/machinery/telecomms/computers/message.dm
@@ -33,6 +33,13 @@
var/message = "System bootup complete. Please select an option." // The message that shows on the main menu.
var/auth = FALSE // Are they authenticated?
var/optioncount = 7
+ // Custom Message Properties
+ var/customsender = "System Administrator"
+ var/customrecepient = null
+ var/customjob = "Admin"
+ var/custommessage = "This is a test, please ignore."
+
+
/obj/machinery/computer/message_monitor/screwdriver_act(mob/living/user, obj/item/I)
if(obj_flags & EMAGGED)
@@ -183,6 +190,24 @@
10010000001110100011010000110000101110100001000000111010
001101001011011010110010100101110"}
+ //Fake messages
+ if(MSG_MON_SCREEN_CUSTOM_MSG)
+ dat += "
[span_alert("Meteors have been detected on collision course with the station.")]
")
+ SEND_SOUND(target, SSstation.announcer.event_sounds[ANNOUNCER_METEORS])
+ if("supermatter")
+ SEND_SOUND(target, 'sound/magic/charge.ogg')
+ to_chat(target, span_boldannounce("You feel reality distort for a moment..."))
diff --git a/code/modules/hallucination/death.dm b/code/modules/hallucination/death.dm
new file mode 100644
index 0000000000000..744ff878a5608
--- /dev/null
+++ b/code/modules/hallucination/death.dm
@@ -0,0 +1,34 @@
+/datum/hallucination/death
+
+/datum/hallucination/death/New(mob/living/carbon/C, forced = TRUE)
+ set waitfor = FALSE
+ ..()
+ target.set_screwyhud(SCREWYHUD_DEAD)
+ target.Paralyze(300)
+ target.silent += 10
+ to_chat(target, span_deadsay("[target.real_name] has died at [get_area_name(target)]."))
+
+ var/delay = 0
+
+ if(prob(50))
+ var/mob/fakemob
+ var/list/dead_people = list()
+ for(var/mob/dead/observer/G in GLOB.player_list)
+ dead_people += G
+ if(LAZYLEN(dead_people))
+ fakemob = pick(dead_people)
+ else
+ fakemob = target //ever been so lonely you had to haunt yourself?
+ if(fakemob)
+ delay = rand(20, 50)
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/to_chat, target, "DEAD: [fakemob.name] says, \"[pick("rip","why did i just drop dead?","hey [target.first_name()]","git gud","you too?","is the AI rogue?",\
+ "i[prob(50)?" fucking":""] hate [pick("blood cult", "clock cult", "revenants", "this round","this","myself","admins","you")]")]\""), delay)
+
+ addtimer(CALLBACK(src, .proc/cleanup), delay + rand(70, 90))
+
+/datum/hallucination/death/proc/cleanup()
+ if (target)
+ target.set_screwyhud(SCREWYHUD_NONE)
+ target.SetParalyzed(0)
+ target.silent = FALSE
+ qdel(src)
diff --git a/code/modules/hallucination/fire.dm b/code/modules/hallucination/fire.dm
new file mode 100644
index 0000000000000..81396caa45c06
--- /dev/null
+++ b/code/modules/hallucination/fire.dm
@@ -0,0 +1,89 @@
+#define RAISE_FIRE_COUNT 3
+#define RAISE_FIRE_TIME 3
+
+/datum/hallucination/fire
+ var/active = TRUE
+ var/stage = 0
+ var/image/fire_overlay
+
+ var/next_action = 0
+ var/times_to_lower_stamina
+ var/fire_clearing = FALSE
+ var/increasing_stages = TRUE
+ var/time_spent = 0
+
+/datum/hallucination/fire/New(mob/living/carbon/C, forced = TRUE)
+ set waitfor = FALSE
+ ..()
+ target.set_fire_stacks(max(target.fire_stacks, 0.1)) //Placebo flammability
+ fire_overlay = image('icons/mob/onfire.dmi', target, "human_burning", ABOVE_MOB_LAYER)
+ if(target.client)
+ target.client.images += fire_overlay
+ to_chat(target, span_userdanger("You're set on fire!"))
+ target.throw_alert(ALERT_FIRE, /atom/movable/screen/alert/fire, override = TRUE)
+ times_to_lower_stamina = rand(5, 10)
+ addtimer(CALLBACK(src, .proc/start_expanding), 20)
+
+/datum/hallucination/fire/Destroy()
+ . = ..()
+ STOP_PROCESSING(SSfastprocess, src)
+
+/datum/hallucination/fire/proc/start_expanding()
+ if (isnull(target))
+ qdel(src)
+ return
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/hallucination/fire/process(delta_time)
+ if (isnull(target))
+ qdel(src)
+ return
+
+ if(target.fire_stacks <= 0)
+ clear_fire()
+
+ time_spent += delta_time
+
+ if (fire_clearing)
+ next_action -= delta_time
+ if (next_action < 0)
+ stage -= 1
+ update_temp()
+ next_action += 3
+ else if (increasing_stages)
+ var/new_stage = min(round(time_spent / RAISE_FIRE_TIME), RAISE_FIRE_COUNT)
+ if (stage != new_stage)
+ stage = new_stage
+ update_temp()
+
+ if (stage == RAISE_FIRE_COUNT)
+ increasing_stages = FALSE
+ else if (times_to_lower_stamina)
+ next_action -= delta_time
+ if (next_action < 0)
+ target.adjustStaminaLoss(15)
+ next_action += 2
+ times_to_lower_stamina -= 1
+ else
+ clear_fire()
+
+/datum/hallucination/fire/proc/update_temp()
+ if(stage <= 0)
+ target.clear_alert(ALERT_TEMPERATURE, clear_override = TRUE)
+ else
+ target.clear_alert(ALERT_TEMPERATURE, clear_override = TRUE)
+ target.throw_alert(ALERT_TEMPERATURE, /atom/movable/screen/alert/hot, stage, override = TRUE)
+
+/datum/hallucination/fire/proc/clear_fire()
+ if(!active)
+ return
+ active = FALSE
+ target.clear_alert(ALERT_FIRE, clear_override = TRUE)
+ if(target.client)
+ target.client.images -= fire_overlay
+ QDEL_NULL(fire_overlay)
+ fire_clearing = TRUE
+ next_action = 0
+
+#undef RAISE_FIRE_COUNT
+#undef RAISE_FIRE_TIME
diff --git a/code/modules/hallucination/hazard.dm b/code/modules/hallucination/hazard.dm
new file mode 100644
index 0000000000000..5278c9d7e8a28
--- /dev/null
+++ b/code/modules/hallucination/hazard.dm
@@ -0,0 +1,128 @@
+/* Hazard Hallucinations
+ *
+ * Contains:
+ * Lava
+ * Chasm
+ * Anomaly
+ */
+
+/datum/hallucination/dangerflash
+
+/datum/hallucination/dangerflash/New(mob/living/carbon/C, forced = TRUE, danger_type)
+ set waitfor = FALSE
+ ..()
+ //Flashes of danger
+
+ var/list/possible_points = list()
+ for(var/turf/open/floor/F in view(target,world.view))
+ possible_points += F
+ if(possible_points.len)
+ var/turf/open/floor/danger_point = pick(possible_points)
+ if(!danger_type)
+ danger_type = pick("lava","chasm","anomaly")
+ switch(danger_type)
+ if("lava")
+ new /obj/effect/hallucination/danger/lava(danger_point, target)
+ if("chasm")
+ new /obj/effect/hallucination/danger/chasm(danger_point, target)
+ if("anomaly")
+ new /obj/effect/hallucination/danger/anomaly(danger_point, target)
+
+ qdel(src)
+
+/obj/effect/hallucination/danger
+ var/image/image
+
+/obj/effect/hallucination/danger/proc/show_icon()
+ return
+
+/obj/effect/hallucination/danger/proc/clear_icon()
+ if(image && target.client)
+ target.client.images -= image
+
+/obj/effect/hallucination/danger/Initialize(mapload, _target)
+ . = ..()
+ target = _target
+ show_icon()
+ QDEL_IN(src, rand(200, 450))
+
+/obj/effect/hallucination/danger/Destroy()
+ clear_icon()
+ . = ..()
+
+/obj/effect/hallucination/danger/lava
+ name = "lava"
+
+/obj/effect/hallucination/danger/lava/Initialize(mapload, _target)
+ . = ..()
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = .proc/on_entered,
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+/obj/effect/hallucination/danger/lava/show_icon()
+ var/turf/danger_turf = get_turf(src)
+ image = image('icons/turf/floors/lava.dmi', src, "lava-[danger_turf.smoothing_junction || 0]", TURF_LAYER)
+ if(target.client)
+ target.client.images += image
+
+/obj/effect/hallucination/danger/lava/proc/on_entered(datum/source, atom/movable/AM)
+ SIGNAL_HANDLER
+ if(AM == target)
+ target.adjustStaminaLoss(20)
+ new /datum/hallucination/fire(target)
+
+/obj/effect/hallucination/danger/chasm
+ name = "chasm"
+
+/obj/effect/hallucination/danger/chasm/Initialize(mapload, _target)
+ . = ..()
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = .proc/on_entered,
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+/obj/effect/hallucination/danger/chasm/show_icon()
+ var/turf/danger_turf = get_turf(src)
+ image = image('icons/turf/floors/chasms.dmi', src, "chasms-[danger_turf.smoothing_junction || 0]", TURF_LAYER)
+ if(target.client)
+ target.client.images += image
+
+/obj/effect/hallucination/danger/chasm/proc/on_entered(datum/source, atom/movable/AM)
+ SIGNAL_HANDLER
+ if(AM == target)
+ if(istype(target, /obj/effect/dummy/phased_mob))
+ return
+ to_chat(target, span_userdanger("You fall into the chasm!"))
+ target.Paralyze(40)
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/to_chat, target, span_notice("It's surprisingly shallow.")), 15)
+ QDEL_IN(src, 30)
+
+/obj/effect/hallucination/danger/anomaly
+ name = "flux wave anomaly"
+
+/obj/effect/hallucination/danger/anomaly/Initialize(mapload)
+ . = ..()
+ START_PROCESSING(SSobj, src)
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = .proc/on_entered,
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+
+/obj/effect/hallucination/danger/anomaly/process(delta_time)
+ if(DT_PROB(45, delta_time))
+ step(src,pick(GLOB.alldirs))
+
+/obj/effect/hallucination/danger/anomaly/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
+/obj/effect/hallucination/danger/anomaly/show_icon()
+ image = image('icons/effects/effects.dmi',src,"electricity2",OBJ_LAYER+0.01)
+ if(target.client)
+ target.client.images += image
+
+/obj/effect/hallucination/danger/anomaly/proc/on_entered(datum/source, atom/movable/AM)
+ SIGNAL_HANDLER
+ if(AM == target)
+ new /datum/hallucination/shock(target)
diff --git a/code/modules/hallucination/hostile_mob.dm b/code/modules/hallucination/hostile_mob.dm
new file mode 100644
index 0000000000000..b6ccf58183821
--- /dev/null
+++ b/code/modules/hallucination/hostile_mob.dm
@@ -0,0 +1,171 @@
+/* Hostile Mob Hallucinations
+ *
+ * Contains:
+ * Xeno
+ * Clown
+ * Bubblegum
+ */
+
+/obj/effect/hallucination/simple/xeno
+ image_icon = 'icons/mob/alien.dmi'
+ image_state = "alienh_pounce"
+
+/obj/effect/hallucination/simple/xeno/Initialize(mapload, mob/living/carbon/T)
+ . = ..()
+ name = "alien hunter ([rand(1, 1000)])"
+
+/obj/effect/hallucination/simple/xeno/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
+ update_icon(ALL, "alienh_pounce")
+ if(hit_atom == target && target.stat!=DEAD)
+ target.Paralyze(100)
+ target.visible_message(span_danger("[target] flails around wildly."),span_userdanger("[name] pounces on you!"))
+
+// The numbers of seconds it takes to get to each stage of the xeno attack choreography
+#define XENO_ATTACK_STAGE_LEAP_AT_TARGET 1
+#define XENO_ATTACK_STAGE_LEAP_AT_PUMP 2
+#define XENO_ATTACK_STAGE_CLIMB 3
+#define XENO_ATTACK_STAGE_FINISH 6
+
+/// Xeno crawls from nearby vent,jumps at you, and goes back in
+/datum/hallucination/xeno_attack
+ var/turf/pump_location = null
+ var/obj/effect/hallucination/simple/xeno/xeno = null
+ var/time_processing = 0
+ var/stage = XENO_ATTACK_STAGE_LEAP_AT_TARGET
+
+/datum/hallucination/xeno_attack/New(mob/living/carbon/C, forced = TRUE)
+ ..()
+ for(var/obj/machinery/atmospherics/components/unary/vent_pump/U in orange(7,target))
+ if(!U.welded)
+ pump_location = get_turf(U)
+ break
+
+ if(pump_location)
+ feedback_details += "Vent Coords: [pump_location.x],[pump_location.y],[pump_location.z]"
+ xeno = new(pump_location, target)
+ START_PROCESSING(SSfastprocess, src)
+ else
+ qdel(src)
+
+/datum/hallucination/xeno_attack/process(delta_time)
+ time_processing += delta_time
+
+ if (time_processing >= stage)
+ switch (time_processing)
+ if (XENO_ATTACK_STAGE_FINISH to INFINITY)
+ to_chat(target, span_notice("[xeno.name] scrambles into the ventilation ducts!"))
+ qdel(src)
+ if (XENO_ATTACK_STAGE_CLIMB to XENO_ATTACK_STAGE_FINISH)
+ to_chat(target, span_notice("[xeno.name] begins climbing into the ventilation system..."))
+ stage = XENO_ATTACK_STAGE_FINISH
+ if (XENO_ATTACK_STAGE_LEAP_AT_PUMP to XENO_ATTACK_STAGE_CLIMB)
+ xeno.update_icon(ALL, "alienh_leap", 'icons/mob/alienleap.dmi', -32, -32)
+ xeno.throw_at(pump_location, 7, 1, spin = FALSE, diagonals_first = TRUE)
+ stage = XENO_ATTACK_STAGE_CLIMB
+ if (XENO_ATTACK_STAGE_LEAP_AT_TARGET to XENO_ATTACK_STAGE_LEAP_AT_PUMP)
+ xeno.update_icon(ALL, "alienh_leap", 'icons/mob/alienleap.dmi', -32, -32)
+ xeno.throw_at(target, 7, 1, spin = FALSE, diagonals_first = TRUE)
+ stage = XENO_ATTACK_STAGE_LEAP_AT_PUMP
+
+/datum/hallucination/xeno_attack/Destroy()
+ . = ..()
+
+ STOP_PROCESSING(SSfastprocess, src)
+ QDEL_NULL(xeno)
+ pump_location = null
+
+#undef XENO_ATTACK_STAGE_LEAP_AT_TARGET
+#undef XENO_ATTACK_STAGE_LEAP_AT_PUMP
+#undef XENO_ATTACK_STAGE_CLIMB
+#undef XENO_ATTACK_STAGE_FINISH
+
+/obj/effect/hallucination/simple/clown
+ image_icon = 'icons/mob/animal.dmi'
+ image_state = "clown"
+
+/obj/effect/hallucination/simple/clown/Initialize(mapload, mob/living/carbon/T, duration)
+ ..(loc, T)
+ name = pick(GLOB.clown_names)
+ QDEL_IN(src,duration)
+
+/obj/effect/hallucination/simple/clown/scary
+ image_state = "scary_clown"
+
+/obj/effect/hallucination/simple/bubblegum
+ name = "Bubblegum"
+ image_icon = 'icons/mob/lavaland/96x96megafauna.dmi'
+ image_state = "bubblegum"
+ px = -32
+
+/datum/hallucination/oh_yeah
+ var/obj/effect/hallucination/simple/bubblegum/bubblegum
+ var/image/fakebroken
+ var/image/fakerune
+ var/turf/landing
+ var/charged
+ var/next_action = 0
+
+/datum/hallucination/oh_yeah/New(mob/living/carbon/C, forced = TRUE)
+ set waitfor = FALSE
+ . = ..()
+ var/turf/closed/wall/wall
+ for(var/turf/closed/wall/W in range(7,target))
+ wall = W
+ break
+ if(!wall)
+ return INITIALIZE_HINT_QDEL
+ feedback_details += "Source: [wall.x],[wall.y],[wall.z]"
+
+ fakebroken = image('icons/turf/floors.dmi', wall, "plating", layer = TURF_LAYER)
+ landing = get_turf(target)
+ var/turf/landing_image_turf = get_step(landing, SOUTHWEST) //the icon is 3x3
+ fakerune = image('icons/effects/96x96.dmi', landing_image_turf, "landing", layer = ABOVE_OPEN_TURF_LAYER)
+ fakebroken.override = TRUE
+ if(target.client)
+ target.client.images |= fakebroken
+ target.client.images |= fakerune
+ target.playsound_local(wall,'sound/effects/meteorimpact.ogg', 150, 1)
+ bubblegum = new(wall, target)
+ addtimer(CALLBACK(src, .proc/start_processing), 10)
+
+/datum/hallucination/oh_yeah/proc/start_processing()
+ if (isnull(target))
+ qdel(src)
+ return
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/hallucination/oh_yeah/process(delta_time)
+ next_action -= delta_time
+
+ if (next_action > 0)
+ return
+
+ if (get_turf(bubblegum) != landing && target?.stat != DEAD)
+ if(!landing || (get_turf(bubblegum)).loc.z != landing.loc.z)
+ qdel(src)
+ return
+ bubblegum.forceMove(get_step_towards(bubblegum, landing))
+ bubblegum.setDir(get_dir(bubblegum, landing))
+ target.playsound_local(get_turf(bubblegum), 'sound/effects/meteorimpact.ogg', 150, 1)
+ shake_camera(target, 2, 1)
+ if(bubblegum.Adjacent(target) && !charged)
+ charged = TRUE
+ target.Paralyze(80)
+ target.adjustStaminaLoss(40)
+ step_away(target, bubblegum)
+ shake_camera(target, 4, 3)
+ target.visible_message(span_warning("[target] jumps backwards, falling on the ground!"),span_userdanger("[bubblegum] slams into you!"))
+ next_action = 0.2
+ else
+ STOP_PROCESSING(SSfastprocess, src)
+ QDEL_IN(src, 3 SECONDS)
+
+/datum/hallucination/oh_yeah/Destroy()
+ if(target.client)
+ target.client.images.Remove(fakebroken)
+ target.client.images.Remove(fakerune)
+ QDEL_NULL(fakebroken)
+ QDEL_NULL(fakerune)
+ QDEL_NULL(bubblegum)
+ STOP_PROCESSING(SSfastprocess, src)
+ return ..()
diff --git a/code/modules/hallucination/husk.dm b/code/modules/hallucination/husk.dm
new file mode 100644
index 0000000000000..5bbb94af5b669
--- /dev/null
+++ b/code/modules/hallucination/husk.dm
@@ -0,0 +1,31 @@
+/datum/hallucination/husks
+ var/image/halbody
+
+/datum/hallucination/husks/New(mob/living/carbon/C, forced = TRUE)
+ set waitfor = FALSE
+ ..()
+ var/list/possible_points = list()
+ for(var/turf/open/floor/F in view(target,world.view))
+ possible_points += F
+ if(possible_points.len)
+ var/turf/open/floor/husk_point = pick(possible_points)
+ switch(rand(1,4))
+ if(1)
+ var/image/body = image('icons/mob/human.dmi',husk_point,"husk",TURF_LAYER)
+ var/matrix/M = matrix()
+ M.Turn(90)
+ body.transform = M
+ halbody = body
+ if(2,3)
+ halbody = image('icons/mob/human.dmi',husk_point,"husk",TURF_LAYER)
+ if(4)
+ halbody = image('icons/mob/alien.dmi',husk_point,"alienother",TURF_LAYER)
+
+ if(target.client)
+ target.client.images += halbody
+ QDEL_IN(src, rand(30,50)) //Only seen for a brief moment.
+
+/datum/hallucination/husks/Destroy()
+ target?.client?.images -= halbody
+ QDEL_NULL(halbody)
+ return ..()
diff --git a/code/modules/hallucination/item.dm b/code/modules/hallucination/item.dm
new file mode 100644
index 0000000000000..dd462f2b15a1b
--- /dev/null
+++ b/code/modules/hallucination/item.dm
@@ -0,0 +1,172 @@
+/* Item Hallucinations
+ *
+ * Contains:
+ * Putting items in other nearby peoples hands
+ * Putting items in your hands
+ */
+
+/datum/hallucination/items_other
+
+/datum/hallucination/items_other/New(mob/living/carbon/C, forced = TRUE, item_type)
+ set waitfor = FALSE
+ ..()
+ var/item
+ if(!item_type)
+ item = pick(list("esword","taser","ebow","baton","dual_esword","ttv","flash","armblade"))
+ else
+ item = item_type
+ feedback_details += "Item: [item]"
+ var/side
+ var/image_file
+ var/image/A = null
+ var/list/mob_pool = list()
+
+ for(var/mob/living/carbon/human/M in view(7,target))
+ if(M != target)
+ mob_pool += M
+ if(!mob_pool.len)
+ return
+
+ var/mob/living/carbon/human/H = pick(mob_pool)
+ feedback_details += " Mob: [H.real_name]"
+
+ var/free_hand = H.get_empty_held_index_for_side(LEFT_HANDS)
+ if(free_hand)
+ side = "left"
+ else
+ free_hand = H.get_empty_held_index_for_side(RIGHT_HANDS)
+ if(free_hand)
+ side = "right"
+
+ if(side)
+ switch(item)
+ if("esword")
+ if(side == "right")
+ image_file = 'icons/mob/inhands/weapons/swords_righthand.dmi'
+ else
+ image_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi'
+ target.playsound_local(H, 'sound/weapons/saberon.ogg',35,1)
+ A = image(image_file,H,"e_sword_on_red", layer=ABOVE_MOB_LAYER)
+ if("dual_esword")
+ if(side == "right")
+ image_file = 'icons/mob/inhands/weapons/swords_righthand.dmi'
+ else
+ image_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi'
+ target.playsound_local(H, 'sound/weapons/saberon.ogg',35,1)
+ A = image(image_file,H,"dualsaberred1", layer=ABOVE_MOB_LAYER)
+ if("taser")
+ if(side == "right")
+ image_file = 'icons/mob/inhands/weapons/guns_righthand.dmi'
+ else
+ image_file = 'icons/mob/inhands/weapons/guns_lefthand.dmi'
+ A = image(image_file,H,"advtaserstun4", layer=ABOVE_MOB_LAYER)
+ if("ebow")
+ if(side == "right")
+ image_file = 'icons/mob/inhands/weapons/guns_righthand.dmi'
+ else
+ image_file = 'icons/mob/inhands/weapons/guns_lefthand.dmi'
+ A = image(image_file,H,"crossbow", layer=ABOVE_MOB_LAYER)
+ if("baton")
+ if(side == "right")
+ image_file = 'icons/mob/inhands/equipment/security_righthand.dmi'
+ else
+ image_file = 'icons/mob/inhands/equipment/security_lefthand.dmi'
+ target.playsound_local(H, SFX_SPARKS,75,1,-1)
+ A = image(image_file,H,"baton", layer=ABOVE_MOB_LAYER)
+ if("ttv")
+ if(side == "right")
+ image_file = 'icons/mob/inhands/weapons/bombs_righthand.dmi'
+ else
+ image_file = 'icons/mob/inhands/weapons/bombs_lefthand.dmi'
+ A = image(image_file,H,"ttv", layer=ABOVE_MOB_LAYER)
+ if("flash")
+ if(side == "right")
+ image_file = 'icons/mob/inhands/equipment/security_righthand.dmi'
+ else
+ image_file = 'icons/mob/inhands/equipment/security_lefthand.dmi'
+ A = image(image_file,H,"flashtool", layer=ABOVE_MOB_LAYER)
+ if("armblade")
+ if(side == "right")
+ image_file = 'icons/mob/inhands/antag/changeling_righthand.dmi'
+ else
+ image_file = 'icons/mob/inhands/antag/changeling_lefthand.dmi'
+ target.playsound_local(H, 'sound/effects/blobattack.ogg',30,1)
+ A = image(image_file,H,"arm_blade", layer=ABOVE_MOB_LAYER)
+ if(target.client)
+ target.client.images |= A
+ addtimer(CALLBACK(src, .proc/cleanup, item, A, H), rand(15 SECONDS, 25 SECONDS))
+ return
+ qdel(src)
+
+/datum/hallucination/items_other/proc/cleanup(item, atom/image_used, has_the_item)
+ if (isnull(target))
+ qdel(src)
+ return
+ if(item == "esword" || item == "dual_esword")
+ target.playsound_local(has_the_item, 'sound/weapons/saberoff.ogg',35,1)
+ if(item == "armblade")
+ target.playsound_local(has_the_item, 'sound/effects/blobattack.ogg',30,1)
+ target.client.images.Remove(image_used)
+ qdel(src)
+
+/datum/hallucination/items/New(mob/living/carbon/C, forced = TRUE)
+ set waitfor = FALSE
+ ..()
+ //Strange items
+
+ var/obj/halitem = new
+
+ halitem = new
+ var/obj/item/l_hand = target.get_item_for_held_index(1)
+ var/obj/item/r_hand = target.get_item_for_held_index(2)
+ var/l = ui_hand_position(target.get_held_index_of_item(l_hand))
+ var/r = ui_hand_position(target.get_held_index_of_item(r_hand))
+ var/list/slots_free = list(l,r)
+ if(l_hand)
+ slots_free -= l
+ if(r_hand)
+ slots_free -= r
+ if(ishuman(target))
+ var/mob/living/carbon/human/H = target
+ if(!H.belt)
+ slots_free += ui_belt
+ if(!H.l_store)
+ slots_free += ui_storage1
+ if(!H.r_store)
+ slots_free += ui_storage2
+ if(slots_free.len)
+ halitem.screen_loc = pick(slots_free)
+ halitem.plane = ABOVE_HUD_PLANE
+ switch(rand(1,6))
+ if(1) //revolver
+ halitem.icon = 'icons/obj/guns/ballistic.dmi'
+ halitem.icon_state = "revolver"
+ halitem.name = "Revolver"
+ if(2) //c4
+ halitem.icon = 'icons/obj/grenade.dmi'
+ halitem.icon_state = "plastic-explosive0"
+ halitem.name = "C4"
+ if(prob(25))
+ halitem.icon_state = "plasticx40"
+ if(3) //sword
+ halitem.icon = 'icons/obj/transforming_energy.dmi'
+ halitem.icon_state = "e_sword"
+ halitem.name = "energy sword"
+ if(4) //stun baton
+ halitem.icon = 'icons/obj/items_and_weapons.dmi'
+ halitem.icon_state = "stunbaton"
+ halitem.name = "Stun Baton"
+ if(5) //emag
+ halitem.icon = 'icons/obj/card.dmi'
+ halitem.icon_state = "emag"
+ halitem.name = "Cryptographic Sequencer"
+ if(6) //flashbang
+ halitem.icon = 'icons/obj/grenade.dmi'
+ halitem.icon_state = "flashbang1"
+ halitem.name = "Flashbang"
+ feedback_details += "Type: [halitem.name]"
+ if(target.client)
+ target.client.screen += halitem
+ QDEL_IN(halitem, rand(150, 350))
+
+ qdel(src)
diff --git a/code/modules/hallucination/plasma_flood.dm b/code/modules/hallucination/plasma_flood.dm
new file mode 100644
index 0000000000000..ea76106231cd2
--- /dev/null
+++ b/code/modules/hallucination/plasma_flood.dm
@@ -0,0 +1,83 @@
+#define FAKE_FLOOD_EXPAND_TIME 20
+#define FAKE_FLOOD_MAX_RADIUS 10
+
+/obj/effect/plasma_image_holder
+ icon_state = "nothing"
+ anchored = TRUE
+ layer = FLY_LAYER
+ plane = ABOVE_GAME_PLANE
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+
+/datum/hallucination/fake_flood
+ //Plasma starts flooding from the nearby vent
+ var/turf/center
+ var/list/flood_images = list()
+ var/list/flood_image_holders = list()
+ var/list/turf/flood_turfs = list()
+ var/image_icon = 'icons/effects/atmospherics.dmi'
+ var/image_state = "plasma"
+ var/radius = 0
+ var/next_expand = 0
+
+/datum/hallucination/fake_flood/New(mob/living/carbon/C, forced = TRUE)
+ ..()
+ for(var/obj/machinery/atmospherics/components/unary/vent_pump/U in orange(7,target))
+ if(!U.welded)
+ center = get_turf(U)
+ break
+ if(!center)
+ qdel(src)
+ return
+ feedback_details += "Vent Coords: [center.x],[center.y],[center.z]"
+ var/obj/effect/plasma_image_holder/pih = new(center)
+ var/image/plasma_image = image(image_icon, pih, image_state, FLY_LAYER)
+ plasma_image.alpha = 50
+ plasma_image.plane = ABOVE_GAME_PLANE
+ flood_images += plasma_image
+ flood_image_holders += pih
+ flood_turfs += center
+ if(target.client)
+ target.client.images |= flood_images
+ next_expand = world.time + FAKE_FLOOD_EXPAND_TIME
+ START_PROCESSING(SSobj, src)
+
+/datum/hallucination/fake_flood/process()
+ if(next_expand <= world.time)
+ radius++
+ if(radius > FAKE_FLOOD_MAX_RADIUS)
+ qdel(src)
+ return
+ Expand()
+ if((get_turf(target) in flood_turfs) && !target.internal)
+ new /datum/hallucination/fake_alert(target, TRUE, ALERT_TOO_MUCH_PLASMA)
+ next_expand = world.time + FAKE_FLOOD_EXPAND_TIME
+
+/datum/hallucination/fake_flood/proc/Expand()
+ for(var/image/I in flood_images)
+ I.alpha = min(I.alpha + 50, 255)
+ for(var/turf/FT in flood_turfs)
+ for(var/dir in GLOB.cardinals)
+ var/turf/T = get_step(FT, dir)
+ if((T in flood_turfs) || !TURFS_CAN_SHARE(T, FT) || isspaceturf(T)) //If we've gottem already, or if they're not alright to spread with.
+ continue
+ var/obj/effect/plasma_image_holder/pih = new(T)
+ var/image/new_plasma = image(image_icon, pih, image_state, FLY_LAYER)
+ new_plasma.alpha = 50
+ new_plasma.plane = ABOVE_GAME_PLANE
+ flood_images += new_plasma
+ flood_image_holders += pih
+ flood_turfs += T
+ if(target.client)
+ target.client.images |= flood_images
+
+/datum/hallucination/fake_flood/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ qdel(flood_turfs)
+ flood_turfs = list()
+ if(target.client)
+ target.client.images.Remove(flood_images)
+ qdel(flood_images)
+ flood_images = list()
+ qdel(flood_image_holders)
+ flood_image_holders = list()
+ return ..()
diff --git a/code/modules/hallucination/polymorph.dm b/code/modules/hallucination/polymorph.dm
new file mode 100644
index 0000000000000..2ad4471f1199b
--- /dev/null
+++ b/code/modules/hallucination/polymorph.dm
@@ -0,0 +1,101 @@
+/* Polymorph Hallucinations
+ *
+ * Contains:
+ * Nearby mobs are polymorphed into other creatures
+ * Polymorphing yourself into other creatures
+ */
+/datum/hallucination/delusion
+ var/list/image/delusions = list()
+
+/datum/hallucination/delusion/New(mob/living/carbon/C, forced, force_kind = null , duration = 300,skip_nearby = TRUE, custom_icon = null, custom_icon_file = null, custom_name = null)
+ set waitfor = FALSE
+ . = ..()
+ var/image/A = null
+ var/kind = force_kind ? force_kind : pick("nothing","monkey","corgi","carp","skeleton","demon","zombie")
+ feedback_details += "Type: [kind]"
+ var/list/nearby
+ if(skip_nearby)
+ nearby = get_hearers_in_view(7, target)
+ for(var/mob/living/carbon/human/H in GLOB.alive_mob_list)
+ if(H == target)
+ continue
+ if(skip_nearby && (H in nearby))
+ continue
+ switch(kind)
+ if("nothing")
+ A = image('icons/effects/effects.dmi',H,"nothing")
+ A.name = "..."
+ if("monkey")//Monkey
+ A = image('icons/mob/human.dmi',H,"monkey")
+ A.name = "Monkey ([rand(1,999)])"
+ if("carp")//Carp
+ A = image('icons/mob/carp.dmi',H,"carp")
+ A.name = "Space Carp"
+ if("corgi")//Corgi
+ A = image('icons/mob/pets.dmi',H,"corgi")
+ A.name = "Corgi"
+ if("skeleton")//Skeletons
+ A = image('icons/mob/human.dmi',H,"skeleton")
+ A.name = "Skeleton"
+ if("zombie")//Zombies
+ A = image('icons/mob/human.dmi',H,"zombie")
+ A.name = "Zombie"
+ if("demon")//Demon
+ A = image('icons/mob/mob.dmi',H,"daemon")
+ A.name = "Demon"
+ if("custom")
+ A = image(custom_icon_file, H, custom_icon)
+ A.name = custom_name
+ A.override = 1
+ if(target.client)
+ delusions |= A
+ target.client.images |= A
+ if(duration)
+ QDEL_IN(src, duration)
+
+/datum/hallucination/delusion/Destroy()
+ for(var/image/I in delusions)
+ if(target.client)
+ target.client.images.Remove(I)
+ return ..()
+
+/datum/hallucination/self_delusion
+ var/image/delusion
+
+/datum/hallucination/self_delusion/New(mob/living/carbon/C, forced, force_kind = null , duration = 300, custom_icon = null, custom_icon_file = null, wabbajack = TRUE) //set wabbajack to false if you want to use another fake source
+ set waitfor = FALSE
+ ..()
+ var/image/A = null
+ var/kind = force_kind ? force_kind : pick("monkey","corgi","carp","skeleton","demon","zombie","robot")
+ feedback_details += "Type: [kind]"
+ switch(kind)
+ if("monkey")//Monkey
+ A = image('icons/mob/human.dmi',target,"monkey")
+ if("carp")//Carp
+ A = image('icons/mob/animal.dmi',target,"carp")
+ if("corgi")//Corgi
+ A = image('icons/mob/pets.dmi',target,"corgi")
+ if("skeleton")//Skeletons
+ A = image('icons/mob/human.dmi',target,"skeleton")
+ if("zombie")//Zombies
+ A = image('icons/mob/human.dmi',target,"zombie")
+ if("demon")//Demon
+ A = image('icons/mob/mob.dmi',target,"daemon")
+ if("robot")//Cyborg
+ A = image('icons/mob/robots.dmi',target,"robot")
+ target.playsound_local(target,'sound/voice/liveagain.ogg', 75, 1)
+ if("custom")
+ A = image(custom_icon_file, target, custom_icon)
+ A.override = 1
+ if(target.client)
+ if(wabbajack)
+ to_chat(target, span_hear("...wabbajack...wabbajack..."))
+ target.playsound_local(target,'sound/magic/staff_change.ogg', 50, 1)
+ delusion = A
+ target.client.images |= A
+ QDEL_IN(src, duration)
+
+/datum/hallucination/self_delusion/Destroy()
+ if(target.client)
+ target.client.images.Remove(delusion)
+ return ..()
diff --git a/code/modules/hallucination/shock.dm b/code/modules/hallucination/shock.dm
new file mode 100644
index 0000000000000..eb8a425736680
--- /dev/null
+++ b/code/modules/hallucination/shock.dm
@@ -0,0 +1,31 @@
+/datum/hallucination/shock
+ var/image/shock_image
+ var/image/electrocution_skeleton_anim
+
+/datum/hallucination/shock/New(mob/living/carbon/C, forced = TRUE)
+ set waitfor = FALSE
+ ..()
+ shock_image = image(target, target, dir = target.dir)
+ shock_image.appearance_flags |= KEEP_APART
+ shock_image.color = rgb(0,0,0)
+ shock_image.override = TRUE
+ electrocution_skeleton_anim = image('icons/mob/human.dmi', target, icon_state = "electrocuted_base", layer=ABOVE_MOB_LAYER)
+ electrocution_skeleton_anim.appearance_flags |= RESET_COLOR|KEEP_APART
+ to_chat(target, span_userdanger("You feel a powerful shock course through your body!"))
+ if(target.client)
+ target.client.images |= shock_image
+ target.client.images |= electrocution_skeleton_anim
+ addtimer(CALLBACK(src, .proc/reset_shock_animation), 40)
+ target.playsound_local(get_turf(src), SFX_SPARKS, 100, 1)
+ target.staminaloss += 50
+ target.Stun(4 SECONDS)
+ target.do_jitter_animation(300) // Maximum jitter
+ target.adjust_timed_status_effect(20 SECONDS, /datum/status_effect/jitter)
+ addtimer(CALLBACK(src, .proc/shock_drop), 2 SECONDS)
+
+/datum/hallucination/shock/proc/reset_shock_animation()
+ target.client?.images.Remove(shock_image)
+ target.client?.images.Remove(electrocution_skeleton_anim)
+
+/datum/hallucination/shock/proc/shock_drop()
+ target.Paralyze(6 SECONDS)
diff --git a/code/modules/hallucination/sound.dm b/code/modules/hallucination/sound.dm
new file mode 100644
index 0000000000000..00dc752a19113
--- /dev/null
+++ b/code/modules/hallucination/sound.dm
@@ -0,0 +1,243 @@
+/* Sound Hallucinations
+ *
+ * Contains:
+ * Fighting sounds
+ * Machinery sounds
+ * Special effects sounds
+ */
+
+/datum/hallucination/battle
+ var/battle_type
+ var/iterations_left
+ var/hits = 0
+ var/next_action = 0
+ var/turf/source
+
+/datum/hallucination/battle/New(mob/living/carbon/C, forced = TRUE, new_battle_type)
+ ..()
+
+ source = random_far_turf()
+
+ battle_type = new_battle_type
+ if (isnull(battle_type))
+ battle_type = pick("laser", "disabler", "esword", "gun", "stunprod", "harmbaton", "bomb")
+ feedback_details += "Type: [battle_type]"
+ var/process = TRUE
+
+ switch(battle_type)
+ if("disabler", "laser")
+ iterations_left = rand(5, 10)
+ if("esword")
+ iterations_left = rand(4, 8)
+ target.playsound_local(source, 'sound/weapons/saberon.ogg',15, 1)
+ if("gun")
+ iterations_left = rand(3, 6)
+ if("stunprod") //Stunprod + cablecuff
+ process = FALSE
+ target.playsound_local(source, 'sound/weapons/egloves.ogg', 40, 1)
+ target.playsound_local(source, get_sfx(SFX_BODYFALL), 25, 1)
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, 'sound/weapons/cablecuff.ogg', 15, 1), 20)
+ if("harmbaton") //zap n slap
+ iterations_left = rand(5, 12)
+ target.playsound_local(source, 'sound/weapons/egloves.ogg', 40, 1)
+ target.playsound_local(source, get_sfx(SFX_BODYFALL), 25, 1)
+ next_action = 2 SECONDS
+ if("bomb") // Tick Tock
+ iterations_left = rand(3, 11)
+
+ if (process)
+ START_PROCESSING(SSfastprocess, src)
+ else
+ qdel(src)
+
+/datum/hallucination/battle/process(delta_time)
+ next_action -= (delta_time * 10)
+
+ if (next_action > 0)
+ return
+
+ switch (battle_type)
+ if ("disabler", "laser", "gun")
+ var/fire_sound
+ var/hit_person_sound
+ var/hit_wall_sound
+ var/number_of_hits
+ var/chance_to_fall
+
+ switch (battle_type)
+ if ("disabler")
+ fire_sound = 'sound/weapons/taser2.ogg'
+ hit_person_sound = 'sound/weapons/tap.ogg'
+ hit_wall_sound = 'sound/weapons/effects/searwall.ogg'
+ number_of_hits = 3
+ chance_to_fall = 70
+ if ("laser")
+ fire_sound = 'sound/weapons/laser.ogg'
+ hit_person_sound = 'sound/weapons/sear.ogg'
+ hit_wall_sound = 'sound/weapons/effects/searwall.ogg'
+ number_of_hits = 4
+ chance_to_fall = 70
+ if ("gun")
+ fire_sound = 'sound/weapons/gun/shotgun/shot.ogg'
+ hit_person_sound = 'sound/weapons/pierce.ogg'
+ hit_wall_sound = SFX_RICOCHET
+ number_of_hits = 2
+ chance_to_fall = 80
+
+ target.playsound_local(source, fire_sound, 25, 1)
+
+ if(prob(50))
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, hit_person_sound, 25, 1), rand(5,10))
+ hits += 1
+ else
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, hit_wall_sound, 25, 1), rand(5,10))
+
+ next_action = rand(CLICK_CD_RANGE, CLICK_CD_RANGE + 6)
+
+ if(hits >= number_of_hits && prob(chance_to_fall))
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, get_sfx(SFX_BODYFALL), 25, 1), next_action)
+ qdel(src)
+ return
+ if ("esword")
+ target.playsound_local(source, 'sound/weapons/blade1.ogg', 50, 1)
+
+ if (hits == 4)
+ target.playsound_local(source, get_sfx(SFX_BODYFALL), 25, 1)
+
+ next_action = rand(CLICK_CD_MELEE, CLICK_CD_MELEE + 6)
+ hits += 1
+
+ if (iterations_left == 1)
+ target.playsound_local(source, 'sound/weapons/saberoff.ogg', 15, 1)
+ if ("harmbaton")
+ target.playsound_local(source, SFX_SWING_HIT, 50, 1)
+ next_action = rand(CLICK_CD_MELEE, CLICK_CD_MELEE + 4)
+ if ("bomb")
+ target.playsound_local(source, 'sound/items/timer.ogg', 25, 0)
+ next_action = 15
+
+ iterations_left -= 1
+ if (iterations_left == 0)
+ qdel(src)
+
+/datum/hallucination/battle/Destroy()
+ . = ..()
+ source = null
+ STOP_PROCESSING(SSfastprocess, src)
+
+/datum/hallucination/sounds
+
+/datum/hallucination/sounds/New(mob/living/carbon/C, forced = TRUE, sound_type)
+ set waitfor = FALSE
+ ..()
+ var/turf/source = random_far_turf()
+ if(!sound_type)
+ sound_type = pick("airlock","airlock pry","console","explosion","far explosion","mech","glass","alarm","beepsky","mech","wall decon","door hack")
+ feedback_details += "Type: [sound_type]"
+ //Strange audio
+ switch(sound_type)
+ if("airlock")
+ target.playsound_local(source,'sound/machines/airlock.ogg', 30, 1)
+ if("airlock pry")
+ target.playsound_local(source,'sound/machines/airlock_alien_prying.ogg', 100, 1)
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, 'sound/machines/airlockforced.ogg', 30, 1), 50)
+ if("console")
+ target.playsound_local(source,'sound/machines/terminal_prompt.ogg', 25, 1)
+ if("explosion")
+ if(prob(50))
+ target.playsound_local(source,'sound/effects/explosion1.ogg', 50, 1)
+ else
+ target.playsound_local(source, 'sound/effects/explosion2.ogg', 50, 1)
+ if("far explosion")
+ target.playsound_local(source, 'sound/effects/explosionfar.ogg', 50, 1)
+ if("glass")
+ target.playsound_local(source, pick('sound/effects/glassbr1.ogg','sound/effects/glassbr2.ogg','sound/effects/glassbr3.ogg'), 50, 1)
+ if("alarm")
+ target.playsound_local(source, 'sound/machines/alarm.ogg', 100, 0)
+ if("beepsky")
+ target.playsound_local(source, 'sound/voice/beepsky/freeze.ogg', 35, 0)
+ if("mech")
+ new /datum/hallucination/mech_sounds(C, forced, sound_type)
+ //Deconstructing a wall
+ if("wall decon")
+ target.playsound_local(source, 'sound/items/welder.ogg', 50, 1)
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, 'sound/items/welder2.ogg', 50, 1), 105)
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, 'sound/items/ratchet.ogg', 50, 1), 120)
+ //Hacking a door
+ if("door hack")
+ target.playsound_local(source, 'sound/items/screwdriver.ogg', 50, 1)
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, 'sound/machines/airlockforced.ogg', 30, 1), rand(40, 80))
+ qdel(src)
+
+/datum/hallucination/mech_sounds
+ var/mech_dir
+ var/steps_left
+ var/next_action = 0
+ var/turf/source
+
+/datum/hallucination/mech_sounds/New()
+ . = ..()
+ mech_dir = pick(GLOB.cardinals)
+ steps_left = rand(4, 9)
+ source = random_far_turf()
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/hallucination/mech_sounds/process(delta_time)
+ next_action -= delta_time
+ if (next_action > 0)
+ return
+
+ if(prob(75))
+ target.playsound_local(source, 'sound/mecha/mechstep.ogg', 40, 1)
+ source = get_step(source, mech_dir)
+ else
+ target.playsound_local(source, 'sound/mecha/mechturn.ogg', 40, 1)
+ mech_dir = pick(GLOB.cardinals)
+
+ steps_left -= 1
+ if (!steps_left)
+ qdel(src)
+ return
+ next_action = 1
+
+/datum/hallucination/mech_sounds/Destroy()
+ . = ..()
+ STOP_PROCESSING(SSfastprocess, src)
+
+/datum/hallucination/weird_sounds
+
+/datum/hallucination/weird_sounds/New(mob/living/carbon/C, forced = TRUE, sound_type)
+ set waitfor = FALSE
+ ..()
+ var/turf/source = random_far_turf()
+ if(!sound_type)
+ sound_type = pick("phone","hallelujah","highlander","laughter","hyperspace","game over","creepy","tesla")
+ feedback_details += "Type: [sound_type]"
+ //Strange audio
+ switch(sound_type)
+ if("phone")
+ target.playsound_local(source, 'sound/weapons/ring.ogg', 15)
+ for (var/next_rings in 1 to 3)
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, 'sound/weapons/ring.ogg', 15), 25 * next_rings)
+ if("hyperspace")
+ target.playsound_local(null, 'sound/runtime/hyperspace/hyperspace_begin.ogg', 50)
+ if("hallelujah")
+ target.playsound_local(source, 'sound/effects/pray_chaplain.ogg', 50)
+ if("highlander")
+ target.playsound_local(null, 'sound/misc/highlander.ogg', 50)
+ if("game over")
+ target.playsound_local(source, 'sound/misc/compiler-failure.ogg', 50)
+ if("laughter")
+ if(prob(50))
+ target.playsound_local(source, 'sound/voice/human/womanlaugh.ogg', 50, 1)
+ else
+ target.playsound_local(source, pick('sound/voice/human/manlaugh1.ogg', 'sound/voice/human/manlaugh2.ogg'), 50, 1)
+ if("creepy")
+ //These sounds are (mostly) taken from Hidden: Source
+ target.playsound_local(source, pick(GLOB.creepy_ambience), 50, 1)
+ if("tesla") //Tesla loose!
+ target.playsound_local(source, 'sound/magic/lightningbolt.ogg', 35, 1)
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, 'sound/magic/lightningbolt.ogg', 65, 1), 30)
+ addtimer(CALLBACK(target, /mob/.proc/playsound_local, source, 'sound/magic/lightningbolt.ogg', 100, 1), 60)
+
+ qdel(src)
diff --git a/code/modules/hallucination/stray_bullet.dm b/code/modules/hallucination/stray_bullet.dm
new file mode 100644
index 0000000000000..b0fba6d724a93
--- /dev/null
+++ b/code/modules/hallucination/stray_bullet.dm
@@ -0,0 +1,21 @@
+//hallucination projectile code in code/modules/projectiles/projectile/special.dm
+/datum/hallucination/stray_bullet
+
+/datum/hallucination/stray_bullet/New(mob/living/carbon/C, forced = TRUE)
+ set waitfor = FALSE
+ ..()
+ var/list/turf/startlocs = list()
+ for(var/turf/open/T in view(world.view+1,target)-view(world.view,target))
+ startlocs += T
+ if(!startlocs.len)
+ qdel(src)
+ return
+ var/turf/start = pick(startlocs)
+ var/proj_type = pick(subtypesof(/obj/projectile/hallucination))
+ feedback_details += "Type: [proj_type]"
+ var/obj/projectile/hallucination/H = new proj_type(start)
+ target.playsound_local(start, H.hal_fire_sound, 60, 1)
+ H.hal_target = target
+ H.preparePixelProjectile(target, start)
+ H.fire()
+ qdel(src)
diff --git a/code/modules/hydroponics/grown/chili.dm b/code/modules/hydroponics/grown/chili.dm
index c6fef467a7dd9..3691d5734257c 100644
--- a/code/modules/hydroponics/grown/chili.dm
+++ b/code/modules/hydroponics/grown/chili.dm
@@ -106,4 +106,4 @@
foodtypes = FRUIT
/obj/item/food/grown/bell_pepper/MakeBakeable()
- AddComponent(/datum/component/bakeable, /obj/item/food/roasted_bell_pepper, rand(15 SECONDS, 35 SECONDS), TRUE, TRUE)
+ AddComponent(/datum/component/bakeable, /obj/item/food/roasted_bell_pepper, rand(15 SECONDS, 25 SECONDS), TRUE, TRUE)
diff --git a/code/modules/hydroponics/grown/corn.dm b/code/modules/hydroponics/grown/corn.dm
index 101ed1d08fb58..de65b87928014 100644
--- a/code/modules/hydroponics/grown/corn.dm
+++ b/code/modules/hydroponics/grown/corn.dm
@@ -25,12 +25,13 @@
trash_type = /obj/item/grown/corncob
bite_consumption_mod = 2
foodtypes = VEGETABLES
+ grind_results = list(/datum/reagent/consumable/cornmeal = 0)
juice_results = list(/datum/reagent/consumable/corn_starch = 0)
tastes = list("corn" = 1)
distill_reagent = /datum/reagent/consumable/ethanol/whiskey
/obj/item/food/grown/corn/MakeBakeable()
- AddComponent(/datum/component/bakeable, /obj/item/food/oven_baked_corn, rand(15 SECONDS, 35 SECONDS), TRUE, TRUE)
+ AddComponent(/datum/component/bakeable, /obj/item/food/oven_baked_corn, rand(15 SECONDS, 25 SECONDS), TRUE, TRUE)
/obj/item/grown/corncob
name = "corn cob"
diff --git a/code/modules/hydroponics/grown/olive.dm b/code/modules/hydroponics/grown/olive.dm
new file mode 100644
index 0000000000000..8f93a9695ecfb
--- /dev/null
+++ b/code/modules/hydroponics/grown/olive.dm
@@ -0,0 +1,27 @@
+// Olive
+/obj/item/seeds/olive
+ name = "pack of olive seeds"
+ desc = "These seeds grow into olive trees."
+ icon_state = "seed-olive"
+ species = "olive"
+ plantname = "Olive Tree"
+ product = /obj/item/food/grown/olive
+ lifespan = 150
+ endurance = 35
+ yield = 5
+ maturation = 10
+ growing_icon = 'icons/obj/hydroponics/growing_fruits.dmi'
+ icon_grow = "olive-grow"
+ icon_dead = "olive-dead"
+ genes = list(/datum/plant_gene/trait/repeated_harvest, /datum/plant_gene/trait/one_bite)
+ reagents_add = list(/datum/reagent/consumable/nutriment/vitamin = 0.04, /datum/reagent/consumable/nutriment = 0.1)
+
+/obj/item/food/grown/olive
+ seed = /obj/item/seeds/olive
+ name = "olive"
+ desc = "A small cylindrical salty fruit closely related to mangoes. Can be ground into a paste and mixed with water to make quality oil."
+ icon_state = "olive"
+ foodtypes = FRUIT
+ grind_results = list(/datum/reagent/consumable/olivepaste = 0)
+ tastes = list("olive" = 1)
+
diff --git a/code/modules/hydroponics/grown/onion.dm b/code/modules/hydroponics/grown/onion.dm
index 93940e1a82cf7..38320df982e44 100644
--- a/code/modules/hydroponics/grown/onion.dm
+++ b/code/modules/hydroponics/grown/onion.dm
@@ -53,7 +53,7 @@
var/datum/effect_system/fluid_spread/smoke/chem/cry_about_it = new //Since the onion is destroyed when it's sliced,
var/splat_location = get_turf(src) //we need to set up the smoke beforehand
cry_about_it.attach(splat_location)
- cry_about_it.set_up(0, location = splat_location, carry = reagents, silent = FALSE)
+ cry_about_it.set_up(0, holder = src, location = splat_location, carry = reagents, silent = FALSE)
cry_about_it.start()
qdel(cry_about_it)
return ..()
diff --git a/code/modules/hydroponics/grown/weeds/kudzu.dm b/code/modules/hydroponics/grown/weeds/kudzu.dm
index b965a5fbffb1f..45be37cd671fc 100644
--- a/code/modules/hydroponics/grown/weeds/kudzu.dm
+++ b/code/modules/hydroponics/grown/weeds/kudzu.dm
@@ -54,7 +54,7 @@
var/output_message = ""
for(var/datum/spacevine_mutation/SM in mutations)
kudzu_mutations += "[(kudzu_mutations == "") ? "" : ", "][SM.name]"
- output_message += "- Plant Mutations: [(kudzu_mutations == "") ? "None." : "[kudzu_mutations]."]"
+ output_message += "Plant Mutations: [(kudzu_mutations == "") ? "None." : "[kudzu_mutations]."]"
return output_message
/obj/item/seeds/kudzu/on_chem_reaction(datum/reagents/reagents)
diff --git a/code/modules/hydroponics/hydroitemdefines.dm b/code/modules/hydroponics/hydroitemdefines.dm
index 19ba2b4377f9a..aa1c332029334 100644
--- a/code/modules/hydroponics/hydroitemdefines.dm
+++ b/code/modules/hydroponics/hydroitemdefines.dm
@@ -74,19 +74,19 @@
*/
/obj/item/plant_analyzer/proc/do_plant_stats_scan(atom/scan_target, mob/user)
if(istype(scan_target, /obj/machinery/hydroponics))
- to_chat(user, scan_tray_stats(scan_target))
+ to_chat(user, examine_block(scan_tray_stats(scan_target)))
return TRUE
if(istype(scan_target, /obj/structure/glowshroom))
var/obj/structure/glowshroom/shroom_plant = scan_target
- to_chat(user, scan_plant_stats(shroom_plant.myseed))
+ to_chat(user, examine_block(scan_plant_stats(shroom_plant.myseed)))
return TRUE
if(istype(scan_target, /obj/item/graft))
- to_chat(user, get_graft_text(scan_target))
+ to_chat(user, examine_block(get_graft_text(scan_target)))
return TRUE
if(isitem(scan_target))
var/obj/item/scanned_object = scan_target
if(scanned_object.get_plant_seed() || istype(scanned_object, /obj/item/seeds))
- to_chat(user, scan_plant_stats(scanned_object))
+ to_chat(user, examine_block(scan_plant_stats(scanned_object)))
return TRUE
if(isliving(scan_target))
var/mob/living/L = scan_target
@@ -107,19 +107,19 @@
*/
/obj/item/plant_analyzer/proc/do_plant_chem_scan(atom/scan_target, mob/user)
if(istype(scan_target, /obj/machinery/hydroponics))
- to_chat(user, scan_tray_chems(scan_target))
+ to_chat(user, examine_block(scan_tray_chems(scan_target)))
return TRUE
if(istype(scan_target, /obj/structure/glowshroom))
var/obj/structure/glowshroom/shroom_plant = scan_target
- to_chat(user, scan_plant_chems(shroom_plant.myseed))
+ to_chat(user, examine_block(scan_plant_chems(shroom_plant.myseed)))
return TRUE
if(istype(scan_target, /obj/item/graft))
- to_chat(user, get_graft_text(scan_target))
+ to_chat(user, examine_block(get_graft_text(scan_target)))
return TRUE
if(isitem(scan_target))
var/obj/item/scanned_object = scan_target
if(scanned_object.get_plant_seed() || istype(scanned_object, /obj/item/seeds))
- to_chat(user, scan_plant_chems(scanned_object))
+ to_chat(user, examine_block(scan_plant_chems(scanned_object)))
return TRUE
if(isliving(scan_target))
var/mob/living/L = scan_target
@@ -167,24 +167,24 @@
* Returns the formatted message as text.
*/
/obj/item/plant_analyzer/proc/scan_tray_stats(obj/machinery/hydroponics/scanned_tray)
- var/returned_message = "*---------*\n"
+ var/returned_message = ""
if(scanned_tray.myseed)
- returned_message += "*** [span_bold("[scanned_tray.myseed.plantname]")] ***\n"
- returned_message += "- Plant Age: [span_notice("[scanned_tray.age]")]\n"
- returned_message += "- Plant Health: [span_notice("[scanned_tray.plant_health]")]\n"
- returned_message += scan_plant_stats(scanned_tray.myseed)
+ returned_message += "[span_bold("[scanned_tray.myseed.plantname]")]"
+ returned_message += "\nPlant Age: [span_notice("[scanned_tray.age]")]"
+ returned_message += "\nPlant Health: [span_notice("[scanned_tray.plant_health]")]"
+ returned_message += scan_plant_stats(scanned_tray.myseed, TRUE)
+ returned_message += "\nGrowth medium"
else
- returned_message += span_bold("No plant found.\n")
+ returned_message += span_bold("No plant found.")
- returned_message += "- Weed level: [span_notice("[scanned_tray.weedlevel] / [MAX_TRAY_WEEDS]")]\n"
- returned_message += "- Pest level: [span_notice("[scanned_tray.pestlevel] / [MAX_TRAY_PESTS]")]\n"
- returned_message += "- Toxicity level: [span_notice("[scanned_tray.toxic] / [MAX_TRAY_TOXINS]")]\n"
- returned_message += "- Water level: [span_notice("[scanned_tray.waterlevel] / [scanned_tray.maxwater]")]\n"
- returned_message += "- Nutrition level: [span_notice("[scanned_tray.reagents.total_volume] / [scanned_tray.maxnutri]")]\n"
+ returned_message += "\nWeed level: [span_notice("[scanned_tray.weedlevel] / [MAX_TRAY_WEEDS]")]"
+ returned_message += "\nPest level: [span_notice("[scanned_tray.pestlevel] / [MAX_TRAY_PESTS]")]"
+ returned_message += "\nToxicity level: [span_notice("[scanned_tray.toxic] / [MAX_TRAY_TOXINS]")]"
+ returned_message += "\nWater level: [span_notice("[scanned_tray.waterlevel] / [scanned_tray.maxwater]")]"
+ returned_message += "\nNutrition level: [span_notice("[scanned_tray.reagents.total_volume] / [scanned_tray.maxnutri]")]"
if(scanned_tray.yieldmod != 1)
- returned_message += "- Yield modifier on harvest: [span_notice("[scanned_tray.yieldmod]x")]\n"
+ returned_message += "\nYield modifier on harvest: [span_notice("[scanned_tray.yieldmod]x")]"
- returned_message += "*---------*"
return span_info(returned_message)
/**
@@ -196,22 +196,21 @@
* Returns the formatted message as text.
*/
/obj/item/plant_analyzer/proc/scan_tray_chems(obj/machinery/hydroponics/scanned_tray)
- var/returned_message = "*---------*\n"
+ var/returned_message = ""
if(scanned_tray.myseed)
- returned_message += "*** [span_bold("[scanned_tray.myseed.plantname]")] ***\n"
- returned_message += "- Plant Age: [span_notice("[scanned_tray.age]")]\n"
- returned_message += scan_plant_chems(scanned_tray.myseed)
+ returned_message += "[span_bold("[scanned_tray.myseed.plantname]")]"
+ returned_message += "\nPlant Age: [span_notice("[scanned_tray.age]")]"
+ returned_message += scan_plant_chems(scanned_tray.myseed, TRUE)
else
- returned_message += span_bold("No plant found.\n")
+ returned_message += span_bold("No plant found.")
- returned_message += "- Tray contains:\n"
+ returned_message += "\nGrowth medium contains:"
if(scanned_tray.reagents.reagent_list.len)
for(var/datum/reagent/reagent_id in scanned_tray.reagents.reagent_list)
- returned_message += "- [span_notice("[reagent_id.volume] / [scanned_tray.maxnutri] units of [reagent_id]")]\n"
+ returned_message += "\n[span_notice("• [reagent_id.volume] / [scanned_tray.maxnutri] units of [reagent_id]")]"
else
- returned_message += "[span_notice("No reagents found.")]\n"
+ returned_message += "\n[span_notice("No reagents found.")]"
- returned_message += "*---------*"
return span_info(returned_message)
/**
@@ -222,8 +221,12 @@
*
* Returns the formatted output as text.
*/
-/obj/item/plant_analyzer/proc/scan_plant_stats(obj/item/scanned_object)
- var/returned_message = "*---------*\nThis is [span_name("\a [scanned_object]")].\n"
+/obj/item/plant_analyzer/proc/scan_plant_stats(obj/item/scanned_object, in_tray = FALSE)
+ var/returned_message = ""
+ if(!in_tray)
+ returned_message += "This is [span_name("\a [scanned_object]")]."
+ else
+ returned_message += "\nSeed Stats"
var/obj/item/seeds/our_seed = scanned_object
if(!istype(our_seed)) //if we weren't passed a seed, we were passed a plant with a seed
our_seed = scanned_object.get_plant_seed()
@@ -231,9 +234,8 @@
if(our_seed && istype(our_seed))
returned_message += get_analyzer_text_traits(our_seed)
else
- returned_message += "*---------*\nNo genes found.\n*---------*"
+ returned_message += "\nNo genes found."
- returned_message += "\n"
return span_info(returned_message)
/**
@@ -244,8 +246,12 @@
*
* Returns the formatted output as text.
*/
-/obj/item/plant_analyzer/proc/scan_plant_chems(obj/item/scanned_object)
- var/returned_message = "*---------*\nThis is [span_name("\a [scanned_object]")].\n"
+/obj/item/plant_analyzer/proc/scan_plant_chems(obj/item/scanned_object, in_tray = FALSE)
+ var/returned_message = ""
+ if(!in_tray)
+ returned_message += "This is [span_name("\a [scanned_object]")]."
+ else
+ returned_message += "\nSeed Stats"
var/obj/item/seeds/our_seed = scanned_object
if(!istype(our_seed)) //if we weren't passed a seed, we were passed a plant with a seed
our_seed = scanned_object.get_plant_seed()
@@ -255,9 +261,8 @@
else if (our_seed.reagents_add?.len) //we have a seed with reagent genes
returned_message += get_analyzer_text_chem_genes(our_seed)
else
- returned_message += "*---------*\nNo reagents found.\n*---------*"
+ returned_message += "\nNo reagents found."
- returned_message += "\n"
return span_info(returned_message)
/**
@@ -270,28 +275,28 @@
/obj/item/plant_analyzer/proc/get_analyzer_text_traits(obj/item/seeds/scanned)
var/text = ""
if(scanned.get_gene(/datum/plant_gene/trait/plant_type/weed_hardy))
- text += "- Plant type: [span_notice("Weed. Can grow in nutrient-poor soil.")]\n"
+ text += "\nPlant type: [span_notice("Weed. Can grow in nutrient-poor soil.")]"
else if(scanned.get_gene(/datum/plant_gene/trait/plant_type/fungal_metabolism))
- text += "- Plant type: [span_notice("Mushroom. Can grow in dry soil.")]\n"
+ text += "\nPlant type: [span_notice("Mushroom. Can grow in dry soil.")]"
else if(scanned.get_gene(/datum/plant_gene/trait/plant_type/alien_properties))
- text += "- Plant type: [span_warning("UNKNOWN")] \n"
+ text += "\nPlant type: [span_warning("UNKNOWN")]"
else
- text += "- Plant type: [span_notice("Normal plant")]\n"
+ text += "\nPlant type: [span_notice("Normal plant")]"
if(scanned.potency != -1)
- text += "- Potency: [span_notice("[scanned.potency]")]\n"
+ text += "\nPotency: [span_notice("[scanned.potency]")]"
if(scanned.yield != -1)
- text += "- Yield: [span_notice("[scanned.yield]")]\n"
- text += "- Maturation speed: [span_notice("[scanned.maturation]")]\n"
+ text += "\nYield: [span_notice("[scanned.yield]")]"
+ text += "\nMaturation speed: [span_notice("[scanned.maturation]")]"
if(scanned.yield != -1)
- text += "- Production speed: [span_notice("[scanned.production]")]\n"
- text += "- Endurance: [span_notice("[scanned.endurance]")]\n"
- text += "- Lifespan: [span_notice("[scanned.lifespan]")]\n"
- text += "- Instability: [span_notice("[scanned.instability]")]\n"
- text += "- Weed Growth Rate: [span_notice("[scanned.weed_rate]")]\n"
- text += "- Weed Vulnerability: [span_notice("[scanned.weed_chance]")]\n"
+ text += "\nProduction speed: [span_notice("[scanned.production]")]"
+ text += "\nEndurance: [span_notice("[scanned.endurance]")]"
+ text += "\nLifespan: [span_notice("[scanned.lifespan]")]"
+ text += "\nInstability: [span_notice("[scanned.instability]")]"
+ text += "\nWeed Growth Rate: [span_notice("[scanned.weed_rate]")]"
+ text += "\nWeed Vulnerability: [span_notice("[scanned.weed_chance]")]"
if(scanned.rarity)
- text += "- Species Discovery Value: [span_notice("[scanned.rarity]")]\n"
+ text += "\nSpecies Discovery Value: [span_notice("[scanned.rarity]")]"
var/all_removable_traits = ""
var/all_immutable_traits = ""
for(var/datum/plant_gene/trait/traits in scanned.genes)
@@ -302,17 +307,14 @@
else
all_immutable_traits += "[(all_immutable_traits == "") ? "" : ", "][traits.get_name()]"
- text += "- Plant Traits: [span_notice("[all_removable_traits? all_removable_traits : "None."]")]\n"
- text += "- Core Plant Traits: [span_notice("[all_immutable_traits? all_immutable_traits : "None."]")]\n"
+ text += "\nPlant Traits: [span_notice("[all_removable_traits? all_removable_traits : "None."]")]"
+ text += "\nCore Plant Traits: [span_notice("[all_immutable_traits? all_immutable_traits : "None."]")]"
var/datum/plant_gene/scanned_graft_result = scanned.graft_gene? new scanned.graft_gene : new /datum/plant_gene/trait/repeated_harvest
- text += "- Grafting this plant would give: [span_notice("[scanned_graft_result.get_name()]")]\n"
+ text += "\nGrafting this plant would give: [span_notice("[scanned_graft_result.get_name()]")]"
QDEL_NULL(scanned_graft_result) //graft genes are stored as typepaths so if we want to get their formatted name we need a datum ref - musn't forget to clean up afterwards
- text += "*---------*"
var/unique_text = scanned.get_unique_analyzer_text()
if(unique_text)
- text += "\n"
- text += unique_text
- text += "\n*---------*"
+ text += "\n[unique_text]"
return text
/**
@@ -323,12 +325,9 @@
* Returns the formatted output as text.
*/
/obj/item/plant_analyzer/proc/get_analyzer_text_chem_genes(obj/item/seeds/scanned)
- var/text = ""
- text += "- Plant Reagent Genes -\n"
- text += "*---------*\n"
+ var/text = "\nPlant Reagent Genes:"
for(var/datum/plant_gene/reagent/gene in scanned.genes)
- text += "- [gene.get_name()] -\n"
- text += "*---------*"
+ text += "\n• [gene.get_name()]"
return text
/**
@@ -341,21 +340,19 @@
/obj/item/plant_analyzer/proc/get_analyzer_text_chem_contents(obj/item/scanned_plant)
var/text = ""
var/reagents_text = ""
- text += "- Plant Reagents -\n"
- text += "Maximum reagent capacity: [scanned_plant.reagents.maximum_volume]\n"
+ text += "\nPlant Reagents:"
var/chem_cap = 0
for(var/_reagent in scanned_plant.reagents.reagent_list)
var/datum/reagent/reagent = _reagent
var/amount = reagent.volume
chem_cap += reagent.volume
- reagents_text += "\n- [reagent.name]: [amount]"
- if(chem_cap > 100)
- text += "- [span_danger("Reagent Traits Over 100% Production")]\n"
-
+ reagents_text += "\n• [reagent.name]: [amount]"
if(reagents_text)
- text += "*---------*"
text += reagents_text
- text += "\n*---------*"
+ text += "\nMaximum reagent capacity: [scanned_plant.reagents.maximum_volume]"
+ if(chem_cap > 100)
+ text += "\n[span_danger("Reagent Traits Over 100% Production")]"
+
return text
/**
@@ -366,19 +363,17 @@
* Returns the formatted output as text.
*/
/obj/item/plant_analyzer/proc/get_graft_text(obj/item/graft/scanned_graft)
- var/text = "*---------*\n- Plant Graft -\n"
+ var/text = "Plant Graft"
if(scanned_graft.parent_name)
- text += "- Parent Plant: [span_notice("[scanned_graft.parent_name]")] -\n"
+ text += "\nParent Plant: [span_notice("[scanned_graft.parent_name]")]"
if(scanned_graft.stored_trait)
- text += "- Graftable Traits: [span_notice("[scanned_graft.stored_trait.get_name()]")] -\n"
- text += "*---------*\n"
- text += "- Yield: [span_notice("[scanned_graft.yield]")]\n"
- text += "- Production speed: [span_notice("[scanned_graft.production]")]\n"
- text += "- Endurance: [span_notice("[scanned_graft.endurance]")]\n"
- text += "- Lifespan: [span_notice("[scanned_graft.lifespan]")]\n"
- text += "- Weed Growth Rate: [span_notice("[scanned_graft.weed_rate]")]\n"
- text += "- Weed Vulnerability: [span_notice("[scanned_graft.weed_chance]")]\n"
- text += "*---------*"
+ text += "\nGraftable Traits: [span_notice("[scanned_graft.stored_trait.get_name()]")]"
+ text += "\nYield: [span_notice("[scanned_graft.yield]")]"
+ text += "\nProduction speed: [span_notice("[scanned_graft.production]")]"
+ text += "\nEndurance: [span_notice("[scanned_graft.endurance]")]"
+ text += "\nLifespan: [span_notice("[scanned_graft.lifespan]")]"
+ text += "\nWeed Growth Rate: [span_notice("[scanned_graft.weed_rate]")]"
+ text += "\nWeed Vulnerability: [span_notice("[scanned_graft.weed_chance]")]"
return span_info(text)
diff --git a/code/modules/hydroponics/hydroponics.dm b/code/modules/hydroponics/hydroponics.dm
index 82008d72e208f..127c45b2d2514 100644
--- a/code/modules/hydroponics/hydroponics.dm
+++ b/code/modules/hydroponics/hydroponics.dm
@@ -627,7 +627,6 @@
/obj/machinery/hydroponics/proc/hardmutate()
mutate(4, 10, 2, 4, 50, 4, 10, 0, 4)
-
/obj/machinery/hydroponics/proc/mutatespecie() // Mutagent produced a new plant!
if(!myseed || plant_status == HYDROTRAY_PLANT_DEAD || !LAZYLEN(myseed.mutatelist))
return
@@ -645,6 +644,23 @@
var/message = span_warning("[oldPlantName] suddenly mutates into [myseed.plantname]!")
addtimer(CALLBACK(src, .proc/after_mutation, message), 0.5 SECONDS)
+/obj/machinery/hydroponics/proc/polymorph() // Polymorph a plant into another plant
+ if(!myseed || plant_status == HYDROTRAY_PLANT_DEAD)
+ return
+
+ var/oldPlantName = myseed.plantname
+ var/polymorph_seed = pick(subtypesof(/obj/item/seeds))
+ set_seed(new polymorph_seed(src))
+
+ hardmutate()
+ age = 0
+ set_plant_health(myseed.endurance, update_icon = FALSE)
+ lastcycle = world.time
+ set_weedlevel(0, update_icon = FALSE)
+
+ var/message = span_warning("[oldPlantName] suddenly polymorphs into [myseed.plantname]!")
+ addtimer(CALLBACK(src, .proc/after_mutation, message), 0.5 SECONDS)
+
/obj/machinery/hydroponics/proc/mutateweed() // If the weeds gets the mutagent instead. Mind you, this pretty much destroys the old plant
if( weedlevel > 5 )
set_seed(null)
diff --git a/code/modules/hydroponics/plant_genes.dm b/code/modules/hydroponics/plant_genes.dm
index 24a2ec5745042..0095bd055335f 100644
--- a/code/modules/hydroponics/plant_genes.dm
+++ b/code/modules/hydroponics/plant_genes.dm
@@ -651,8 +651,8 @@
var/splat_location = get_turf(target)
var/range = sqrt(our_seed.potency * 0.1)
smoke.attach(splat_location)
- smoke.set_up(round(range), location = splat_location, carry = our_plant.reagents, silent = FALSE)
- smoke.start()
+ smoke.set_up(round(range), holder = our_plant, location = splat_location, carry = our_plant.reagents, silent = FALSE)
+ smoke.start(log = TRUE)
our_plant.reagents.clear_reagents()
/// Makes the plant and its seeds fireproof. From lavaland plants.
@@ -854,7 +854,7 @@
/datum/plant_gene/trait/never_mutate
name = "Prosophobic Inclination"
mutability_flags = PLANT_GENE_REMOVABLE | PLANT_GENE_MUTATABLE | PLANT_GENE_GRAFTABLE
-
+
/// Prevents stat mutation caused by instability. Trait acts as a tag for hydroponics.dm to recognise.
/datum/plant_gene/trait/stable_stats
name = "Symbiotic Resilience"
diff --git a/code/modules/hydroponics/unique_plant_genes.dm b/code/modules/hydroponics/unique_plant_genes.dm
index 5ea7f604a1572..7370e86f50a19 100644
--- a/code/modules/hydroponics/unique_plant_genes.dm
+++ b/code/modules/hydroponics/unique_plant_genes.dm
@@ -38,6 +38,10 @@
name = "On Attack Trait"
/// The multiplier we apply to the potency to calculate force. Set to 0 to not affect the force.
var/force_multiplier = 0
+ /// If TRUE, our plant will degrade in force every hit until diappearing.
+ var/degrades_after_hit = FALSE
+ /// When we fully degrade, what degraded off of us?
+ var/degradation_noun = "leaves"
/datum/plant_gene/trait/attack/on_new_plant(obj/item/our_plant, newloc)
. = ..()
@@ -50,97 +54,101 @@
RegisterSignal(our_plant, COMSIG_ITEM_ATTACK, .proc/on_plant_attack)
RegisterSignal(our_plant, COMSIG_ITEM_AFTERATTACK, .proc/after_plant_attack)
+/// Signal proc for [COMSIG_ITEM_ATTACK] that allows for effects on attack
+/datum/plant_gene/trait/attack/proc/on_plant_attack(obj/item/source, mob/living/target, mob/living/user)
+ SIGNAL_HANDLER
+
+ INVOKE_ASYNC(src, .proc/attack_effect, source, target, user)
+
/*
- * Plant effects ON attack.
+ * Effects done when we hit people with our plant, ON attack.
+ * Override on a per-plant basis.
*
* our_plant - our plant, that we're attacking with
* user - the person who is attacking with the plant
* target - the person who is attacked by the plant
*/
-/datum/plant_gene/trait/attack/proc/on_plant_attack(obj/item/our_plant, mob/living/target, mob/living/user)
+/datum/plant_gene/trait/attack/proc/attack_effect(obj/item/our_plant, mob/living/target, mob/living/user)
+ return
+
+/// Signal proc for [COMSIG_ITEM_AFTERATTACK] that allows for effects after an attack is done
+/datum/plant_gene/trait/attack/proc/after_plant_attack(obj/item/source, atom/target, mob/user, proximity_flag, click_parameters)
SIGNAL_HANDLER
+ if(!proximity_flag)
+ return
+
+ if(!ismovable(target))
+ return
+
+ if(isobj(target))
+ var/obj/object_target = target
+ if(!(object_target.obj_flags & CAN_BE_HIT))
+ return
+
+ INVOKE_ASYNC(src, .proc/after_attack_effect, source, target, user)
+
/*
- * Plant effects AFTER attack.
+ * Effects done when we hit people with our plant, AFTER the attack is done.
+ * Extend on a per-plant basis.
*
* our_plant - our plant, that we're attacking with
* user - the person who is attacking with the plant
* target - the atom which is attacked by the plant
- *
- * return TRUE if plant attack is acceptable, otherwise FALSE to early return subtypes.
*/
-/datum/plant_gene/trait/attack/proc/after_plant_attack(obj/item/our_plant, atom/target, mob/user, proximity_flag, click_parameters)
- SIGNAL_HANDLER
+/datum/plant_gene/trait/attack/proc/after_attack_effect(obj/item/our_plant, atom/target, mob/living/user)
+ SHOULD_CALL_PARENT(TRUE)
- if(!proximity_flag)
- return FALSE
- return TRUE
+ if(!degrades_after_hit)
+ return
+
+ // We probably hit something or someone. Reduce our force
+ if(our_plant.force > 0)
+ our_plant.force -= rand(1, (our_plant.force / 3) + 1)
+ return
+
+ // When our force degrades to zero or below, we're all done
+ to_chat(user, span_warning("All the [degradation_noun] have fallen off [our_plant] from violent whacking!"))
+ qdel(our_plant)
/// Novaflower's attack effects (sets people on fire) + degradation on attack
/datum/plant_gene/trait/attack/novaflower_attack
name = "Heated Petals"
force_multiplier = 0.2
+ degrades_after_hit = TRUE
+ degradation_noun = "petals"
-/datum/plant_gene/trait/attack/novaflower_attack/on_plant_attack(obj/item/our_plant, mob/living/target, mob/living/user)
- . = ..()
- if(!.)
+/datum/plant_gene/trait/attack/novaflower_attack/attack_effect(obj/item/our_plant, mob/living/target, mob/living/user)
+ if(!istype(target))
return
var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
- to_chat(target, "You are lit on fire from the intense heat of [our_plant]!")
- target.adjust_fire_stacks(our_seed.potency / 20)
+ to_chat(target, span_danger("You are lit on fire from the intense heat of [our_plant]!"))
+ target.adjust_fire_stacks(round(our_seed.potency / 20))
if(target.ignite_mob())
message_admins("[ADMIN_LOOKUPFLW(user)] set [ADMIN_LOOKUPFLW(target)] on fire with [our_plant] at [AREACOORD(user)]")
log_game("[key_name(user)] set [key_name(target)] on fire with [our_plant] at [AREACOORD(user)]")
our_plant.investigate_log("was used by [key_name(user)] to burn [key_name(target)] at [AREACOORD(user)]", INVESTIGATE_BOTANY)
-/datum/plant_gene/trait/attack/novaflower_attack/after_plant_attack(obj/item/our_plant, atom/target, mob/user, proximity_flag, click_parameters)
- . = ..()
- if(!.)
- return
-
- if(!ismovable(target))
- return
- if(our_plant.force > 0)
- our_plant.force -= rand(1, (our_plant.force / 3) + 1)
- else
- to_chat(user, "All the petals have fallen off [our_plant] from violent whacking!")
- qdel(our_plant)
-
/// Sunflower's attack effect (shows cute text)
/datum/plant_gene/trait/attack/sunflower_attack
name = "Bright Petals"
-/datum/plant_gene/trait/attack/sunflower_attack/after_plant_attack(obj/item/our_plant, atom/target, mob/user, proximity_flag, click_parameters)
- . = ..()
- if(!.)
- return
+/datum/plant_gene/trait/attack/sunflower_attack/after_attack_effect(obj/item/our_plant, atom/target, mob/user, proximity_flag, click_parameters)
+ if(ismob(target))
+ var/mob/target_mob = target
+ user.visible_message("[user] smacks [target_mob] with [user.p_their()] [our_plant.name]! FLOWER POWER!", ignored_mobs = list(target_mob, user))
+ if(target_mob != user)
+ to_chat(target_mob, "[user] smacks you with [our_plant]!FLOWER POWER!")
+ to_chat(user, "Your [our_plant.name]'s FLOWER POWER strikes [target_mob]!")
- if(!ismob(target))
- return
- var/mob/target_mob = target
- user.visible_message("[user] smacks [target_mob] with [user.p_their()] [our_plant.name]! FLOWER POWER!", ignored_mobs = list(target_mob, user))
- if(target_mob != user)
- to_chat(target_mob, "[user] smacks you with [our_plant]!FLOWER POWER!")
- to_chat(user, "Your [our_plant.name]'s FLOWER POWER strikes [target_mob]!")
+ return ..()
/// Normal nettle's force + degradation on attack
/datum/plant_gene/trait/attack/nettle_attack
name = "Sharpened Leaves"
force_multiplier = 0.2
-
-/datum/plant_gene/trait/attack/nettle_attack/after_plant_attack(obj/item/our_plant, atom/target, mob/user, proximity_flag, click_parameters)
- . = ..()
- if(!.)
- return
-
- if(!ismovable(target))
- return
- if(our_plant.force > 0)
- our_plant.force -= rand(1, (our_plant.force / 3) + 1)
- else
- to_chat(user, "All the leaves have fallen off [our_plant] from violent whacking!")
- qdel(our_plant)
+ degrades_after_hit = TRUE
/// Deathnettle force + degradation on attack
/datum/plant_gene/trait/attack/nettle_attack/death
@@ -153,9 +161,9 @@
/// Whether our actions are cancelled when the backfire triggers.
var/cancel_action_on_backfire = FALSE
/// A list of extra traits to check to be considered safe.
- var/traits_to_check
+ var/list/traits_to_check
/// A list of extra genes to check to be considered safe.
- var/genes_to_check
+ var/list/genes_to_check
/datum/plant_gene/trait/backfire/on_new_plant(obj/item/our_plant, newloc)
. = ..()
@@ -163,16 +171,20 @@
return
our_plant.AddElement(/datum/element/plant_backfire, cancel_action_on_backfire, traits_to_check, genes_to_check)
- RegisterSignal(our_plant, COMSIG_PLANT_ON_BACKFIRE, .proc/backfire_effect)
+ RegisterSignal(our_plant, COMSIG_PLANT_ON_BACKFIRE, .proc/on_backfire)
-/*
- * The backfire effect. Override with plant-specific effects.
- *
- * user - the person who is carrying the plant
- * our_plant - our plant
+/// Signal proc for [COMSIG_PLANT_ON_BACKFIRE] that causes the backfire effect.
+/datum/plant_gene/trait/backfire/proc/on_backfire(obj/item/source, mob/living/carbon/user)
+ SIGNAL_HANDLER
+
+ INVOKE_ASYNC(src, .proc/backfire_effect, source, user)
+
+/**
+ * The actual backfire effect on the user.
+ * Override with plant-specific effects.
*/
/datum/plant_gene/trait/backfire/proc/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
- SIGNAL_HANDLER
+ return
/// Rose's prick on backfire
/datum/plant_gene/trait/backfire/rose_thorns
@@ -180,18 +192,15 @@
traits_to_check = list(TRAIT_PIERCEIMMUNE)
/datum/plant_gene/trait/backfire/rose_thorns/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
- . = ..()
-
var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
if(!our_seed.get_gene(/datum/plant_gene/trait/sticky) && prob(66))
- to_chat(user, "[our_plant]'s thorns nearly prick your hand. Best be careful.")
+ to_chat(user, span_danger("[our_plant]'s thorns nearly prick your hand. Best be careful."))
return
- to_chat(user, "[our_plant]'s thorns prick your hand. Ouch.")
+ to_chat(user, span_danger("[our_plant]'s thorns prick your hand. Ouch."))
our_plant.investigate_log("rose-pricked [key_name(user)] at [AREACOORD(user)]", INVESTIGATE_BOTANY)
var/obj/item/bodypart/affecting = user.get_active_hand()
- if(affecting?.receive_damage(2))
- user.update_damage_overlays()
+ affecting?.receive_damage(2)
/// Novaflower's hand burn on backfire
/datum/plant_gene/trait/backfire/novaflower_heat
@@ -199,26 +208,20 @@
cancel_action_on_backfire = TRUE
/datum/plant_gene/trait/backfire/novaflower_heat/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
- . = ..()
-
- to_chat(user, "[our_plant] singes your bare hand!")
+ to_chat(user, span_danger("[our_plant] singes your bare hand!"))
our_plant.investigate_log("self-burned [key_name(user)] for [our_plant.force] at [AREACOORD(user)]", INVESTIGATE_BOTANY)
var/obj/item/bodypart/affecting = user.get_active_hand()
- if(affecting?.receive_damage(0, our_plant.force, wound_bonus = CANT_WOUND))
- user.update_damage_overlays()
+ return affecting?.receive_damage(0, our_plant.force, wound_bonus = CANT_WOUND)
/// Normal Nettle hannd burn on backfire
/datum/plant_gene/trait/backfire/nettle_burn
name = "Stinging Stem"
/datum/plant_gene/trait/backfire/nettle_burn/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
- . = ..()
-
- to_chat(user, "[our_plant] burns your bare hand!")
+ to_chat(user, span_danger("[our_plant] burns your bare hand!"))
our_plant.investigate_log("self-burned [key_name(user)] for [our_plant.force] at [AREACOORD(user)]", INVESTIGATE_BOTANY)
var/obj/item/bodypart/affecting = user.get_active_hand()
- if(affecting?.receive_damage(0, our_plant.force, wound_bonus = CANT_WOUND))
- user.update_damage_overlays()
+ return affecting?.receive_damage(0, our_plant.force, wound_bonus = CANT_WOUND)
/// Deathnettle hand burn + stun on backfire
/datum/plant_gene/trait/backfire/nettle_burn/death
@@ -227,10 +230,11 @@
/datum/plant_gene/trait/backfire/nettle_burn/death/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
. = ..()
+ if(!. || prob(50))
+ return
- if(prob(50))
- user.Paralyze(100)
- to_chat(user, "You are stunned by the powerful acids of [our_plant]!")
+ user.Paralyze(10 SECONDS)
+ to_chat(user, span_userdanger("You are stunned by the powerful acids of [our_plant]!"))
/// Ghost-Chili heating up on backfire
/datum/plant_gene/trait/backfire/chili_heat
@@ -256,8 +260,6 @@
* user - the mob holding our plant
*/
/datum/plant_gene/trait/backfire/chili_heat/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
- . = ..()
-
held_mob = WEAKREF(user)
START_PROCESSING(SSobj, src)
@@ -296,11 +298,14 @@
genes_to_check = list(/datum/plant_gene/trait/squash)
/datum/plant_gene/trait/backfire/bluespace/backfire_effect(obj/item/our_plant, mob/living/carbon/user)
- . = ..()
-
if(prob(50))
- to_chat(user, "[our_plant] slips out of your hand!")
- INVOKE_ASYNC(our_plant, /obj/item/.proc/attack_self, user)
+ return
+
+ to_chat(user, span_danger("[our_plant] slips out of your hand!"))
+
+ var/obj/item/seeds/our_seed = our_plant.get_plant_seed()
+ var/datum/plant_gene/trait/squash/squash_gene = our_seed.get_gene(/datum/plant_gene/trait/squash)
+ squash_gene.squash_plant(our_plant, user)
/// Traits for plants that can be activated to turn into a mob.
/datum/plant_gene/trait/mob_transformation
@@ -347,8 +352,8 @@
return
if(target != user)
- to_chat(user, "[our_plant] is twitching and shaking, preventing you from feeding it to [target].")
- to_chat(target, "[our_plant] is twitching and shaking, preventing you from eating it.")
+ to_chat(user, span_warning("[our_plant] is twitching and shaking, preventing you from feeding it to [target]."))
+ to_chat(target, span_warning("[our_plant] is twitching and shaking, preventing you from eating it."))
return COMPONENT_CANCEL_ATTACK_CHAIN
/*
@@ -365,10 +370,10 @@
return
if(dangerous && HAS_TRAIT(user, TRAIT_PACIFISM))
- to_chat(user, "You decide not to awaken [our_plant]. It may be very dangerous!")
+ to_chat(user, span_notice("You decide not to awaken [our_plant]. It may be very dangerous!"))
return
- to_chat(user, "You begin to awaken [our_plant]...")
+ to_chat(user, span_notice("You begin to awaken [our_plant]..."))
begin_awaken(our_plant, 3 SECONDS)
our_plant.investigate_log("was awakened by [key_name(user)] at [AREACOORD(user)].", INVESTIGATE_BOTANY)
@@ -382,7 +387,7 @@
SIGNAL_HANDLER
if(!awakening && !isspaceturf(user.loc) && prob(25))
- to_chat(user, "[our_plant] begins to growl and shake!")
+ our_plant.visible_message(span_danger("[our_plant] begins to growl and shake!"))
begin_awaken(our_plant, 1 SECONDS)
our_plant.investigate_log("was awakened (via plant backfire) by [key_name(user)] at [AREACOORD(user)].", INVESTIGATE_BOTANY)
@@ -416,7 +421,7 @@
spawned_simplemob.melee_damage_upper += round(our_seed.potency * mob_melee_multiplier)
spawned_simplemob.move_to_delay -= round(our_seed.production * mob_speed_multiplier)
our_plant.forceMove(our_plant.drop_location())
- spawned_mob.visible_message("[our_plant] growls as it suddenly awakens!")
+ spawned_mob.visible_message(span_notice("[our_plant] growls as it suddenly awakens!"))
qdel(our_plant)
/// Killer Tomato's transformation gene.
@@ -505,7 +510,10 @@
our_plant.color = COLOR_RED
playsound(our_plant, 'sound/effects/fuse.ogg', our_seed.potency, FALSE)
- user.visible_message("[user] plucks the stem from [our_plant]!", "You pluck the stem from [our_plant], which begins to hiss loudly!")
+ user.visible_message(
+ span_warning("[user] plucks the stem from [our_plant]!"),
+ span_userdanger("You pluck the stem from [our_plant], which begins to hiss loudly!"),
+ )
log_bomber(user, "primed a", our_plant, "for detonation")
detonate(our_plant)
@@ -551,7 +559,10 @@
name = "Explosive Nature"
/datum/plant_gene/trait/bomb_plant/potency_based/trigger_detonation(obj/item/our_plant, mob/living/user)
- user.visible_message("[user] primes [our_plant]!", "You prime [our_plant]!")
+ user.visible_message(
+ span_warning("[user] primes [our_plant]!"),
+ span_userdanger("You prime [our_plant]!"),
+ )
log_bomber(user, "primed a", our_plant, "for detonation")
var/obj/item/food/grown/grown_plant = our_plant
diff --git a/code/game/machinery/crossing_signal.dm b/code/modules/industrial_lift/crossing_signal.dm
similarity index 88%
rename from code/game/machinery/crossing_signal.dm
rename to code/modules/industrial_lift/crossing_signal.dm
index 3a6e5c145d125..2d51acd7aba29 100644
--- a/code/game/machinery/crossing_signal.dm
+++ b/code/modules/industrial_lift/crossing_signal.dm
@@ -26,7 +26,7 @@
/// green, amber, or red.
var/signal_state = XING_STATE_GREEN
/// The ID of the tram we control
- var/tram_id = "tram_station"
+ var/tram_id = MAIN_STATION_TRAM
/// Weakref to the tram piece we control
var/datum/weakref/tram_ref
/// Proximity threshold for amber warning (slow people may be in danger)
@@ -42,14 +42,14 @@
. = ..()
find_tram()
- var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve()
+ var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
if(tram_part)
RegisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING, .proc/on_tram_travelling)
/obj/machinery/crossing_signal/Destroy()
. = ..()
- var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve()
+ var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
if(tram_part)
UnregisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING)
@@ -67,8 +67,8 @@
* Locates tram parts in the lift global list after everything is done.
*/
/obj/machinery/crossing_signal/proc/find_tram()
- for(var/obj/structure/industrial_lift/tram/central/tram as anything in GLOB.central_trams)
- if(tram.tram_id != tram_id)
+ for(var/datum/lift_master/tram/tram as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID])
+ if(tram.specific_lift_id != tram_id)
continue
tram_ref = WEAKREF(tram)
break
@@ -103,10 +103,10 @@
end_processing()
/obj/machinery/crossing_signal/process()
- var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve()
+ var/datum/lift_master/tram/tram = tram_ref?.resolve()
// Check for stopped states.
- if(!tram_part || !is_operational)
+ if(!tram || !is_operational)
// Tram missing, or we lost power.
// Tram missing is always safe (green)
set_signal_state(XING_STATE_GREEN, force = !is_operational)
@@ -114,26 +114,32 @@
use_power(active_power_usage)
+ var/obj/structure/industrial_lift/tram/tram_part = tram.return_closest_platform_to(src)
+
+ if(QDELETED(tram_part))
+ set_signal_state(XING_STATE_GREEN, force = !is_operational)
+ return PROCESS_KILL
+
// Everything will be based on position and travel direction
var/signal_pos
var/tram_pos
var/tram_velocity_sign // 1 for positive axis movement, -1 for negative
// Try to be agnostic about N-S vs E-W movement
- if(tram_part.travel_direction & (NORTH|SOUTH))
+ if(tram.travel_direction & (NORTH|SOUTH))
signal_pos = y
tram_pos = tram_part.y
- tram_velocity_sign = tram_part.travel_direction & NORTH ? 1 : -1
+ tram_velocity_sign = tram.travel_direction & NORTH ? 1 : -1
else
signal_pos = x
tram_pos = tram_part.x
- tram_velocity_sign = tram_part.travel_direction & EAST ? 1 : -1
+ tram_velocity_sign = tram.travel_direction & EAST ? 1 : -1
// How far away are we? negative if already passed.
var/approach_distance = tram_velocity_sign * (signal_pos - tram_pos)
// Check for stopped state.
// Will kill the process since tram starting up will restart process.
- if(!tram_part.travelling)
+ if(!tram.travelling)
// if super close, show red anyway since tram could suddenly start moving
if(abs(approach_distance) < red_distance_threshold)
set_signal_state(XING_STATE_RED)
diff --git a/code/modules/industrial_lift/industrial_lift.dm b/code/modules/industrial_lift/industrial_lift.dm
new file mode 100644
index 0000000000000..d7b3d9cfbec0e
--- /dev/null
+++ b/code/modules/industrial_lift/industrial_lift.dm
@@ -0,0 +1,752 @@
+GLOBAL_LIST_EMPTY(lifts)
+
+/obj/structure/industrial_lift
+ name = "lift platform"
+ desc = "A lightweight lift platform. It moves up and down."
+ icon = 'icons/obj/smooth_structures/catwalk.dmi'
+ icon_state = "catwalk-0"
+ base_icon_state = "catwalk"
+ density = FALSE
+ anchored = TRUE
+ armor = list(MELEE = 50, BULLET = 0, LASER = 0, ENERGY = 0, BOMB = 0, BIO = 0, FIRE = 80, ACID = 50)
+ max_integrity = 50
+ layer = LATTICE_LAYER //under pipes
+ plane = FLOOR_PLANE
+ smoothing_flags = SMOOTH_BITMASK
+ smoothing_groups = list(SMOOTH_GROUP_INDUSTRIAL_LIFT)
+ canSmoothWith = list(SMOOTH_GROUP_INDUSTRIAL_LIFT)
+ obj_flags = CAN_BE_HIT | BLOCK_Z_OUT_DOWN
+ appearance_flags = PIXEL_SCALE|KEEP_TOGETHER //no TILE_BOUND since we're potentially multitile
+
+ ///ID used to determine what lift types we can merge with
+ var/lift_id = BASIC_LIFT_ID
+
+ ///if true, the elevator works through floors
+ var/pass_through_floors = FALSE
+
+ ///what movables on our platform that we are moving
+ var/list/atom/movable/lift_load
+ ///lazylist of weakrefs to the contents we have when we're first created. stored so that admins can clear the tram to its initial state
+ ///if someone put a bunch of stuff onto it.
+ var/list/datum/weakref/initial_contents
+
+ ///what glide_size we set our moving contents to.
+ var/glide_size_override = 8
+ ///lazy list of movables inside lift_load who had their glide_size changed since our last movement.
+ ///used so that we dont have to change the glide_size of every object every movement, which scales to cost more than you'd think
+ var/list/atom/movable/changed_gliders
+
+ ///master datum that controls our movement. in general /industrial_lift subtypes control moving themselves, and
+ /// /datum/lift_master instances control moving the entire tram and any behavior associated with that.
+ var/datum/lift_master/lift_master_datum
+ ///what subtype of /datum/lift_master to create for itself if no other platform on this tram has created one yet.
+ ///very important for some behaviors since
+ var/lift_master_type = /datum/lift_master
+
+ ///how many tiles this platform extends on the x axis
+ var/width = 1
+ ///how many tiles this platform extends on the y axis (north-south not up-down, that would be the z axis)
+ var/height = 1
+
+ ///if TRUE, this platform will late initialize and then expand to become a multitile object across all other linked platforms on this z level
+ var/create_multitile_platform = FALSE
+
+/obj/structure/industrial_lift/Initialize(mapload)
+ . = ..()
+ GLOB.lifts.Add(src)
+
+ set_movement_registrations()
+
+ //since lift_master datums find all connected platforms when an industrial lift first creates it and then
+ //sets those platforms' lift_master_datum to itself, this check will only evaluate to true once per tram platform
+ if(!lift_master_datum && lift_master_type)
+ lift_master_datum = new lift_master_type(src)
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/structure/industrial_lift/LateInitialize()
+ //after everything is initialized the lift master can order everything
+ lift_master_datum.order_platforms_by_z_level()
+
+/obj/structure/industrial_lift/Destroy()
+ GLOB.lifts.Remove(src)
+ lift_master_datum = null
+ return ..()
+
+
+///set the movement registrations to our current turf(s) so contents moving out of our tile(s) are removed from our movement lists
+/obj/structure/industrial_lift/proc/set_movement_registrations(list/turfs_to_set)
+ for(var/turf/turf_loc as anything in turfs_to_set || locs)
+ RegisterSignal(turf_loc, COMSIG_ATOM_EXITED, .proc/UncrossedRemoveItemFromLift)
+ RegisterSignal(turf_loc, list(COMSIG_ATOM_ENTERED,COMSIG_ATOM_INITIALIZED_ON), .proc/AddItemOnLift)
+
+///unset our movement registrations from turfs that no longer contain us (or every loc if turfs_to_unset is unspecified)
+/obj/structure/industrial_lift/proc/unset_movement_registrations(list/turfs_to_unset)
+ var/static/list/registrations = list(COMSIG_ATOM_ENTERED, COMSIG_ATOM_EXITED, COMSIG_ATOM_INITIALIZED_ON)
+ for(var/turf/turf_loc as anything in turfs_to_unset || locs)
+ UnregisterSignal(turf_loc, registrations)
+
+
+/obj/structure/industrial_lift/proc/UncrossedRemoveItemFromLift(datum/source, atom/movable/gone, direction)
+ SIGNAL_HANDLER
+ if(!(gone.loc in locs))
+ RemoveItemFromLift(gone)
+
+/obj/structure/industrial_lift/proc/RemoveItemFromLift(atom/movable/potential_rider)
+ SIGNAL_HANDLER
+ if(!(potential_rider in lift_load))
+ return
+ if(isliving(potential_rider) && HAS_TRAIT(potential_rider, TRAIT_CANNOT_BE_UNBUCKLED))
+ REMOVE_TRAIT(potential_rider, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT)
+ LAZYREMOVE(lift_load, potential_rider)
+ LAZYREMOVE(changed_gliders, potential_rider)
+ UnregisterSignal(potential_rider, list(COMSIG_PARENT_QDELETING, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE))
+
+/obj/structure/industrial_lift/proc/AddItemOnLift(datum/source, atom/movable/new_lift_contents)
+ SIGNAL_HANDLER
+ var/static/list/blacklisted_types = typecacheof(list(/obj/structure/fluff/tram_rail, /obj/effect/decal/cleanable, /obj/structure/industrial_lift, /mob/camera))
+ if(is_type_in_typecache(new_lift_contents, blacklisted_types) || new_lift_contents.invisibility == INVISIBILITY_ABSTRACT) //prevents the tram from stealing things like landmarks
+ return FALSE
+ if(new_lift_contents in lift_load)
+ return FALSE
+
+ if(isliving(new_lift_contents) && !HAS_TRAIT(new_lift_contents, TRAIT_CANNOT_BE_UNBUCKLED))
+ ADD_TRAIT(new_lift_contents, TRAIT_CANNOT_BE_UNBUCKLED, BUCKLED_TRAIT)
+ LAZYADD(lift_load, new_lift_contents)
+ RegisterSignal(new_lift_contents, COMSIG_PARENT_QDELETING, .proc/RemoveItemFromLift)
+
+ return TRUE
+
+///adds everything on our tile that can be added to our lift_load and initial_contents lists when we're created
+/obj/structure/industrial_lift/proc/add_initial_contents()
+ for(var/turf/turf_loc in locs)
+ for(var/atom/movable/movable_contents as anything in turf_loc)
+ if(movable_contents == src)
+ continue
+
+ if(AddItemOnLift(src, movable_contents))
+
+ var/datum/weakref/new_initial_contents = WEAKREF(movable_contents)
+ if(!new_initial_contents)
+ continue
+
+ LAZYADD(initial_contents, new_initial_contents)
+
+///signal handler for COMSIG_MOVABLE_UPDATE_GLIDE_SIZE: when a movable in lift_load changes its glide_size independently.
+///adds that movable to a lazy list, movables in that list have their glide_size updated when the tram next moves
+/obj/structure/industrial_lift/proc/on_changed_glide_size(atom/movable/moving_contents, new_glide_size)
+ SIGNAL_HANDLER
+ if(new_glide_size != glide_size_override)
+ LAZYADD(changed_gliders, moving_contents)
+
+
+///make this tram platform multitile, expanding to cover all the tram platforms adjacent to us and deleting them. makes movement more efficient.
+///the platform becoming multitile should be in the bottom left corner since thats assumed to be the loc of multitile objects
+/obj/structure/industrial_lift/proc/create_multitile_platform(min_x, min_y, max_x, max_y, z)
+
+ if(!(min_x && min_y && max_x && max_y && z))
+ for(var/obj/structure/industrial_lift/other_lift as anything in lift_master_datum.lift_platforms)
+ if(other_lift.z != z)
+ continue
+
+ min_x = min(min_x, other_lift.x)
+ max_x = max(max_x, other_lift.x)
+
+ min_y = min(min_y, other_lift.y)
+ max_y = max(max_y, other_lift.y)
+
+ var/turf/bottom_left_loc = locate(min_x, min_y, z)
+ var/obj/structure/industrial_lift/loc_corner_lift = locate() in bottom_left_loc
+
+ if(!loc_corner_lift)
+ stack_trace("no lift in the bottom left corner of a lift level!")
+ return FALSE
+
+ if(loc_corner_lift != src)
+ //the loc of a multitile object must always be the lower left corner
+ return loc_corner_lift.create_multitile_platform()
+
+ width = (max_x - min_x) + 1
+ height = (max_y - min_y) + 1
+
+ ///list of turfs we dont go over. if for whatever reason we encounter an already multitile lift platform
+ ///we add all of its locs to this list so we dont add that lift platform multiple times as we iterate through its locs
+ var/list/locs_to_skip = locs.Copy()
+
+ bound_width = bound_width * width
+ bound_height = bound_height * height
+
+ //multitile movement code assumes our loc is on the lower left corner of our bounding box
+
+ var/first_x = 0
+ var/first_y = 0
+
+ var/last_x = max(max_x - min_x, 0)
+ var/last_y = max(max_y - min_y, 0)
+
+ for(var/y in first_y to last_y)
+
+ var/y_pixel_offset = world.icon_size * y
+
+ for(var/x in first_x to last_x)
+
+ var/x_pixel_offset = world.icon_size * x
+
+ var/turf/lift_turf = locate(x + min_x, y + min_y, z)
+
+ if(!lift_turf)
+ continue
+
+ if(lift_turf in locs_to_skip)
+ continue
+
+ var/obj/structure/industrial_lift/other_lift = locate() in lift_turf
+
+ if(!other_lift)
+ continue
+
+ locs_to_skip += other_lift.locs.Copy()//make sure we never go over multitile platforms multiple times
+
+ other_lift.pixel_x = x_pixel_offset
+ other_lift.pixel_y = y_pixel_offset
+
+ overlays += other_lift
+
+ //now we vore all the other lifts connected to us on our z level
+ for(var/obj/structure/industrial_lift/other_lift in lift_master_datum.lift_platforms)
+ if(other_lift == src || other_lift.z != z)
+ continue
+
+ lift_master_datum.lift_platforms -= other_lift
+ if(other_lift.lift_load)
+ LAZYOR(lift_load, other_lift.lift_load)
+ if(other_lift.initial_contents)
+ LAZYOR(initial_contents, other_lift.initial_contents)
+
+ qdel(other_lift)
+
+ lift_master_datum.multitile_platform = TRUE
+
+ var/turf/old_loc = loc
+
+ forceMove(locate(min_x, min_y, z))//move to the lower left corner
+ set_movement_registrations(locs - old_loc)
+ return TRUE
+
+///returns an unordered list of all lift platforms adjacent to us. used so our lift_master_datum can control all connected platforms.
+///includes platforms directly above or below us as well. only includes platforms with an identical lift_id to our own.
+/obj/structure/industrial_lift/proc/lift_platform_expansion(datum/lift_master/lift_master_datum)
+ . = list()
+ for(var/direction in GLOB.cardinals_multiz)
+ var/obj/structure/industrial_lift/neighbor = locate() in get_step_multiz(src, direction)
+ if(!neighbor || neighbor.lift_id != lift_id)
+ continue
+ . += neighbor
+
+///main proc for moving the lift in the direction [going]. handles horizontal and/or vertical movement for multi platformed lifts and multitile lifts.
+/obj/structure/industrial_lift/proc/travel(going)
+ var/list/things_to_move = lift_load
+ var/turf/destination
+ if(!isturf(going))
+ destination = get_step_multiz(src, going)
+ else
+ destination = going
+ going = get_dir_multiz(loc, going)
+
+ var/x_offset = ROUND_UP(bound_width / 32) - 1 //how many tiles our horizontally farthest edge is from us
+ var/y_offset = ROUND_UP(bound_height / 32) - 1 //how many tiles our vertically farthest edge is from us
+
+ //the x coordinate of the edge furthest from our future destination, which would be our right hand side
+ var/back_edge_x = destination.x + x_offset//if we arent multitile this should just be destination.x
+ var/top_edge_y = destination.y + y_offset
+
+ var/turf/top_right_corner = locate(min(world.maxx, back_edge_x), min(world.maxy, top_edge_y), destination.z)
+
+ var/list/dest_locs = block(
+ destination,
+ top_right_corner
+ )
+
+ var/list/entering_locs = dest_locs - locs
+ var/list/exited_locs = locs - dest_locs
+
+ if(going == DOWN)
+ for(var/turf/dest_turf as anything in entering_locs)
+ SEND_SIGNAL(dest_turf, COMSIG_TURF_INDUSTRIAL_LIFT_ENTER, things_to_move)
+
+ if(istype(dest_turf, /turf/closed/wall))
+ var/turf/closed/wall/C = dest_turf
+ do_sparks(2, FALSE, C)
+ C.dismantle_wall(devastated = TRUE)
+ for(var/mob/M in urange(8, src))
+ shake_camera(M, 2, 3)
+ playsound(C, 'sound/effects/meteorimpact.ogg', 100, TRUE)
+
+ for(var/mob/living/crushed in dest_turf.contents)
+ to_chat(crushed, span_userdanger("You are crushed by [src]!"))
+ crushed.gib(FALSE,FALSE,FALSE)//the nicest kind of gibbing, keeping everything intact.
+
+ else if(going == UP)
+ for(var/turf/dest_turf as anything in entering_locs)
+ ///handles any special interactions objects could have with the lift/tram, handled on the item itself
+ SEND_SIGNAL(dest_turf, COMSIG_TURF_INDUSTRIAL_LIFT_ENTER, things_to_move)
+
+ if(istype(dest_turf, /turf/closed/wall))
+ var/turf/closed/wall/C = dest_turf
+ do_sparks(2, FALSE, C)
+ C.dismantle_wall(devastated = TRUE)
+ for(var/mob/client_mob in SSspatial_grid.orthogonal_range_search(src, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS, 8))
+ shake_camera(client_mob, 2, 3)
+ playsound(C, 'sound/effects/meteorimpact.ogg', 100, TRUE)
+
+ else
+ ///potentially finds a spot to throw the victim at for daring to be hit by a tram. is null if we havent found anything to throw
+ var/atom/throw_target
+ var/datum/lift_master/tram/our_lift = lift_master_datum
+ var/collision_lethality = our_lift.collision_lethality
+
+ for(var/turf/dest_turf as anything in entering_locs)
+ ///handles any special interactions objects could have with the lift/tram, handled on the item itself
+ SEND_SIGNAL(dest_turf, COMSIG_TURF_INDUSTRIAL_LIFT_ENTER, things_to_move)
+
+ if(istype(dest_turf, /turf/closed/wall))
+ var/turf/closed/wall/collided_wall = dest_turf
+ do_sparks(2, FALSE, collided_wall)
+ collided_wall.dismantle_wall(devastated = TRUE)
+ for(var/mob/client_mob in SSspatial_grid.orthogonal_range_search(collided_wall, SPATIAL_GRID_CONTENTS_TYPE_CLIENTS, 8))
+ if(get_dist(dest_turf, client_mob) <= 8)
+ shake_camera(client_mob, 2, 3)
+
+ playsound(collided_wall, 'sound/effects/meteorimpact.ogg', 100, TRUE)
+
+ for(var/obj/structure/victim_structure in dest_turf.contents)
+ if(QDELING(victim_structure))
+ continue
+ if(!is_type_in_typecache(victim_structure, lift_master_datum.ignored_smashthroughs) && victim_structure.layer >= LOW_OBJ_LAYER)
+
+ if(victim_structure.anchored && initial(victim_structure.anchored) == TRUE)
+ visible_message(span_danger("[src] smashes through [victim_structure]!"))
+ victim_structure.deconstruct(FALSE)
+
+ else
+ if(!throw_target)
+ throw_target = get_edge_target_turf(src, turn(going, pick(45, -45)))
+ visible_message(span_danger("[src] violently rams [victim_structure] out of the way!"))
+ victim_structure.anchored = FALSE
+ victim_structure.take_damage(rand(20, 25) * collision_lethality)
+ victim_structure.throw_at(throw_target, 200 * collision_lethality, 4 * collision_lethality)
+
+ for(var/obj/machinery/victim_machine in dest_turf.contents)
+ if(QDELING(victim_machine))
+ continue
+ if(is_type_in_typecache(victim_machine, lift_master_datum.ignored_smashthroughs))
+ continue
+ if(istype(victim_machine, /obj/machinery/field)) //graceful break handles this scenario
+ continue
+ if(victim_machine.layer >= LOW_OBJ_LAYER) //avoids stuff that is probably flush with the ground
+ playsound(src, 'sound/effects/bang.ogg', 50, TRUE)
+ visible_message(span_danger("[src] smashes through [victim_machine]!"))
+ qdel(victim_machine)
+
+ for(var/mob/living/collided in dest_turf.contents)
+ if(lift_master_datum.ignored_smashthroughs[collided.type])
+ continue
+ to_chat(collided, span_userdanger("[src] collides into you!"))
+ playsound(src, 'sound/effects/splat.ogg', 50, TRUE)
+ var/damage = rand(5, 10) * collision_lethality
+ collided.apply_damage(2 * damage, BRUTE, BODY_ZONE_HEAD)
+ collided.apply_damage(2 * damage, BRUTE, BODY_ZONE_CHEST)
+ collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_LEG)
+ collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_LEG)
+ collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_ARM)
+ collided.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_ARM)
+
+ if(QDELETED(collided)) //in case it was a mob that dels on death
+ continue
+ if(!throw_target)
+ throw_target = get_edge_target_turf(src, turn(going, pick(45, -45)))
+
+ var/turf/T = get_turf(collided)
+ T.add_mob_blood(collided)
+
+ collided.throw_at()
+ //if going EAST, will turn to the NORTHEAST or SOUTHEAST and throw the ran over guy away
+ var/datum/callback/land_slam = new(collided, /mob/living/.proc/tram_slam_land)
+ collided.throw_at(throw_target, 200 * collision_lethality, 4 * collision_lethality, callback = land_slam)
+
+ unset_movement_registrations(exited_locs)
+ group_move(things_to_move, going)
+ set_movement_registrations(entering_locs)
+
+///move the movers list of movables on our tile to destination if we successfully move there first.
+///this is like calling forceMove() on everything in movers and ourselves, except nothing in movers
+///has destination.Entered() and origin.Exited() called on them, as only our movement can be perceived.
+///none of the movers are able to react to the movement of any other mover, saving a lot of needless processing cost
+///and is more sensible. without this, if you and a banana are on the same platform, when that platform moves you will slip
+///on the banana even if youre not moving relative to it.
+/obj/structure/industrial_lift/proc/group_move(list/atom/movable/movers, movement_direction)
+ if(movement_direction == NONE)
+ stack_trace("an industrial lift was told to move to somewhere it already is!")
+ return FALSE
+
+ var/turf/our_dest = get_step(src, movement_direction)
+
+ var/area/our_area = get_area(src)
+ var/area/their_area = get_area(our_dest)
+ var/different_areas = our_area != their_area
+ var/turf/mover_old_loc
+
+ if(glide_size != glide_size_override)
+ set_glide_size(glide_size_override)
+
+ forceMove(our_dest)
+ if(loc != our_dest || QDELETED(src))//check if our movement succeeded, if it didnt then the movers cant be moved
+ return FALSE
+
+ for(var/atom/movable/mover as anything in changed_gliders)
+ if(mover.glide_size != glide_size_override)
+ mover.set_glide_size(glide_size_override)
+
+ LAZYREMOVE(changed_gliders, mover)
+
+ if(different_areas)
+ for(var/atom/movable/mover as anything in movers)
+ if(QDELETED(mover))
+ movers -= mover
+ continue
+
+ //we dont need to call Entered() and Exited() for origin and destination here for each mover because
+ //all of them are considered to be on top of us, so the turfs and anything on them can only perceive us,
+ //which is why the platform itself uses forceMove()
+ mover_old_loc = mover.loc
+
+ our_area.Exited(mover, movement_direction)
+ mover.loc = get_step(mover, movement_direction)
+ their_area.Entered(mover, movement_direction)
+
+ mover.Moved(mover_old_loc, movement_direction, TRUE, null, FALSE)
+
+ else
+ for(var/atom/movable/mover as anything in movers)
+ if(QDELETED(mover))
+ movers -= mover
+ continue
+
+ mover_old_loc = mover.loc
+ mover.loc = get_step(mover, movement_direction)
+
+ mover.Moved(mover_old_loc, movement_direction, TRUE, null, FALSE)
+
+ return TRUE
+
+/**
+ * reset the contents of this lift platform to its original state in case someone put too much shit on it.
+ * used by an admin via calling reset_lift_contents() on our lift_master_datum.
+ *
+ * Arguments:
+ * * consider_anything_past - number. if > 0 this platform will only handle foreign contents that exceed this number on each of our locs
+ * * foreign_objects - bool. if true this platform will consider /atom/movable's that arent mobs as part of foreign contents
+ * * foreign_non_player_mobs - bool. if true we consider mobs that dont have a mind to be foreign
+ * * consider_player_mobs - bool. if true we consider player mobs to be foreign. only works if foreign_non_player_mobs is true as well
+ */
+/obj/structure/industrial_lift/proc/reset_contents(consider_anything_past = 0, foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = FALSE)
+ if(!foreign_objects && !foreign_non_player_mobs && !consider_player_mobs)
+ return FALSE
+
+ consider_anything_past = isnum(consider_anything_past) ? max(consider_anything_past, 0) : 0
+ //just in case someone fucks up the arguments
+
+ if(consider_anything_past && length(lift_load) <= consider_anything_past)
+ return FALSE
+
+ ///list of resolve()'d initial_contents that are still in lift_load
+ var/list/atom/movable/original_contents = list(src)
+
+ ///list of objects we consider foreign according to the given arguments
+ var/list/atom/movable/foreign_contents = list()
+
+
+ for(var/datum/weakref/initial_contents_ref as anything in initial_contents)
+ if(!initial_contents_ref)
+ continue
+
+ var/atom/movable/resolved_contents = initial_contents_ref.resolve()
+
+ if(!resolved_contents)
+ continue
+
+ if(!(resolved_contents in lift_load))
+ continue
+
+ original_contents += resolved_contents
+
+ for(var/turf/turf_loc as anything in locs)
+ var/list/atom/movable/foreign_contents_in_loc = list()
+
+ for(var/atom/movable/foreign_movable as anything in (turf_loc.contents - original_contents))
+ if(foreign_objects && ismovable(foreign_movable) && !ismob(foreign_movable))
+ foreign_contents_in_loc += foreign_movable
+ continue
+
+ if(foreign_non_player_mobs && ismob(foreign_movable))
+ var/mob/foreign_mob = foreign_movable
+ if(consider_player_mobs || !foreign_mob.mind)
+ foreign_contents_in_loc += foreign_mob
+ continue
+
+ if(consider_anything_past)
+ foreign_contents_in_loc.len -= consider_anything_past
+ //hey cool this works, neat. this takes from the opposite side of the list that youd expect but its easy so idc
+ //also this means that if you use consider_anything_past then foreign mobs are less likely to be deleted than foreign objects
+ //because internally the contents list is 2 linked lists of obj contents - mob contents, thus mobs are always last in the order
+ //when you iterate it.
+
+ foreign_contents += foreign_contents_in_loc
+
+ for(var/atom/movable/contents_to_delete as anything in foreign_contents)
+ qdel(contents_to_delete)
+
+ return TRUE
+
+/obj/structure/industrial_lift/proc/use(mob/living/user)
+ if(!isliving(user) || !in_range(src, user) || user.combat_mode)
+ return
+
+ var/list/tool_list = list()
+ if(lift_master_datum.Check_lift_move(UP))
+ tool_list["Up"] = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH)
+ if(lift_master_datum.Check_lift_move(DOWN))
+ tool_list["Down"] = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH)
+ if(!length(tool_list))
+ to_chat(user, span_warning("[src] doesn't seem to able to move anywhere!"))
+ add_fingerprint(user)
+ return
+ if(lift_master_datum.controls_locked)
+ to_chat(user, span_warning("[src] has its controls locked! It must already be trying to do something!"))
+ add_fingerprint(user)
+ return
+ var/result = show_radial_menu(user, src, tool_list, custom_check = CALLBACK(src, .proc/check_menu, user, src.loc), require_near = TRUE, tooltips = TRUE)
+ if(!isliving(user) || !in_range(src, user) || user.combat_mode)
+ return //nice try
+ switch(result)
+ if("Up")
+ // We have to make sure that they don't do illegal actions by not having their radial menu refresh from someone else moving the lift.
+ if(!lift_master_datum.Check_lift_move(UP))
+ to_chat(user, span_warning("[src] doesn't seem to able to move up!"))
+ add_fingerprint(user)
+ return
+ lift_master_datum.MoveLift(UP, user)
+ show_fluff_message(TRUE, user)
+ use(user)
+ if("Down")
+ if(!lift_master_datum.Check_lift_move(DOWN))
+ to_chat(user, span_warning("[src] doesn't seem to able to move down!"))
+ add_fingerprint(user)
+ return
+ lift_master_datum.MoveLift(DOWN, user)
+ show_fluff_message(FALSE, user)
+ use(user)
+ if("Cancel")
+ return
+ add_fingerprint(user)
+
+/**
+ * Proc to ensure that the radial menu closes when it should.
+ * Arguments:
+ * * user - The person that opened the menu.
+ * * starting_loc - The location of the lift when the menu was opened, used to prevent the menu from being interacted with after the lift was moved by someone else.
+ *
+ * Returns:
+ * * boolean, FALSE if the menu should be closed, TRUE if the menu is clear to stay opened.
+ */
+/obj/structure/industrial_lift/proc/check_menu(mob/user, starting_loc)
+ if(user.incapacitated() || !user.Adjacent(src) || starting_loc != src.loc)
+ return FALSE
+ return TRUE
+
+/obj/structure/industrial_lift/attack_hand(mob/user, list/modifiers)
+ . = ..()
+ if(.)
+ return
+ use(user)
+
+//ai probably shouldn't get to use lifts but they sure are great for admins to crush people with
+/obj/structure/industrial_lift/attack_ghost(mob/user)
+ . = ..()
+ if(.)
+ return
+ if(isAdminGhostAI(user))
+ use(user)
+
+/obj/structure/industrial_lift/attack_paw(mob/user, list/modifiers)
+ return use(user)
+
+/obj/structure/industrial_lift/attackby(obj/item/W, mob/user, params)
+ return use(user)
+
+/obj/structure/industrial_lift/attack_robot(mob/living/silicon/robot/R)
+ if(R.Adjacent(src))
+ return use(R)
+
+/**
+ * Shows a message indicating that the lift has moved up or down.
+ * Arguments:
+ * * going_up - Boolean on whether or not we're going up, to adjust the message appropriately.
+ * * user - The mob that caused the lift to move, for the visible message.
+ */
+/obj/structure/industrial_lift/proc/show_fluff_message(going_up, mob/user)
+ if(going_up)
+ user.visible_message(span_notice("[user] moves the lift upwards."), span_notice("You move the lift upwards."))
+ else
+ user.visible_message(span_notice("[user] moves the lift downwards."), span_notice("You move the lift downwards."))
+
+/obj/structure/industrial_lift/debug
+ name = "transport platform"
+ desc = "A lightweight platform. It moves in any direction, except up and down."
+ color = "#5286b9ff"
+ lift_id = DEBUG_LIFT_ID
+
+/obj/structure/industrial_lift/debug/use(mob/user)
+ if (!in_range(src, user))
+ return
+//NORTH, SOUTH, EAST, WEST, NORTHEAST, NORTHWEST, SOUTHEAST, SOUTHWEST
+ var/static/list/tool_list = list(
+ "NORTH" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH),
+ "NORTHEAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = NORTH),
+ "EAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = EAST),
+ "SOUTHEAST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = EAST),
+ "SOUTH" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH),
+ "SOUTHWEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = SOUTH),
+ "WEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = WEST),
+ "NORTHWEST" = image(icon = 'icons/testing/turf_analysis.dmi', icon_state = "red_arrow", dir = WEST)
+ )
+
+ var/result = show_radial_menu(user, src, tool_list, custom_check = CALLBACK(src, .proc/check_menu, user), require_near = TRUE, tooltips = FALSE)
+ if (!in_range(src, user))
+ return // nice try
+
+ switch(result)
+ if("NORTH")
+ lift_master_datum.MoveLiftHorizontal(NORTH, z)
+ use(user)
+ if("NORTHEAST")
+ lift_master_datum.MoveLiftHorizontal(NORTHEAST, z)
+ use(user)
+ if("EAST")
+ lift_master_datum.MoveLiftHorizontal(EAST, z)
+ use(user)
+ if("SOUTHEAST")
+ lift_master_datum.MoveLiftHorizontal(SOUTHEAST, z)
+ use(user)
+ if("SOUTH")
+ lift_master_datum.MoveLiftHorizontal(SOUTH, z)
+ use(user)
+ if("SOUTHWEST")
+ lift_master_datum.MoveLiftHorizontal(SOUTHWEST, z)
+ use(user)
+ if("WEST")
+ lift_master_datum.MoveLiftHorizontal(WEST, z)
+ use(user)
+ if("NORTHWEST")
+ lift_master_datum.MoveLiftHorizontal(NORTHWEST, z)
+ use(user)
+ if("Cancel")
+ return
+
+ add_fingerprint(user)
+
+/obj/structure/industrial_lift/tram
+ name = "tram"
+ desc = "A tram for tramversing the station."
+ icon = 'icons/turf/floors.dmi'
+ icon_state = "titanium_yellow"
+ base_icon_state = null
+ smoothing_flags = NONE
+ smoothing_groups = null
+ canSmoothWith = null
+ //kind of a centerpiece of the station, so pretty tough to destroy
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
+
+ lift_id = TRAM_LIFT_ID
+ lift_master_type = /datum/lift_master/tram
+
+ /// Set by the tram control console in late initialize
+ var/travelling = FALSE
+
+ //the following are only used to give to the lift_master datum when it's first created
+
+ ///decisecond delay between horizontal movements. cannot make the tram move faster than 1 movement per world.tick_lag. only used to give to the lift_master
+ var/horizontal_speed = 0.5
+
+ create_multitile_platform = TRUE
+
+/obj/structure/industrial_lift/tram/AddItemOnLift(datum/source, atom/movable/AM)
+ . = ..()
+ if(travelling)
+ on_changed_glide_size(AM, AM.glide_size)
+
+/obj/structure/industrial_lift/tram/proc/set_travelling(travelling)
+ if (src.travelling == travelling)
+ return
+
+ for(var/atom/movable/glider as anything in lift_load)
+ if(travelling)
+ glider.set_glide_size(glide_size_override)
+ RegisterSignal(glider, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, .proc/on_changed_glide_size)
+ else
+ LAZYREMOVE(changed_gliders, glider)
+ UnregisterSignal(glider, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE)
+
+ src.travelling = travelling
+ SEND_SIGNAL(src, COMSIG_TRAM_SET_TRAVELLING, travelling)
+
+/obj/structure/industrial_lift/tram/use(mob/user) //dont click the floor dingus we use computers now
+ return
+
+/obj/structure/industrial_lift/tram/set_currently_z_moving()
+ return FALSE //trams can never z fall and shouldnt waste any processing time trying to do so
+
+/**
+ * Handles unlocking the tram controls for use after moving
+ *
+ * More safety checks to make sure the tram has actually docked properly
+ * at a location before users are allowed to interact with the tram console again.
+ * Tram finds its location at this point before fully unlocking controls to the user.
+ */
+/obj/structure/industrial_lift/tram/proc/unlock_controls()
+ visible_message(span_notice("[src]'s controls are now unlocked."))
+ for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_master_datum.lift_platforms) //only thing everyone needs to know is the new location.
+ tram_part.set_travelling(FALSE)
+ lift_master_datum.set_controls(LIFT_PLATFORM_UNLOCKED)
+
+///debug proc to highlight the locs of the tram platform
+/obj/structure/industrial_lift/tram/proc/find_dimensions(iterations = 100)
+ message_admins("num turfs: [length(locs)]")
+
+ var/overlay = /obj/effect/overlay/ai_detect_hud
+ var/list/turfs = list()
+
+ for(var/turf/our_turf as anything in locs)
+ new overlay(our_turf)
+ turfs += our_turf
+
+ addtimer(CALLBACK(src, .proc/clear_turfs, turfs, iterations), 1)
+
+/obj/structure/industrial_lift/tram/proc/clear_turfs(list/turfs_to_clear, iterations)
+ for(var/turf/our_old_turf as anything in turfs_to_clear)
+ var/obj/effect/overlay/ai_detect_hud/hud = locate() in our_old_turf
+ if(hud)
+ qdel(hud)
+
+ var/overlay = /obj/effect/overlay/ai_detect_hud
+
+ for(var/turf/our_turf as anything in locs)
+ new overlay(our_turf)
+
+ iterations--
+
+ var/list/turfs = list()
+ for(var/turf/our_turf as anything in locs)
+ turfs += our_turf
+
+ if(iterations)
+ addtimer(CALLBACK(src, .proc/clear_turfs, turfs, iterations), 1)
diff --git a/code/modules/industrial_lift/lift_master.dm b/code/modules/industrial_lift/lift_master.dm
new file mode 100644
index 0000000000000..45c2ae3b81a75
--- /dev/null
+++ b/code/modules/industrial_lift/lift_master.dm
@@ -0,0 +1,397 @@
+///associative list of the form: list(lift_id = list(all lift_master datums attached to lifts of that type))
+GLOBAL_LIST_EMPTY(active_lifts_by_type)
+
+///coordinate and control movement across linked industrial_lift's. allows moving large single multitile platforms and many 1 tile platforms.
+///also is capable of linking platforms across linked z levels
+/datum/lift_master
+ ///the lift platforms we consider as part of this lift. ordered in order of lowest z level to highest z level after init.
+ ///(the sorting algorithm sucks btw)
+ var/list/obj/structure/industrial_lift/lift_platforms
+
+ /// Typepath list of what to ignore smashing through, controls all lifts
+ var/static/list/ignored_smashthroughs = list(
+ /obj/machinery/power/supermatter_crystal,
+ /obj/structure/holosign,
+ /obj/machinery/field,
+ )
+
+ ///whether the lift handled by this lift_master datum is multitile as opposed to nxm platforms per z level
+ var/multitile_platform = FALSE
+
+ ///taken from our lift platforms. if true we go through each z level of platforms and attempt to make the lowest left corner platform
+ ///into one giant multitile object the size of all other platforms on that z level.
+ var/create_multitile_platform = FALSE
+
+ ///lift platforms have already been sorted in order of z level.
+ var/z_sorted = FALSE
+
+ ///lift_id taken from our base lift platform, used to put us into GLOB.active_lifts_by_type
+ var/lift_id = BASIC_LIFT_ID
+
+ ///overridable ID string to link control units to this specific lift_master datum. created by placing a lift id landmark object
+ ///somewhere on the tram, if its anywhere on the tram we'll find it in init and set this to whatever it specifies
+ var/specific_lift_id
+ ///what directions we're allowed to move
+ var/allowed_travel_directions = ALL
+
+ ///if true, the lift cannot be manually moved.
+ var/controls_locked = FALSE
+
+/datum/lift_master/New(obj/structure/industrial_lift/lift_platform)
+ lift_id = lift_platform.lift_id
+ create_multitile_platform = lift_platform.create_multitile_platform
+
+ Rebuild_lift_plaform(lift_platform)
+ ignored_smashthroughs = typecacheof(ignored_smashthroughs)
+
+ LAZYADDASSOCLIST(GLOB.active_lifts_by_type, lift_id, src)
+
+ for(var/obj/structure/industrial_lift/lift as anything in lift_platforms)
+ lift.add_initial_contents()
+
+/datum/lift_master/Destroy()
+ for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
+ lift_platform.lift_master_datum = null
+ lift_platforms = null
+
+ LAZYREMOVEASSOC(GLOB.active_lifts_by_type, lift_id, src)
+ if(isnull(GLOB.active_lifts_by_type))
+ GLOB.active_lifts_by_type = list()//im lazy
+
+ return ..()
+
+/datum/lift_master/proc/add_lift_platforms(obj/structure/industrial_lift/new_lift_platform)
+ if(new_lift_platform in lift_platforms)
+ return
+ for(var/obj/structure/industrial_lift/other_platform in new_lift_platform.loc)
+ if(other_platform != new_lift_platform)
+ stack_trace("there is more than one lift platform on a tile when a lift_master adds it. this causes problems")
+ qdel(other_platform)
+
+ new_lift_platform.lift_master_datum = src
+ LAZYADD(lift_platforms, new_lift_platform)
+ RegisterSignal(new_lift_platform, COMSIG_PARENT_QDELETING, .proc/remove_lift_platforms)
+
+ check_for_landmarks(new_lift_platform)
+
+ if(z_sorted)//make sure we dont lose z ordering if we get additional platforms after init
+ order_platforms_by_z_level()
+
+/datum/lift_master/proc/remove_lift_platforms(obj/structure/industrial_lift/old_lift_platform)
+ SIGNAL_HANDLER
+
+ if(!(old_lift_platform in lift_platforms))
+ return
+
+ old_lift_platform.lift_master_datum = null
+ LAZYREMOVE(lift_platforms, old_lift_platform)
+ UnregisterSignal(old_lift_platform, COMSIG_PARENT_QDELETING)
+ if(!length(lift_platforms))
+ qdel(src)
+
+///Collect all bordered platforms via a simple floodfill algorithm. allows multiz trams because its funny
+/datum/lift_master/proc/Rebuild_lift_plaform(obj/structure/industrial_lift/base_lift_platform)
+ add_lift_platforms(base_lift_platform)
+ var/list/possible_expansions = list(base_lift_platform)
+
+ while(possible_expansions.len)
+ for(var/obj/structure/industrial_lift/borderline as anything in possible_expansions)
+ var/list/result = borderline.lift_platform_expansion(src)
+ if(length(result))
+ for(var/obj/structure/industrial_lift/lift_platform as anything in result)
+ if(lift_platforms.Find(lift_platform))
+ continue
+
+ add_lift_platforms(lift_platform)
+ possible_expansions |= lift_platform
+
+ possible_expansions -= borderline
+
+///check for any landmarks placed inside the locs of the given lift_platform
+/datum/lift_master/proc/check_for_landmarks(obj/structure/industrial_lift/new_lift_platform)
+ SHOULD_CALL_PARENT(TRUE)
+
+ for(var/turf/platform_loc as anything in new_lift_platform.locs)
+ var/obj/effect/landmark/lift_id/id_giver = locate() in platform_loc
+
+ if(id_giver)
+ set_info_from_id_landmark(id_giver)
+
+///set vars and such given an overriding lift_id landmark
+/datum/lift_master/proc/set_info_from_id_landmark(obj/effect/landmark/lift_id/landmark)
+ SHOULD_CALL_PARENT(TRUE)
+
+ if(!istype(landmark, /obj/effect/landmark/lift_id))//lift_master subtypes can want differnet id's than the base type wants
+ return
+
+ if(landmark.specific_lift_id)
+ specific_lift_id = landmark.specific_lift_id
+
+ qdel(landmark)
+
+///orders the lift platforms in order of lowest z level to highest z level.
+/datum/lift_master/proc/order_platforms_by_z_level()
+ //contains nested lists for every z level in the world. why? because its really easy to sort
+ var/list/platforms_by_z = list()
+ platforms_by_z.len = world.maxz
+
+ for(var/z in 1 to world.maxz)
+ platforms_by_z[z] = list()
+
+ for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
+ if(QDELETED(lift_platform) || !lift_platform.z)
+ lift_platforms -= lift_platform
+ continue
+
+ platforms_by_z[lift_platform.z] += lift_platform
+
+ if(create_multitile_platform)
+ for(var/list/z_list as anything in platforms_by_z)
+ if(!length(z_list))
+ continue
+
+ create_multitile_platform_for_z_level(z_list)//this will subtract all but one platform from the list
+
+ var/list/output = list()
+
+ for(var/list/z_list as anything in platforms_by_z)
+ output += z_list
+
+ lift_platforms = output
+
+ z_sorted = TRUE
+
+///goes through all platforms in the given list and finds the one in the lower left corner
+/datum/lift_master/proc/create_multitile_platform_for_z_level(list/obj/structure/industrial_lift/platforms_in_z)
+ var/min_x = INFINITY
+ var/max_x = 0
+
+ var/min_y = INFINITY
+ var/max_y = 0
+
+ var/z = 0
+
+ for(var/obj/structure/industrial_lift/lift_to_sort as anything in platforms_in_z)
+ if(!z)
+ if(!lift_to_sort.z)
+ stack_trace("create_multitile_platform_for_z_level() was given a platform in nullspace or not on a turf!")
+ platforms_in_z -= lift_to_sort
+ continue
+
+ z = lift_to_sort.z
+
+ if(z != lift_to_sort.z)
+ stack_trace("create_multitile_platform_for_z_level() was given lifts on different z levels!")
+ platforms_in_z -= lift_to_sort
+ continue
+
+ min_x = min(min_x, lift_to_sort.x)
+ max_x = max(max_x, lift_to_sort.x)
+
+ min_y = min(min_y, lift_to_sort.y)
+ max_y = max(max_y, lift_to_sort.y)
+
+ var/turf/lower_left_corner_loc = locate(min_x, min_y, z)
+ if(!lower_left_corner_loc)
+ CRASH("was unable to find a turf at the lower left corner of this z")
+
+ var/obj/structure/industrial_lift/lower_left_corner_lift = locate() in lower_left_corner_loc
+
+ if(!lower_left_corner_lift)
+ CRASH("there was no lift in the lower left corner of the given lifts")
+
+ platforms_in_z.Cut()
+ platforms_in_z += lower_left_corner_lift//we want to change the list given to us not create a new one. so we do this
+
+ lower_left_corner_lift.create_multitile_platform(min_x, min_y, max_x, max_y, z)
+
+///returns the closest lift to the specified atom, prioritizing lifts on the same z level. used for comparing distance
+/datum/lift_master/proc/return_closest_platform_to(atom/comparison, allow_multiple_answers = FALSE)
+ if(!istype(comparison) || !comparison.z)
+ return FALSE
+
+ var/list/obj/structure/industrial_lift/candidate_platforms = list()
+
+ for(var/obj/structure/industrial_lift/platform as anything in lift_platforms)
+ if(platform.z == comparison.z)
+ candidate_platforms += platform
+
+ var/obj/structure/industrial_lift/winner = candidate_platforms[1]
+ var/winner_distance = get_dist(comparison, winner)
+
+ var/list/tied_winners = list(winner)
+
+ for(var/obj/structure/industrial_lift/platform_to_sort as anything in candidate_platforms)
+ var/platform_distance = get_dist(comparison, platform_to_sort)
+
+ if(platform_distance < winner_distance)
+ winner = platform_to_sort
+ winner_distance = platform_distance
+
+ if(allow_multiple_answers)
+ tied_winners = list(winner)
+
+ else if(platform_distance == winner_distance && allow_multiple_answers)
+ tied_winners += platform_to_sort
+
+ if(allow_multiple_answers)
+ return tied_winners
+
+ return winner
+
+///returns all industrial_lifts associated with this tram on the given z level or given atoms z level
+/datum/lift_master/proc/get_platforms_on_level(atom/atom_reference_OR_z_level_number)
+ var/z = atom_reference_OR_z_level_number
+ if(isatom(atom_reference_OR_z_level_number))
+ z = atom_reference_OR_z_level_number.z
+
+ if(!isnum(z) || z < 0 || z > world.maxz)
+ return null
+
+ var/list/platforms_in_z = list()
+
+ for(var/obj/structure/industrial_lift/lift_to_check as anything in lift_platforms)
+ if(lift_to_check.z)
+ platforms_in_z += lift_to_check
+
+ return platforms_in_z
+
+/**
+ * Moves the lift UP or DOWN, this is what users invoke with their hand.
+ * This is a SAFE proc, ensuring every part of the lift moves SANELY.
+ * It also locks controls for the (miniscule) duration of the movement, so the elevator cannot be broken by spamming.
+ * Arguments:
+ * going - UP or DOWN directions, where the lift should go. Keep in mind by this point checks of whether it should go up or down have already been done.
+ * user - Whomever made the lift movement.
+ */
+/datum/lift_master/proc/MoveLift(going, mob/user)
+ set_controls(LIFT_PLATFORM_LOCKED)
+ //lift_platforms are sorted in order of lowest z to highest z, so going upwards we need to move them in reverse order to not collide
+ if(going == UP)
+ var/obj/structure/industrial_lift/platform_to_move
+ var/current_index = length(lift_platforms)
+
+ while(current_index > 0)
+ platform_to_move = lift_platforms[current_index]
+ current_index--
+
+ platform_to_move.travel(going)
+
+ else if(going == DOWN)
+ for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
+ lift_platform.travel(going)
+ set_controls(LIFT_PLATFORM_UNLOCKED)
+
+/**
+ * Moves the lift, this is what users invoke with their hand.
+ * This is a SAFE proc, ensuring every part of the lift moves SANELY.
+ * It also locks controls for the (miniscule) duration of the movement, so the elevator cannot be broken by spamming.
+ */
+/datum/lift_master/proc/MoveLiftHorizontal(going)
+ set_controls(LIFT_PLATFORM_LOCKED)
+
+ if(multitile_platform)
+ for(var/obj/structure/industrial_lift/platform_to_move as anything in lift_platforms)
+ platform_to_move.travel(going)
+
+ set_controls(LIFT_PLATFORM_UNLOCKED)
+ return
+
+ var/max_x = 0
+ var/max_y = 0
+ var/max_z = 0
+ var/min_x = world.maxx
+ var/min_y = world.maxy
+ var/min_z = world.maxz
+
+ for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
+ max_z = max(max_z, lift_platform.z)
+ min_z = min(min_z, lift_platform.z)
+
+ min_x = min(min_x, lift_platform.x)
+ max_x = max(max_x, lift_platform.x)
+ //this assumes that all z levels have identical horizontal bounding boxes
+ //but if youre still using a non multitile tram platform at this point
+ //then its your own problem. it wont runtime it will jsut be slower than it needs to be if this assumption isnt
+ //the case
+
+ min_y = min(min_y, lift_platform.y)
+ max_y = max(max_y, lift_platform.y)
+
+ for(var/z in min_z to max_z)
+ //This must be safe way to border tile to tile move of bordered platforms, that excludes platform overlapping.
+ if(going & WEST)
+ //Go along the X axis from min to max, from left to right
+ for(var/x in min_x to max_x)
+ if(going & NORTH)
+ //Go along the Y axis from max to min, from up to down
+ for(var/y in max_y to min_y step -1)
+ var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
+ lift_platform?.travel(going)
+
+ else if(going & SOUTH)
+ //Go along the Y axis from min to max, from down to up
+ for(var/y in min_y to max_y)
+ var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
+ lift_platform?.travel(going)
+
+ else
+ for(var/y in min_y to max_y)
+ var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
+ lift_platform?.travel(going)
+ else
+ //Go along the X axis from max to min, from right to left
+ for(var/x in max_x to min_x step -1)
+ if(going & NORTH)
+ //Go along the Y axis from max to min, from up to down
+ for(var/y in max_y to min_y step -1)
+ var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
+ lift_platform?.travel(going)
+
+ else if (going & SOUTH)
+ for(var/y in min_y to max_y)
+ var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
+ lift_platform?.travel(going)
+
+ else
+ //Go along the Y axis from min to max, from down to up
+ for(var/y in min_y to max_y)
+ var/obj/structure/industrial_lift/lift_platform = locate(/obj/structure/industrial_lift, locate(x, y, z))
+ lift_platform?.travel(going)
+
+ set_controls(LIFT_PLATFORM_UNLOCKED)
+
+///Check destination turfs
+/datum/lift_master/proc/Check_lift_move(check_dir)
+ for(var/obj/structure/industrial_lift/lift_platform as anything in lift_platforms)
+ for(var/turf/bound_turf in lift_platform.locs)
+ var/turf/T = get_step_multiz(lift_platform, check_dir)
+ if(!T)//the edges of multi-z maps
+ return FALSE
+ if(check_dir == UP && !istype(T, /turf/open/openspace)) // We don't want to go through the ceiling!
+ return FALSE
+ if(check_dir == DOWN && !istype(get_turf(lift_platform), /turf/open/openspace)) // No going through the floor!
+ return FALSE
+ return TRUE
+
+/**
+ * Sets all lift parts's controls_locked variable. Used to prevent moving mid movement, or cooldowns.
+ */
+/datum/lift_master/proc/set_controls(state)
+ controls_locked = state
+
+/**
+ * resets the contents of all platforms to their original state in case someone put a bunch of shit onto the tram.
+ * intended to be called by admins. passes all arguments to reset_contents() for each of our platforms.
+ *
+ * Arguments:
+ * * consider_anything_past - number. if > 0 our platforms will only handle foreign contents that exceed this number in each of their locs
+ * * foreign_objects - bool. if true our platforms will consider /atom/movable's that arent mobs as part of foreign contents
+ * * foreign_non_player_mobs - bool. if true our platforms consider mobs that dont have a mind to be foreign
+ * * consider_player_mobs - bool. if true our platforms consider player mobs to be foreign. only works if foreign_non_player_mobs is true as well
+ */
+/datum/lift_master/proc/reset_lift_contents(consider_anything_past = 0, foreign_objects = TRUE, foreign_non_player_mobs = TRUE, consider_player_mobs = FALSE)
+ for(var/obj/structure/industrial_lift/lift_to_reset in lift_platforms)
+ lift_to_reset.reset_contents(consider_anything_past, foreign_objects, foreign_non_player_mobs, consider_player_mobs)
+
+ return TRUE
diff --git a/code/game/machinery/computer/tram_controls.dm b/code/modules/industrial_lift/tram_controls.dm
similarity index 79%
rename from code/game/machinery/computer/tram_controls.dm
rename to code/modules/industrial_lift/tram_controls.dm
index 957547569bebb..a55ff5b18f4f1 100644
--- a/code/game/machinery/computer/tram_controls.dm
+++ b/code/modules/industrial_lift/tram_controls.dm
@@ -6,12 +6,14 @@
circuit = /obj/item/circuitboard/computer/tram_controls
flags_1 = NODECONSTRUCT_1 | SUPERMATTER_IGNORES_1
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
- light_color = LIGHT_COLOR_GREEN
- ///The ID of the tram we control
- var/tram_id = "tram_station"
+
+ light_range = 0 //we dont want to spam SSlighting with source updates every movement
+
///Weakref to the tram piece we control
var/datum/weakref/tram_ref
+ var/specific_lift_id = MAIN_STATION_TRAM
+
/obj/machinery/computer/tram_controls/Initialize(mapload, obj/item/circuitboard/C)
. = ..()
AddComponent(/datum/component/usb_port, list(/obj/item/circuit_component/tram_controls))
@@ -19,7 +21,6 @@
/obj/machinery/computer/tram_controls/LateInitialize()
. = ..()
- //find the tram, late so the tram is all... set up so when this is called? i'm seriously stupid and 90% of what i do consists of barely educated guessing :)
find_tram()
/**
@@ -28,19 +29,17 @@
* Locates tram parts in the lift global list after everything is done.
*/
/obj/machinery/computer/tram_controls/proc/find_tram()
- for(var/obj/structure/industrial_lift/tram/central/tram as anything in GLOB.central_trams)
- if(tram.tram_id != tram_id)
- continue
- tram_ref = WEAKREF(tram)
- break
+ for(var/datum/lift_master/lift as anything in GLOB.active_lifts_by_type[TRAM_LIFT_ID])
+ if(lift.specific_lift_id == specific_lift_id)
+ tram_ref = WEAKREF(lift)
/obj/machinery/computer/tram_controls/ui_state(mob/user)
return GLOB.not_incapacitated_state
/obj/machinery/computer/tram_controls/ui_status(mob/user,/datum/tgui/ui)
- var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve()
+ var/datum/lift_master/tram/tram = tram_ref?.resolve()
- if(tram_part?.travelling)
+ if(tram?.travelling)
return UI_CLOSE
if(!in_range(user, src) && !isobserver(user))
return UI_CLOSE
@@ -54,11 +53,11 @@
ui.open()
/obj/machinery/computer/tram_controls/ui_data(mob/user)
- var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve()
+ var/datum/lift_master/tram/tram_lift = tram_ref?.resolve()
var/list/data = list()
- data["moving"] = tram_part?.travelling
- data["broken"] = tram_part ? FALSE : TRUE
- var/obj/effect/landmark/tram/current_loc = tram_part?.from_where
+ data["moving"] = tram_lift?.travelling
+ data["broken"] = tram_lift ? FALSE : TRUE
+ var/obj/effect/landmark/tram/current_loc = tram_lift?.from_where
if(current_loc)
data["tram_location"] = current_loc.name
return data
@@ -77,9 +76,7 @@
*/
/obj/machinery/computer/tram_controls/proc/get_destinations()
. = list()
- for(var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks)
- if(destination.tram_id != tram_id)
- continue
+ for(var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks[specific_lift_id])
var/list/this_destination = list()
this_destination["name"] = destination.name
this_destination["dest_icons"] = destination.tgui_icons
@@ -94,9 +91,7 @@
switch (action)
if ("send")
var/obj/effect/landmark/tram/to_where
- for (var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks)
- if(destination.tram_id != tram_id)
- continue
+ for (var/obj/effect/landmark/tram/destination as anything in GLOB.tram_landmarks[specific_lift_id])
if(destination.destination_id == params["destination"])
to_where = destination
break
@@ -108,14 +103,13 @@
/// Attempts to sends the tram to the given destination
/obj/machinery/computer/tram_controls/proc/try_send_tram(obj/effect/landmark/tram/to_where)
- var/obj/structure/industrial_lift/tram/central/tram_part = tram_ref?.resolve()
+ var/datum/lift_master/tram/tram_part = tram_ref?.resolve()
if(!tram_part)
return FALSE
- if(tram_part.travelling)
- return FALSE
- if(tram_part.controls_locked) // someone else started
+ if(tram_part.controls_locked) // someone else started already
return FALSE
tram_part.tram_travel(to_where)
+ visible_message("The tram has been called to [to_where.name]")
return TRUE
/obj/item/circuit_component/tram_controls
@@ -147,12 +141,12 @@
. = ..()
if (istype(shell, /obj/machinery/computer/tram_controls))
computer = shell
- var/obj/structure/industrial_lift/tram/central/tram_part = computer.tram_ref?.resolve()
+ var/datum/lift_master/tram/tram_part = computer.tram_ref?.resolve()
RegisterSignal(tram_part, COMSIG_TRAM_SET_TRAVELLING, .proc/on_tram_set_travelling)
RegisterSignal(tram_part, COMSIG_TRAM_TRAVEL, .proc/on_tram_travel)
/obj/item/circuit_component/tram_controls/unregister_usb_parent(atom/movable/shell)
- var/obj/structure/industrial_lift/tram/central/tram_part = computer.tram_ref?.resolve()
+ var/datum/lift_master/tram/tram_part = computer.tram_ref?.resolve()
computer = null
UnregisterSignal(tram_part, list(COMSIG_TRAM_SET_TRAVELLING, COMSIG_TRAM_TRAVEL))
return ..()
@@ -168,9 +162,7 @@
return
var/destination
- for(var/obj/effect/landmark/tram/possible_destination as anything in GLOB.tram_landmarks)
- if(possible_destination.tram_id != computer.tram_id)
- continue
+ for(var/obj/effect/landmark/tram/possible_destination as anything in GLOB.tram_landmarks[computer.specific_lift_id])
if(possible_destination.name == new_destination.value)
destination = possible_destination
break
diff --git a/code/modules/industrial_lift/tram_landmark.dm b/code/modules/industrial_lift/tram_landmark.dm
new file mode 100644
index 0000000000000..db855b58a5c0f
--- /dev/null
+++ b/code/modules/industrial_lift/tram_landmark.dm
@@ -0,0 +1,47 @@
+GLOBAL_LIST_EMPTY(tram_landmarks)
+
+/obj/effect/landmark/tram
+ name = "tram destination" //the tram buttons will mention this.
+ icon_state = "tram"
+
+ ///the id of the tram we're linked to.
+ var/specific_lift_id = MAIN_STATION_TRAM
+ /// The ID of that particular destination.
+ var/destination_id
+ /// Icons for the tgui console to list out for what is at this location
+ var/list/tgui_icons = list()
+
+/obj/effect/landmark/tram/Initialize(mapload)
+ . = ..()
+ LAZYADDASSOCLIST(GLOB.tram_landmarks, specific_lift_id, src)
+
+/obj/effect/landmark/tram/Destroy()
+ LAZYREMOVEASSOC(GLOB.tram_landmarks, specific_lift_id, src)
+ return ..()
+
+
+/obj/effect/landmark/tram/left_part
+ name = "West Wing"
+ destination_id = "left_part"
+ tgui_icons = list("Arrivals" = "plane-arrival", "Command" = "bullhorn", "Security" = "gavel")
+
+/obj/effect/landmark/tram/middle_part
+ name = "Central Wing"
+ destination_id = "middle_part"
+ tgui_icons = list("Service" = "cocktail", "Medical" = "plus", "Engineering" = "wrench")
+
+/obj/effect/landmark/tram/right_part
+ name = "East Wing"
+ destination_id = "right_part"
+ tgui_icons = list("Departures" = "plane-departure", "Cargo" = "box", "Science" = "flask")
+
+/**
+ * lift_id landmarks. used to map in specific_lift_id to trams. when the trams lift_master encounters one on a trams tile
+ * it sets its specific_lift_id to that landmark. allows you to have multiple trams and multiple controls linking to their specific tram
+ */
+/obj/effect/landmark/lift_id
+ name = "lift id setter"
+ icon_state = "lift_id"
+
+ ///what specific id we give to the tram we're placed on, should explicitely set this if its a subtype, or weird things might happen
+ var/specific_lift_id = MAIN_STATION_TRAM
diff --git a/code/modules/industrial_lift/tram_lift_master.dm b/code/modules/industrial_lift/tram_lift_master.dm
new file mode 100644
index 0000000000000..573e961a71d76
--- /dev/null
+++ b/code/modules/industrial_lift/tram_lift_master.dm
@@ -0,0 +1,174 @@
+/datum/lift_master/tram
+
+ ///whether this tram is traveling across vertical and/or horizontal axis for some distance. not all lifts use this
+ var/travelling = FALSE
+ ///if we're travelling, what direction are we going
+ var/travel_direction = NONE
+ ///if we're travelling, how far do we have to go
+ var/travel_distance = 0
+
+ ///multiplier on how much damage/force the tram imparts on things it hits
+ var/collision_lethality = 1
+
+ /// reference to the destination landmark we consider ourselves "at". since we potentially span multiple z levels we dont actually
+ /// know where on us this platform is. as long as we know THAT its on us we can just move the distance and direction between this
+ /// and the destination landmark.
+ var/obj/effect/landmark/tram/from_where
+
+ ///decisecond delay between horizontal movement. cannot make the tram move faster than 1 movement per world.tick_lag.
+ ///this var is poorly named its actually horizontal movement delay but whatever.
+ var/horizontal_speed = 0.5
+
+ ///version of horizontal_speed that gets set in init and is considered our base speed if our lift gets slowed down
+ var/base_horizontal_speed = 0.5
+
+ ///the world.time we should next move at. in case our speed is set to less than 1 movement per tick
+ var/next_move = INFINITY
+
+ ///whether we have been slowed down automatically
+ var/slowed_down = FALSE
+
+ ///how many times we moved while costing more than SStramprocess.max_time milliseconds per movement.
+ ///if this exceeds SStramprocess.max_exceeding_moves
+ var/times_exceeded = 0
+
+ ///how many times we moved while costing less than 0.5 * SStramprocess.max_time milliseconds per movement
+ var/times_below = 0
+
+/datum/lift_master/tram/New(obj/structure/industrial_lift/tram/lift_platform)
+ . = ..()
+ horizontal_speed = lift_platform.horizontal_speed
+ base_horizontal_speed = lift_platform.horizontal_speed
+
+ check_starting_landmark()
+
+/datum/lift_master/tram/vv_edit_var(var_name, var_value)
+ . = ..()
+ if(var_name == "base_horizontal_speed")
+ horizontal_speed = max(horizontal_speed, base_horizontal_speed)
+
+/datum/lift_master/tram/add_lift_platforms(obj/structure/industrial_lift/new_lift_platform)
+ . = ..()
+ RegisterSignal(new_lift_platform, COMSIG_MOVABLE_BUMP, .proc/gracefully_break)
+
+/datum/lift_master/tram/check_for_landmarks(obj/structure/industrial_lift/tram/new_lift_platform)
+ . = ..()
+ for(var/turf/platform_loc as anything in new_lift_platform.locs)
+ var/obj/effect/landmark/tram/initial_destination = locate() in platform_loc
+
+ if(initial_destination)
+ from_where = initial_destination
+
+/datum/lift_master/tram/proc/check_starting_landmark()
+ if(!from_where)
+ CRASH("a tram lift_master was initialized without any tram landmark to give it direction!")
+
+ SStramprocess.can_fire = TRUE
+
+ return TRUE
+
+/**
+ * Signal for when the tram runs into a field of which it cannot go through.
+ * Stops the train's travel fully, sends a message, and destroys the train.
+ * Arguments:
+ * bumped_atom - The atom this tram bumped into
+ */
+/datum/lift_master/tram/proc/gracefully_break(atom/bumped_atom)
+ SIGNAL_HANDLER
+
+ if(istype(bumped_atom, /obj/machinery/field))
+ return
+
+ travel_distance = 0
+
+ bumped_atom.visible_message(span_userdanger("[src] crashes into the field violently!"))
+ for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_platforms)
+ tram_part.set_travelling(FALSE)
+ if(prob(15) || locate(/mob/living) in tram_part.lift_load) //always go boom on people on the track
+ explosion(tram_part, devastation_range = rand(0, 1), heavy_impact_range = 2, light_impact_range = 3) //50% chance of gib
+ qdel(tram_part)
+
+/**
+ * Handles moving the tram
+ *
+ * Tells the individual tram parts where to actually go and has an extra safety check
+ * incase multiple inputs get through, preventing conflicting directions and the tram
+ * literally ripping itself apart. all of the actual movement is handled by SStramprocess
+ */
+/datum/lift_master/tram/proc/tram_travel(obj/effect/landmark/tram/to_where)
+ if(to_where == from_where)
+ return
+
+ travel_direction = get_dir(from_where, to_where)
+ travel_distance = get_dist(from_where, to_where)
+ from_where = to_where
+ set_travelling(TRUE)
+ set_controls(LIFT_PLATFORM_LOCKED)
+ SEND_SIGNAL(src, COMSIG_TRAM_TRAVEL, from_where, to_where)
+
+ for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_platforms) //only thing everyone needs to know is the new location.
+ if(tram_part.travelling) //wee woo wee woo there was a double action queued. damn multi tile structs
+ return //we don't care to undo locked controls, though, as that will resolve itself
+
+ tram_part.glide_size_override = DELAY_TO_GLIDE_SIZE(horizontal_speed)
+ tram_part.set_travelling(TRUE)
+
+ next_move = world.time + horizontal_speed
+
+ START_PROCESSING(SStramprocess, src)
+
+/datum/lift_master/tram/process(delta_time)
+ if(!travel_distance)
+ addtimer(CALLBACK(src, .proc/unlock_controls), 3 SECONDS)
+ return PROCESS_KILL
+ else if(world.time >= next_move)
+ var/start_time = TICK_USAGE
+ travel_distance--
+
+ MoveLiftHorizontal(travel_direction)
+
+ var/duration = TICK_USAGE_TO_MS(start_time)
+ if(slowed_down)
+ if(duration <= (SStramprocess.max_time / 2))
+ times_below++
+ else
+ times_below = 0
+
+ if(times_below >= SStramprocess.max_cheap_moves)
+ horizontal_speed = base_horizontal_speed
+ slowed_down = FALSE
+ times_below = 0
+
+ else if(duration > SStramprocess.max_time)
+ times_exceeded++
+
+ if(times_exceeded >= SStramprocess.max_exceeding_moves)
+ message_admins("The tram at [ADMIN_JMP(lift_platforms[1])] is taking more than [SStramprocess.max_time] milliseconds per movement, halving its movement speed. if this continues to be a problem you can call reset_lift_contents() on the trams lift_master_datum to reset it to its original state and clear added objects")
+ horizontal_speed = base_horizontal_speed * 2 //halves its speed
+ slowed_down = TRUE
+ times_exceeded = 0
+ else
+ times_exceeded = max(times_exceeded - 1, 0)
+
+ next_move = world.time + horizontal_speed
+
+/**
+ * Handles unlocking the tram controls for use after moving
+ *
+ * More safety checks to make sure the tram has actually docked properly
+ * at a location before users are allowed to interact with the tram console again.
+ * Tram finds its location at this point before fully unlocking controls to the user.
+ */
+/datum/lift_master/tram/proc/unlock_controls()
+ set_travelling(FALSE)
+ set_controls(LIFT_PLATFORM_UNLOCKED)
+ for(var/obj/structure/industrial_lift/tram/tram_part as anything in lift_platforms) //only thing everyone needs to know is the new location.
+ tram_part.set_travelling(FALSE)
+
+
+/datum/lift_master/tram/proc/set_travelling(new_travelling)
+ if(travelling == new_travelling)
+ return
+
+ travelling = new_travelling
+ SEND_SIGNAL(src, COMSIG_TRAM_SET_TRAVELLING, travelling)
diff --git a/code/modules/industrial_lift/tram_override_objects.dm b/code/modules/industrial_lift/tram_override_objects.dm
new file mode 100644
index 0000000000000..7a348295a2d27
--- /dev/null
+++ b/code/modules/industrial_lift/tram_override_objects.dm
@@ -0,0 +1,39 @@
+/**
+ * the tram has a few objects mapped onto it at roundstart, by default many of those objects have unwanted properties
+ * for example grilles and windows have the atmos_sensitive element applied to them, which makes them register to
+ * themselves moving to re register signals onto the turf via connect_loc. this is bad and dumb since it makes the tram
+ * more expensive to move.
+ *
+ * if you map something on to the tram, make SURE if possible that it doesnt have anythign reacting to its own movement
+ * it will make the tram more expensive to move and we dont want that because we dont want to return to the days where
+ * the tram took a third of the tick per movement when its just carrying its default mapped in objects
+ */
+/obj/structure/grille/tram/Initialize(mapload)
+ . = ..()
+ RemoveElement(/datum/element/atmos_sensitive, mapload)
+ //atmos_sensitive applies connect_loc which 1. reacts to movement in order to 2. unregister and register signals to
+ //the old and new locs. we dont want that, pretend these grilles and windows are plastic or something idk
+
+/obj/structure/window/reinforced/shuttle/tram/Initialize(mapload, direct)
+ . = ..()
+ RemoveElement(/datum/element/atmos_sensitive, mapload)
+
+/obj/structure/shuttle/engine/propulsion/in_wall/tram
+ //if this has opacity, then every movement of the tram causes lighting updates
+ //DO NOT put something on the tram roundstart that has opacity, it WILL overload SSlighting
+ opacity = FALSE
+
+/obj/machinery/door/window/left/tram
+/obj/machinery/door/window/right/tram
+
+/obj/machinery/door/window/left/tram/Initialize(mapload, set_dir, unres_sides)
+ . = ..()
+ RemoveElement(/datum/element/atmos_sensitive, mapload)
+
+/obj/machinery/door/window/right/tram/Initialize(mapload, set_dir, unres_sides)
+ . = ..()
+ RemoveElement(/datum/element/atmos_sensitive, mapload)
+
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/left/tram, 0)
+MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/door/window/right/tram, 0)
+
diff --git a/code/game/objects/structures/tram_walls.dm b/code/modules/industrial_lift/tram_walls.dm
similarity index 99%
rename from code/game/objects/structures/tram_walls.dm
rename to code/modules/industrial_lift/tram_walls.dm
index 092b1d5e2e593..17844dbfa5d7e 100644
--- a/code/game/objects/structures/tram_walls.dm
+++ b/code/modules/industrial_lift/tram_walls.dm
@@ -10,7 +10,7 @@
base_icon_state = "wall"
layer = LOW_OBJ_LAYER
density = TRUE
- opacity = TRUE
+ opacity = FALSE
max_integrity = 100
smoothing_flags = SMOOTH_BITMASK
smoothing_groups = list(SMOOTH_GROUP_CLOSED_TURFS, SMOOTH_GROUP_WALLS)
diff --git a/code/modules/instruments/items.dm b/code/modules/instruments/items.dm
index 3ac8c64292ebc..29449a195e196 100644
--- a/code/modules/instruments/items.dm
+++ b/code/modules/instruments/items.dm
@@ -215,6 +215,17 @@
. = ..()
UnregisterSignal(M, COMSIG_MOB_SAY)
+/datum/action/item_action/instrument
+ name = "Use Instrument"
+ desc = "Use the instrument specified"
+
+/datum/action/item_action/instrument/Trigger(trigger_flags)
+ if(istype(target, /obj/item/instrument))
+ var/obj/item/instrument/I = target
+ I.interact(usr)
+ return
+ return ..()
+
/obj/item/instrument/bikehorn
name = "gilded bike horn"
desc = "An exquisitely decorated bike horn, capable of honking in a variety of notes."
diff --git a/code/modules/instruments/songs/play_legacy.dm b/code/modules/instruments/songs/play_legacy.dm
index dbcdf2a742094..1b7efcad8a042 100644
--- a/code/modules/instruments/songs/play_legacy.dm
+++ b/code/modules/instruments/songs/play_legacy.dm
@@ -87,5 +87,5 @@
L.apply_status_effect(/datum/status_effect/good_music)
if(!(M?.client?.prefs?.toggles & SOUND_INSTRUMENTS))
continue
- M.playsound_local(source, null, volume * using_instrument.volume_multiplier, S = music_played)
+ M.playsound_local(source, null, volume * using_instrument.volume_multiplier, sound_to_use = music_played)
// Could do environment and echo later but not for now
diff --git a/code/modules/instruments/stationary.dm b/code/modules/instruments/stationary.dm
index 8245ed03f0002..3942132366d2d 100644
--- a/code/modules/instruments/stationary.dm
+++ b/code/modules/instruments/stationary.dm
@@ -2,6 +2,7 @@
name = "Not A Piano"
desc = "Something broke, contact coderbus."
interaction_flags_atom = INTERACT_ATOM_ATTACK_HAND | INTERACT_ATOM_UI_INTERACT | INTERACT_ATOM_REQUIRES_DEXTERITY
+ integrity_failure = 0.25
var/can_play_unanchored = FALSE
var/list/allowed_instrument_ids = list("r3grand","r3harpsi","crharpsi","crgrand1","crbright1", "crichugan", "crihamgan","piano")
var/datum/song/song
@@ -32,22 +33,32 @@
return TOOL_ACT_TOOLTYPE_SUCCESS
/obj/structure/musician/piano
- name = "space minimoog"
+ name = "space piano"
+ desc = "This is a space piano, like a regular piano, but always in tune! Even if the musician isn't."
icon = 'icons/obj/musician.dmi'
- icon_state = "minimoog"
+ icon_state = "piano"
anchored = TRUE
density = TRUE
+ var/broken_icon_state = "pianobroken"
+
+/obj/structure/musician/piano/play_attack_sound(damage_amount, damage_type = BRUTE, damage_flag = 0)
+ switch(damage_type)
+ if(BRUTE)
+ playsound(src, 'sound/effects/piano_hit.ogg', 100, TRUE)
+ if(BURN)
+ playsound(src, 'sound/items/welder.ogg', 100, TRUE)
+
+/obj/structure/musician/piano/atom_break(damage_flag)
+ . = ..()
+ if(!broken)
+ broken = TRUE
+ icon_state = broken_icon_state
/obj/structure/musician/piano/unanchored
anchored = FALSE
-/obj/structure/musician/piano/Initialize(mapload)
- . = ..()
- if(prob(50) && icon_state == initial(icon_state))
- name = "space minimoog"
- desc = "This is a minimoog, like a space piano, but more spacey!"
- icon_state = "minimoog"
- else
- name = "space piano"
- desc = "This is a space piano, like a regular piano, but always in tune! Even if the musician isn't."
- icon_state = "piano"
+/obj/structure/musician/piano/minimoog
+ name = "space minimoog"
+ desc = "This is a minimoog, like a space piano, but more spacey!"
+ icon_state = "minimoog"
+ broken_icon_state = "minimoogbroken"
diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm
index c7d387d2e73c2..cc1a8f3e23881 100644
--- a/code/modules/jobs/job_types/_job.dm
+++ b/code/modules/jobs/job_types/_job.dm
@@ -120,6 +120,9 @@
///RPG job names, for the memes
var/rpg_title
+ /// Does this job ignore human authority?
+ var/ignore_human_authority = FALSE
+
/datum/job/New()
. = ..()
@@ -441,6 +444,10 @@
return // Disconnected while checking for the appearance ban.
var/require_human = CONFIG_GET(flag/enforce_human_authority) && (job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND)
+ if(require_human)
+ var/all_authority_require_human = CONFIG_GET(flag/enforce_human_authority_on_everyone)
+ if(!all_authority_require_human && job.ignore_human_authority)
+ require_human = FALSE
src.job = job.title
diff --git a/code/modules/jobs/job_types/botanist.dm b/code/modules/jobs/job_types/botanist.dm
index 92a9aea6db2d3..7dec53564e298 100644
--- a/code/modules/jobs/job_types/botanist.dm
+++ b/code/modules/jobs/job_types/botanist.dm
@@ -20,13 +20,18 @@
/datum/job_department/service,
)
- family_heirlooms = list(/obj/item/cultivator, /obj/item/reagent_containers/glass/bucket, /obj/item/toy/plush/beeplushie)
+ family_heirlooms = list(
+ /obj/item/cultivator,
+ /obj/item/reagent_containers/glass/watering_can/wood,
+ /obj/item/toy/plush/beeplushie,
+ )
mail_goodies = list(
/obj/item/reagent_containers/glass/bottle/mutagen = 20,
/obj/item/reagent_containers/glass/bottle/saltpetre = 20,
/obj/item/reagent_containers/glass/bottle/diethylamine = 20,
/obj/item/gun/energy/floragun = 10,
+ /obj/item/reagent_containers/glass/watering_can/advanced = 10,
/obj/effect/spawner/random/food_or_drink/seed_rare = 5,// These are strong, rare seeds, so use sparingly.
/obj/item/food/monkeycube/bee = 2
)
diff --git a/code/modules/jobs/job_types/cargo_technician.dm b/code/modules/jobs/job_types/cargo_technician.dm
index 2064af32f37a8..a3c7a1651df95 100644
--- a/code/modules/jobs/job_types/cargo_technician.dm
+++ b/code/modules/jobs/job_types/cargo_technician.dm
@@ -3,11 +3,11 @@
description = "Distribute supplies to the departments that ordered them, \
collect empty crates, load and unload the supply shuttle, \
ship bounty cubes."
- department_head = list(JOB_HEAD_OF_PERSONNEL)
+ department_head = list(JOB_QUARTERMASTER)
faction = FACTION_STATION
total_positions = 3
spawn_positions = 2
- supervisors = "the quartermaster and the head of personnel"
+ supervisors = "the quartermaster"
selection_color = "#dcba97"
exp_granted_type = EXP_TYPE_CREW
diff --git a/code/modules/jobs/job_types/head_of_personnel.dm b/code/modules/jobs/job_types/head_of_personnel.dm
index 69e56ab13e91d..b86d7c08b5060 100644
--- a/code/modules/jobs/job_types/head_of_personnel.dm
+++ b/code/modules/jobs/job_types/head_of_personnel.dm
@@ -4,7 +4,7 @@
protect Ian, run the station when the captain dies."
auto_deadmin_role_flags = DEADMIN_POSITION_HEAD
department_head = list(JOB_CAPTAIN)
- head_announce = list(RADIO_CHANNEL_SUPPLY, RADIO_CHANNEL_SERVICE)
+ head_announce = list(RADIO_CHANNEL_SERVICE)
faction = FACTION_STATION
total_positions = 1
spawn_positions = 1
@@ -21,7 +21,6 @@
plasmaman_outfit = /datum/outfit/plasmaman/head_of_personnel
departments_list = list(
/datum/job_department/service,
- /datum/job_department/cargo,
/datum/job_department/command,
)
diff --git a/code/modules/jobs/job_types/janitor.dm b/code/modules/jobs/job_types/janitor.dm
index 803620a456f7c..23852862ad4ea 100644
--- a/code/modules/jobs/job_types/janitor.dm
+++ b/code/modules/jobs/job_types/janitor.dm
@@ -43,7 +43,7 @@
/datum/outfit/job/janitor/pre_equip(mob/living/carbon/human/H, visualsOnly)
. = ..()
if(GARBAGEDAY in SSevents.holidays)
- backpack_contents += /obj/item/gun/ballistic/revolver
+ backpack_contents += list(/obj/item/gun/ballistic/revolver)
r_pocket = /obj/item/ammo_box/a357
/datum/outfit/job/janitor/get_types_to_preload()
diff --git a/code/modules/jobs/job_types/mime.dm b/code/modules/jobs/job_types/mime.dm
index 80e0e968b60af..ad5cd1637db83 100644
--- a/code/modules/jobs/job_types/mime.dm
+++ b/code/modules/jobs/job_types/mime.dm
@@ -64,6 +64,7 @@
backpack = /obj/item/storage/backpack/mime
satchel = /obj/item/storage/backpack/mime
+ box = /obj/item/storage/box/hug/black/survival
chameleon_extras = /obj/item/stamp/mime
/datum/outfit/job/mime/post_equip(mob/living/carbon/human/H, visualsOnly = FALSE)
@@ -72,8 +73,10 @@
if(visualsOnly)
return
+ // Start our mime out with a vow of silence and the ability to break (or make) it
if(H.mind)
- H.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/mime/speak(null))
+ var/datum/action/cooldown/spell/vow_of_silence/vow = new(H.mind)
+ vow.Grant(H)
H.mind.miming = TRUE
var/datum/atom_hud/fan = GLOB.huds[DATA_HUD_FAN]
@@ -85,23 +88,41 @@
icon_state = "bookmime"
/obj/item/book/mimery/attack_self(mob/user)
+ . = ..()
+ if(.)
+ return
+
var/list/spell_icons = list(
"Invisible Wall" = image(icon = 'icons/mob/actions/actions_mime.dmi', icon_state = "invisible_wall"),
"Invisible Chair" = image(icon = 'icons/mob/actions/actions_mime.dmi', icon_state = "invisible_chair"),
"Invisible Box" = image(icon = 'icons/mob/actions/actions_mime.dmi', icon_state = "invisible_box")
)
var/picked_spell = show_radial_menu(user, src, spell_icons, custom_check = CALLBACK(src, .proc/check_menu, user), radius = 36, require_near = TRUE)
+ var/datum/action/cooldown/spell/picked_spell_type
switch(picked_spell)
if("Invisible Wall")
- user.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/mime_wall(null))
+ picked_spell_type = /datum/action/cooldown/spell/conjure/invisible_wall
+
if("Invisible Chair")
- user.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/mime_chair(null))
+ picked_spell_type = /datum/action/cooldown/spell/conjure/invisible_chair
+
if("Invisible Box")
- user.mind.AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/conjure/mime_box(null))
- else
- return
- to_chat(user, span_warning("The book disappears into thin air."))
- qdel(src)
+ picked_spell_type = /datum/action/cooldown/spell/conjure_item/invisible_box
+
+ if(ispath(picked_spell_type))
+ // Gives the user a vow ability too, if they don't already have one
+ var/datum/action/cooldown/spell/vow_of_silence/vow = locate() in user.actions
+ if(!vow && user.mind)
+ vow = new(user.mind)
+ vow.Grant(user)
+
+ picked_spell_type = new picked_spell_type(user.mind || user)
+ picked_spell_type.Grant(user)
+
+ to_chat(user, span_warning("The book disappears into thin air."))
+ qdel(src)
+
+ return TRUE
/**
* Checks if we are allowed to interact with a radial menu
diff --git a/code/modules/jobs/job_types/quartermaster.dm b/code/modules/jobs/job_types/quartermaster.dm
index 6607aa9e98552..34211559e47b0 100644
--- a/code/modules/jobs/job_types/quartermaster.dm
+++ b/code/modules/jobs/job_types/quartermaster.dm
@@ -2,11 +2,13 @@
title = JOB_QUARTERMASTER
description = "Coordinate cargo technicians and shaft miners, assist with \
economical purchasing."
- department_head = list(JOB_HEAD_OF_PERSONNEL)
+ auto_deadmin_role_flags = DEADMIN_POSITION_HEAD
+ department_head = list(JOB_CAPTAIN)
faction = FACTION_STATION
total_positions = 1
spawn_positions = 1
- supervisors = "the head of personnel"
+ minimal_player_age = 7
+ supervisors = "the captain"
selection_color = "#d7b088"
exp_required_type_department = EXP_TYPE_SUPPLY
exp_granted_type = EXP_TYPE_CREW
@@ -14,15 +16,16 @@
outfit = /datum/outfit/job/quartermaster
plasmaman_outfit = /datum/outfit/plasmaman/cargo
- paycheck = PAYCHECK_CREW
+ paycheck = PAYCHECK_COMMAND
paycheck_department = ACCOUNT_CAR
- liver_traits = list(TRAIT_PRETENDER_ROYAL_METABOLISM)
+ liver_traits = list(TRAIT_ROYAL_METABOLISM) // finally upgraded
display_order = JOB_DISPLAY_ORDER_QUARTERMASTER
bounty_types = CIV_JOB_RANDOM
departments_list = list(
/datum/job_department/cargo,
+ /datum/job_department/command,
)
family_heirlooms = list(/obj/item/stamp, /obj/item/stamp/denied)
mail_goodies = list(
@@ -30,16 +33,19 @@
)
rpg_title = "Steward"
job_flags = JOB_ANNOUNCE_ARRIVAL | JOB_CREW_MANIFEST | JOB_EQUIP_RANK | JOB_CREW_MEMBER | JOB_NEW_PLAYER_JOINABLE | JOB_BOLD_SELECT_TEXT | JOB_REOPEN_ON_ROUNDSTART_LOSS | JOB_ASSIGN_QUIRKS | JOB_CAN_BE_INTERN
-
+ ignore_human_authority = TRUE
/datum/outfit/job/quartermaster
name = "Quartermaster"
jobtype = /datum/job/quartermaster
-
+ backpack_contents = list(
+ /obj/item/melee/baton/telescopic = 1,
+ )
id_trim = /datum/id_trim/job/quartermaster
+ id = /obj/item/card/id/advanced/silver
uniform = /obj/item/clothing/under/rank/cargo/qm
- belt = /obj/item/modular_computer/tablet/pda/quartermaster
- ears = /obj/item/radio/headset/headset_cargo
+ belt = /obj/item/modular_computer/tablet/pda/heads/quartermaster
+ ears = /obj/item/radio/headset/heads/qm
glasses = /obj/item/clothing/glasses/sunglasses
shoes = /obj/item/clothing/shoes/sneakers/brown
l_hand = /obj/item/clipboard
diff --git a/code/modules/jobs/job_types/scientist.dm b/code/modules/jobs/job_types/scientist.dm
index 99b62de0fa1fb..2978c9572e923 100644
--- a/code/modules/jobs/job_types/scientist.dm
+++ b/code/modules/jobs/job_types/scientist.dm
@@ -53,9 +53,19 @@
/datum/outfit/job/scientist/pre_equip(mob/living/carbon/human/H)
..()
- if(prob(0.4))
+ try_giving_horrible_tie()
+
+/datum/outfit/job/scientist/proc/try_giving_horrible_tie()
+ if (prob(0.4))
neck = /obj/item/clothing/neck/tie/horrible
/datum/outfit/job/scientist/get_types_to_preload()
. = ..()
. += /obj/item/clothing/neck/tie/horrible
+
+/// A version of the scientist outfit that is guaranteed to be the same every time
+/datum/outfit/job/scientist/consistent
+ name = "Scientist - Consistent"
+
+/datum/outfit/job/scientist/consistent/try_giving_horrible_tie()
+ return
diff --git a/code/modules/jobs/job_types/shaft_miner.dm b/code/modules/jobs/job_types/shaft_miner.dm
index 717195d1f9887..c04f2b9788ab2 100644
--- a/code/modules/jobs/job_types/shaft_miner.dm
+++ b/code/modules/jobs/job_types/shaft_miner.dm
@@ -2,11 +2,11 @@
title = JOB_SHAFT_MINER
description = "Travel to strange lands. Mine ores. \
Meet strange creatures. Kill them for their gold."
- department_head = list(JOB_HEAD_OF_PERSONNEL)
+ department_head = list(JOB_QUARTERMASTER)
faction = FACTION_STATION
total_positions = 3
spawn_positions = 3
- supervisors = "the quartermaster and the head of personnel"
+ supervisors = "the quartermaster"
selection_color = "#dcba97"
exp_granted_type = EXP_TYPE_CREW
diff --git a/code/modules/jobs/job_types/warden.dm b/code/modules/jobs/job_types/warden.dm
index 6906fb3968153..e5ed4565b8029 100644
--- a/code/modules/jobs/job_types/warden.dm
+++ b/code/modules/jobs/job_types/warden.dm
@@ -22,7 +22,7 @@
paycheck_department = ACCOUNT_SEC
mind_traits = list(TRAIT_DONUT_LOVER)
- liver_traits = list(TRAIT_LAW_ENFORCEMENT_METABOLISM)
+ liver_traits = list(TRAIT_LAW_ENFORCEMENT_METABOLISM, TRAIT_PRETENDER_ROYAL_METABOLISM)
display_order = JOB_DISPLAY_ORDER_WARDEN
bounty_types = CIV_JOB_SEC
diff --git a/code/modules/keybindings/setup.dm b/code/modules/keybindings/setup.dm
index de07c952c72df..bb2757761e00e 100644
--- a/code/modules/keybindings/setup.dm
+++ b/code/modules/keybindings/setup.dm
@@ -32,9 +32,10 @@
var/command = macro_set[key]
winset(src, "default-[REF(key)]", "parent=default;name=[key];command=[command]")
- if(hotkeys)
- winset(src, null, "input.focus=true input.background-color=[COLOR_INPUT_ENABLED]")
- else
- winset(src, null, "input.focus=true input.background-color=[COLOR_INPUT_DISABLED]")
+ //Reactivate any active tgui windows mouse passthroughs macros
+ for(var/datum/tgui_window/window in tgui_windows)
+ if(window.mouse_event_macro_set)
+ window.mouse_event_macro_set = FALSE
+ window.set_mouse_macro()
update_special_keybinds()
diff --git a/code/modules/lighting/lighting_atom.dm b/code/modules/lighting/lighting_atom.dm
index 4beac5b1dd264..f0487b86b1a54 100644
--- a/code/modules/lighting/lighting_atom.dm
+++ b/code/modules/lighting/lighting_atom.dm
@@ -25,8 +25,8 @@
#undef NONSENSICAL_VALUE
-// Will update the light (duh).
-// Creates or destroys it if needed, makes it update values, makes sure it's got the correct source turf...
+/// Will update the light (duh).
+/// Creates or destroys it if needed, makes it update values, makes sure it's got the correct source turf...
/atom/proc/update_light()
set waitfor = FALSE
if (QDELETED(src))
@@ -80,13 +80,6 @@
return
recalculate_directional_opacity()
-
-/atom/movable/Moved(atom/OldLoc, Dir)
- . = ..()
- for (var/datum/light_source/light as anything in light_sources) // Cycle through the light sources on this atom and tell them to update.
- light.source_atom.update_light()
-
-
/atom/proc/flash_lighting_fx(_range = FLASH_LIGHT_RANGE, _power = FLASH_LIGHT_POWER, _color = COLOR_WHITE, _duration = FLASH_LIGHT_DURATION)
return
diff --git a/code/modules/lighting/lighting_corner.dm b/code/modules/lighting/lighting_corner.dm
index 19285ae988818..572cd8b50d7ea 100644
--- a/code/modules/lighting/lighting_corner.dm
+++ b/code/modules/lighting/lighting_corner.dm
@@ -88,8 +88,14 @@
// God that was a mess, now to do the rest of the corner code! Hooray!
/datum/lighting_corner/proc/update_lumcount(delta_r, delta_g, delta_b)
+
+#ifdef VISUALIZE_LIGHT_UPDATES
+ if (!SSlighting.allow_duped_values && !(delta_r || delta_g || delta_b)) // 0 is falsey ok
+ return
+#else
if (!(delta_r || delta_g || delta_b)) // 0 is falsey ok
return
+#endif
lum_r += delta_r
lum_g += delta_g
@@ -109,6 +115,10 @@
if (largest_color_luminosity > 1)
. = 1 / largest_color_luminosity
+ var/old_r = cache_r
+ var/old_g = cache_g
+ var/old_b = cache_b
+
#if LIGHTING_SOFT_THRESHOLD != 0
else if (largest_color_luminosity < LIGHTING_SOFT_THRESHOLD)
. = 0 // 0 means soft lighting.
@@ -123,6 +133,13 @@
#endif
src.largest_color_luminosity = round(largest_color_luminosity, LIGHTING_ROUND_VALUE)
+#ifdef VISUALIZE_LIGHT_UPDATES
+ if(!SSlighting.allow_duped_corners && old_r == cache_r && old_g == cache_g && old_b == cache_b)
+ return
+#else
+ if(old_r == cache_r && old_g == cache_g && old_b == cache_b)
+ return
+#endif
var/datum/lighting_object/lighting_object = master_NE?.lighting_object
if (lighting_object && !lighting_object.needs_update)
diff --git a/code/modules/lighting/lighting_object.dm b/code/modules/lighting/lighting_object.dm
index 21cfb8003cfdd..9004e35b20461 100644
--- a/code/modules/lighting/lighting_object.dm
+++ b/code/modules/lighting/lighting_object.dm
@@ -43,6 +43,11 @@
return ..()
/datum/lighting_object/proc/update()
+#ifdef VISUALIZE_LIGHT_UPDATES
+ affected_turf.add_atom_colour(COLOR_BLUE_LIGHT, ADMIN_COLOUR_PRIORITY)
+ animate(affected_turf, 10, color = null)
+ addtimer(CALLBACK(affected_turf, /atom/proc/remove_atom_colour, ADMIN_COLOUR_PRIORITY, COLOR_BLUE_LIGHT), 10, TIMER_UNIQUE|TIMER_OVERRIDE)
+#endif
// To the future coder who sees this and thinks
// "Why didn't he just use a loop?"
diff --git a/code/modules/lighting/lighting_source.dm b/code/modules/lighting/lighting_source.dm
index ca600b7d8f26d..f9c48fb87ce0f 100644
--- a/code/modules/lighting/lighting_source.dm
+++ b/code/modules/lighting/lighting_source.dm
@@ -40,10 +40,10 @@
/datum/light_source/New(atom/owner, atom/top)
source_atom = owner // Set our new owner.
- LAZYADD(source_atom.light_sources, src)
+ add_to_light_sources(source_atom.light_sources)
top_atom = top
if (top_atom != source_atom)
- LAZYADD(top_atom.light_sources, src)
+ add_to_light_sources(top_atom.light_sources)
source_turf = top_atom
pixel_turf = get_turf_pixel(top_atom) || source_turf
@@ -59,10 +59,10 @@
/datum/light_source/Destroy(force)
remove_lum()
if (source_atom)
- LAZYREMOVE(source_atom.light_sources, src)
+ remove_from_light_sources(source_atom.light_sources)
if (top_atom)
- LAZYREMOVE(top_atom.light_sources, src)
+ remove_from_light_sources(top_atom.light_sources)
if (needs_update)
SSlighting.sources_queue -= src
@@ -74,6 +74,35 @@
return ..()
+///add this light source to new_atom_host's light_sources list. updating movement registrations as needed
+/datum/light_source/proc/add_to_light_sources(atom/new_atom_host)
+ if(QDELETED(new_atom_host))
+ return FALSE
+
+ LAZYADD(new_atom_host.light_sources, src)
+ if(ismovable(new_atom_host) && new_atom_host == source_atom)
+ RegisterSignal(new_atom_host, COMSIG_MOVABLE_MOVED, .proc/update_host_lights)
+ return TRUE
+
+///remove this light source from old_atom_host's light_sources list, unsetting movement registrations
+/datum/light_source/proc/remove_from_light_sources(atom/old_atom_host)
+ if(QDELETED(old_atom_host))
+ return FALSE
+
+ LAZYREMOVE(old_atom_host.light_sources, src)
+ if(ismovable(old_atom_host) && old_atom_host == source_atom)
+ UnregisterSignal(old_atom_host, COMSIG_MOVABLE_MOVED)
+ return TRUE
+
+///signal handler for when our host atom moves and we need to update our effects
+/datum/light_source/proc/update_host_lights(atom/movable/host)
+ SIGNAL_HANDLER
+
+ if(QDELETED(host))
+ return
+
+ host.update_light()
+
// Yes this doesn't align correctly on anything other than 4 width tabs.
// If you want it to go switch everybody to elastic tab stops.
// Actually that'd be great if you could!
@@ -84,17 +113,17 @@
needs_update = level; \
-// This proc will cause the light source to update the top atom, and add itself to the update queue.
+/// This proc will cause the light source to update the top atom, and add itself to the update queue.
/datum/light_source/proc/update(atom/new_top_atom)
// This top atom is different.
if (new_top_atom && new_top_atom != top_atom)
if(top_atom != source_atom && top_atom.light_sources) // Remove ourselves from the light sources of that top atom.
- LAZYREMOVE(top_atom.light_sources, src)
+ remove_from_light_sources(top_atom.light_sources)
top_atom = new_top_atom
if (top_atom != source_atom)
- LAZYADD(top_atom.light_sources, src) // Add ourselves to the light sources of our new top atom.
+ add_to_light_sources(top_atom.light_sources)
EFFECT_UPDATE(LIGHTING_CHECK_UPDATE)
diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm b/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm
index 98aa475b8c2c1..330d3671a5009 100644
--- a/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm
+++ b/code/modules/mapfluff/ruins/lavalandruin_code/biodome_winter.dm
@@ -41,8 +41,9 @@
if(hit_object.resistance_flags & FREEZE_PROOF)
hit_object.visible_message(span_warning("[hit_object] is freeze-proof!"))
return
- if(!(hit_object.obj_flags & FROZEN))
- hit_object.make_frozen_visual()
+ if(HAS_TRAIT(hit_object, TRAIT_FROZEN))
+ return
+ hit_object.AddElement(/datum/element/frozen)
else if(isliving(hit_atom))
var/mob/living/hit_mob = hit_atom
SSmove_manager.stop_looping(hit_mob) //stops them mid pathing even if they're stunimmune
diff --git a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm
index 59839aeb9d9e8..ec7d8afa10e12 100644
--- a/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm
+++ b/code/modules/mapfluff/ruins/lavalandruin_code/elephantgraveyard.dm
@@ -226,16 +226,6 @@
/obj/effect/decal/remains/human/grave
turf_loc_check = FALSE
-/obj/item/book/granter/crafting_recipe/boneyard_notes
- name = "The Complete Works of Lavaland Bone Architecture"
- desc = "Pried from the lead Archaeologist's cold, dead hands, this seems to explain how ancient bone architecture was erected long ago."
- crafting_recipe_types = list(/datum/crafting_recipe/rib, /datum/crafting_recipe/boneshovel, /datum/crafting_recipe/halfskull, /datum/crafting_recipe/skull)
- icon = 'icons/obj/library.dmi'
- icon_state = "boneworking_learing"
- oneuse = FALSE
- remarks = list("Who knew you could bend bones that far back?", "I guess that was much easier before the planet heated up...", "So that's how they made those ruins survive the ashstorms. Neat!", "The page is just filled with insane ramblings about some 'legion' thing.", "But why would they need vinegar to polish the bones? And rags too?", "You spend a few moments cleaning dirt and blood off of the page, yeesh.")
-
-
//***Fluff items for lore/intrigue
/obj/item/paper/crumpled/muddy/fluff/elephant_graveyard
name = "posted warning"
diff --git a/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm b/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm
index 7d12dd93c3c45..1c6a8fb98e655 100644
--- a/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm
+++ b/code/modules/mapfluff/ruins/objects_and_mobs/necropolis_gate.dm
@@ -187,7 +187,7 @@ GLOBAL_DATUM(necropolis_gate, /obj/structure/necropolis_gate/legion_gate)
for(var/mob/M in GLOB.player_list)
if(M.z == z)
to_chat(M, span_userdanger("Discordant whispers flood your mind in a thousand voices. Each one speaks your name, over and over. Something horrible has been released."))
- M.playsound_local(T, null, 100, FALSE, 0, FALSE, pressure_affected = FALSE, S = legion_sound)
+ M.playsound_local(T, null, 100, FALSE, 0, FALSE, pressure_affected = FALSE, sound_to_use = legion_sound)
flash_color(M, flash_color = "#FF0000", flash_time = 50)
var/mutable_appearance/release_overlay = mutable_appearance('icons/effects/effects.dmi', "legiondoor")
notify_ghosts("Legion has been released in the [get_area(src)]!", source = src, alert_overlay = release_overlay, action = NOTIFY_JUMP, flashwindow = FALSE)
diff --git a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
index c587460bb10a3..c9d36928a656a 100644
--- a/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
+++ b/code/modules/mapfluff/ruins/spaceruin_code/hilbertshotel.dm
@@ -494,19 +494,22 @@ GLOBAL_VAR_INIT(hhMysteryRoomNumber, rand(1, 999999))
else
to_chat(user, "No vacated rooms.")
+/obj/effect/landmark/lift_id/hilbert
+ specific_lift_id = HILBERT_TRAM
+
/obj/effect/landmark/tram/left_part/hilbert
+ specific_lift_id = HILBERT_TRAM
destination_id = "left_part_hilbert"
- tram_id = "tram_hilbert"
tgui_icons = list("Reception" = "briefcase", "Botany" = "leaf", "Chemistry" = "flask")
/obj/effect/landmark/tram/middle_part/hilbert
+ specific_lift_id = HILBERT_TRAM
destination_id = "middle_part_hilbert"
- tram_id = "tram_hilbert"
tgui_icons = list("Processing" = "cogs", "Xenobiology" = "paw")
/obj/effect/landmark/tram/right_part/hilbert
+ specific_lift_id = HILBERT_TRAM
destination_id = "right_part_hilbert"
- tram_id = "tram_hilbert"
tgui_icons = list("Ordnance" = "bullseye", "Office" = "user", "Dormitories" = "bed")
/obj/item/keycard/hilbert
@@ -520,6 +523,7 @@ GLOBAL_VAR_INIT(hhMysteryRoomNumber, rand(1, 999999))
puzzle_id = "hilbert_office"
/datum/outfit/doctorhilbert
+ name = "Doctor Hilbert"
id = /obj/item/card/id/advanced/silver
uniform = /obj/item/clothing/under/rank/rnd/research_director/doctor_hilbert
shoes = /obj/item/clothing/shoes/sneakers/brown
diff --git a/code/modules/mapfluff/ruins/spaceruin_code/listeningstation.dm b/code/modules/mapfluff/ruins/spaceruin_code/listeningstation.dm
index 925f9048c4783..c6302e6fefa99 100644
--- a/code/modules/mapfluff/ruins/spaceruin_code/listeningstation.dm
+++ b/code/modules/mapfluff/ruins/spaceruin_code/listeningstation.dm
@@ -1,45 +1,118 @@
-/////////// listening station
+///Papers used in The Listening Station ruin.
/obj/item/paper/fluff/ruins/listeningstation/reports
info = "Nothing of interest to report."
+// Original "background" tone.
+/obj/item/paper/fluff/ruins/listeningstation/reports/march
+ name = "march report"
+ info = {"Accepted new assignment from liasion at MI13. Mission: Report on all Nanotrasen activities in this sector.
+ Secondary Mission is to ensure survival of the person in the sleeper. This should not be hard, I have had personal experience with
+ this model of sleeper, and I know it to be of good quality.
+ "}
+
+/obj/item/paper/fluff/ruins/listeningstation/reports/april
+ name = "april report"
+ info = {"A good start to the operation: intercepted Nanotrasen military communications. A convoy is scheduled to transfer nuclear warheads to a new military base.
+ This is as good a chance as any to get our hands on some heavy weaponry, I suggest we take it.
+ As far as base work goes, I have begun a refurnishing of the base using the supplies I received during my shipment. It takes the mind off for whenever the station goes... "dark".
+
+ I suppose I shall await further commands.
+ "}
+
+/obj/item/paper/fluff/ruins/listeningstation/reports/may
+ name = "may report"
+ info = {"Nothing of real interest to report this month. I have intercepted faint transmissions from what appears to be some sort of pirate radio station. They do not appear to be relevant to my assignment.
+ Using my connections, I was able to procure some signs and posters from Nanotrasen ships. I have a bundle of markers, so I spent today "disguising" the asteroid. Sloppy work, but I think it fits well.
+ It's a nice base, this. I have certainly served in worse conditions.
+ I have not heard anything about the mission on the new military base. I will press the matter later today.
+ "}
+
+/obj/item/paper/fluff/ruins/listeningstation/reports/june
+ name = "june report"
+ info = {"Nanotrasen communications have been noticeably less frequent recently. The pirate radio station I found last month has been transmitting pro-Nanotrasen propaganda. I will continue to monitor it.
+ While I pressed the matter on looting the nuclear warheads, they advised me that it was "above my paygrade". However, I slipped a bug I had lying around the satellite into the pockets of one of my
+ superiors into the coat pocket. They were saying it was useless without some sort of authentication device.
+
+ It's not like anyone but me reads these, why else should I talk about my base upkeep? Today: the "lobby".
+"}
+
+// "Anderson" starts writing here
/obj/item/paper/fluff/ruins/listeningstation/reports/july
name = "july report"
+ info = {"Hey, old guy got a transfer, and I was next in line. I'll show them how we do it over at the Gorlex Marauders! Let's monitor some stuff. This will be fun.
+ It seems "old guy" did some upkeep around the base, and I will admit: it's nice. The lobby is shoddy for some reason. Not sure why that is.
+ I read some of the older reports, and it seems like interesting stuff. No idea where June is. Ah well, maybe he got out in May?
+ Odd sleeper, the frost covered it up. They were telling me about this on the way here, that it's meant to be a replacement to ensure "seamless" operation of this base. Okay?
+ I have enough supplies to last me until November, and then I get picked up. Let's get this money!
+ "}
+// Shift in tone here, i may have gone too edgelord
/obj/item/paper/fluff/ruins/listeningstation/reports/august
name = "august report"
+ info = {"holy shit i am so bored. does this even matter? i'm stranded here. they said they'd come for me after a few months and i have plenty of stores. i tried listening to station stuff... but-
+ ugh. i caught some chatter about some sort of disk, but i think it was cross-ference with ourselves? some gas station jingle came on, some feedback from... not even sure what that is
+ the "old guy" did a good job around this base. it's not finished in the lobby, but everything else is nice.
+ they expressly told me to never leave this post, but i think "old guy" didn't listen for putting up those signs. i did a lap around with a spacewalk, that was fun.
+ i'll stay tuned.
+ "}
/obj/item/paper/fluff/ruins/listeningstation/reports/september
name = "september report"
+ info = {"i'm... not doing good. i'm doing so bad. the sleeper is still there. my friend died in a sleeper malfunction. it overheated.
+ i don't want to unplug it, i don't want to wake them up. i don't go in the bedroom anymore, i have a small cot in the lobby area. i'm lonely.
+ blowing myself up is out of the question. it'll kill them too. i've killed many people in my life, but i think ending another that way
+ will probably send me to hell. if hell is anything like this, i'd rather try and salvage as much as i can before i pass. death awaits us all.
+
+ i'm not even going to send this report off i just need to write. i've written a lot, but i burn it all. one a month stays.
+ walks are the only thing that help. i breathe and breathe in those until the oxygen runs out. it makes me feel alive, when every part of me is dead.
+ "}
+
/obj/item/paper/fluff/ruins/listeningstation/reports/october
name = "october report"
+ info = {"i am so fucking stupid. i ran out of tank oxygen, so i tried to jerry rig one of the auxiliary pressure tanks abroad the station to feed an eva tank.
+ as i have just learned, that was very stupid. the oxygen was below zero degrees, and it sprayed right into my open eyes. i can make out blobs of shapes and colors, but not much more.
+ i still know how to touch-type on this machine, it's second-nature to me. with the amount of painkillers i've used, my liver is also failing. i've had mood swings from the painkillers
+ so i've destroyed a lot of this base with my rage. it's unsustainable. there is not enough money in the world to keep me here any longer.
+ i've kept all of these reports on my person, the amount of times i've re-read them trying to think of "better" times gave it certain crinkles...
+ i know june from september from august just based on the wrinkles alone. i can't read them, but i know them more than i know myself these days.
+
+ my only concern is the person in the sleeper. i'm not going to bother them with anything. if i woke them up, maybe i wouldn't be blind and i would be happy. i couldn't see that.
+ i fear they'll go insane reading these, so i'll make something up for november. i'll lie, effortfully lie. scatter the others, and this one. if you happen upon this note, i'm sorry. i ruined it for you.
+
+ as soon as i find where i dropped my keycard, i'm going to use one last emergency oxygen tank, and float. if i am gunned for being a deserter, so be it. if i am shot by terragov or nanotrasen, so be it.
+ even if the pickup should come in a matter of weeks, i literally can not live with myself. a future in where i live in is completely unfeasible.
+ i will die among the stars.
+ "}
+// Shift back in tone, the "lie".
/obj/item/paper/fluff/ruins/listeningstation/reports/november
name = "november report"
+ info = {"Hey, hey, what's up loser! It's me, your predecessor! I had a radical stint here, and you will too! There's not much supplies left, but I'm certain you'll manage by the time the re-supply ship comes back!
+ I've made so much fucking dough just sitting on this ship and writing up reports, the content doesn't even matter! That's why I chose to use this one to shit on you! Look at your goofy face!
+ Got a bit of frostbite on your widdle-diddle face? Ha, ha ha, ha ha ha. Anyways, you've probably only got a month left, while I toughed it all out! I even left this place in a shitty condition
+ because I hated you so much! Fuck you! I'm off spending all of my dough, while you have to pick up the slack of my job! Anyways, don't get too bored now!
+
+ - Your "Friend" - Anderson
+
+ P.S. GORLEX ROOLZ - "SAIBASAN" DROOLZ.
+ "}
-/obj/item/paper/fluff/ruins/listeningstation/reports/june
- name = "june report"
- info = "Nanotrasen communications have been noticeably less frequent recently. The pirate radio station I found last month has been transmitting pro-Nanotrasen propaganda. I will continue to monitor it."
-
-/obj/item/paper/fluff/ruins/listeningstation/reports/may
- name = "may report"
- info = "Nothing of real interest to report this month. I have intercepted faint transmissions from what appears to be some sort of pirate radio station. They do not appear to be relevant to my assignment."
-
-/obj/item/paper/fluff/ruins/listeningstation/reports/april
- name = "april report"
- info = "A good start to the operation: intercepted Nanotrasen military communications. A convoy is scheduled to transfer nuclear warheads to a new military base. This is as good a chance as any to get our hands on some heavy weaponry, I suggest we take it."
+//Miscellaneous Papers
/obj/item/paper/fluff/ruins/listeningstation/receipt
name = "receipt"
info = "1 x Stechkin pistol - 600 cr 1 x silencer - 200 cr shipping charge - 4360 cr total - 5160 cr"
-/obj/item/paper/fluff/ruins/listeningstation/odd_report
- name = "odd report"
- info = "I wonder how much longer they will accept my empty reports. They will cancel the case soon without results. When the pickup comes, I will tell them I have lost faith in our cause, and beg them to consider a diplomatic solution. How many nuclear teams have been dispatched with those nukes? I must try and prevent more from ever being sent. If they will not listen to reason, I will detonate the warehouse myself. Maybe some day in the immediate future, space will be peaceful, though I don't intend to live to see it. And that is why I write this down- it is my sacrifice that stabilized your worlds, traveller. Spare a thought for me, and please attempt to prevent nuclear proliferation, should it ever rear its ugly head again. - Donk Co. Operative #451"
-
/obj/item/paper/fluff/ruins/listeningstation/briefing
name = "mission briefing"
- info = "Mission Details: You have been assigned to a newly constructed listening post constructed within an asteroid in Nanotrasen space to monitor their plasma mining operations. Accurate intel is crucial to the success of our operatives onboard, do not fail us."
+ info = {"Mission Details:
+
+ You have been assigned to a newly constructed listening post constructed within an asteroid in Nanotrasen space to monitor their plasma mining operations.
+ Accurate intel is crucial to the success of our operatives onboard, do not fail us.
+
+ You may view intelligence reports from your predecessors in the filing cabinet in your office.
+ "}
diff --git a/code/modules/mapping/access_helpers.dm b/code/modules/mapping/access_helpers.dm
index a80f3fd43f2ee..b1fd3571e51bd 100644
--- a/code/modules/mapping/access_helpers.dm
+++ b/code/modules/mapping/access_helpers.dm
@@ -1,5 +1,5 @@
/obj/effect/mapping_helpers/airlock/access
- layer = DOOR_HELPER_LAYER
+ layer = DOOR_ACCESS_HELPER_LAYER
icon_state = "access_helper"
// These are mutually exclusive; can't have req_any and req_all
@@ -8,21 +8,14 @@
log_mapping("[src] at [AREACOORD(src)] tried to set req_one_access, but req_access was already set!")
else
var/list/access_list = get_access()
- // Overwrite if there is no access set, otherwise add onto existing access
- if(airlock.req_one_access == null)
- airlock.req_one_access = access_list
- else
- airlock.req_one_access += access_list
+ airlock.req_one_access += access_list
/obj/effect/mapping_helpers/airlock/access/all/payload(obj/machinery/door/airlock/airlock)
if(airlock.req_one_access != null)
log_mapping("[src] at [AREACOORD(src)] tried to set req_one_access, but req_access was already set!")
else
var/list/access_list = get_access()
- if(airlock.req_access == null)
- airlock.req_access = access_list
- else
- airlock.req_access_txt += access_list
+ airlock.req_access += access_list
/obj/effect/mapping_helpers/airlock/access/proc/get_access()
var/list/access = list()
@@ -53,6 +46,12 @@
access_list += ACCESS_EVA
return access_list
+/obj/effect/mapping_helpers/airlock/access/any/command/minisat/get_access()
+ var/list/access_list = ..()
+ access_list += ACCESS_MINISAT
+ return access_list
+
+
/obj/effect/mapping_helpers/airlock/access/any/command/gateway/get_access()
var/list/access_list = ..()
access_list += ACCESS_GATEWAY
@@ -347,9 +346,9 @@
access_list += ACCESS_CARGO
return access_list
-/obj/effect/mapping_helpers/airlock/access/any/supply/mail_sorting/get_access()
+/obj/effect/mapping_helpers/airlock/access/any/supply/shipping/get_access()
var/list/access_list = ..()
- access_list += ACCESS_MAIL_SORTING
+ access_list += ACCESS_SHIPPING
return access_list
/obj/effect/mapping_helpers/airlock/access/any/supply/mining/get_access()
@@ -529,6 +528,11 @@
access_list += ACCESS_EVA
return access_list
+/obj/effect/mapping_helpers/airlock/access/all/command/minisat/get_access()
+ var/list/access_list = ..()
+ access_list += ACCESS_MINISAT
+ return access_list
+
/obj/effect/mapping_helpers/airlock/access/all/command/gateway/get_access()
var/list/access_list = ..()
access_list += ACCESS_GATEWAY
@@ -793,9 +797,9 @@
access_list += ACCESS_CARGO
return access_list
-/obj/effect/mapping_helpers/airlock/access/all/supply/mail_sorting/get_access()
+/obj/effect/mapping_helpers/airlock/access/all/supply/shipping/get_access()
var/list/access_list = ..()
- access_list += ACCESS_MAIL_SORTING
+ access_list += ACCESS_SHIPPING
return access_list
/obj/effect/mapping_helpers/airlock/access/all/supply/mining/get_access()
diff --git a/code/modules/mapping/mapping_helpers.dm b/code/modules/mapping/mapping_helpers.dm
index e2053966edc78..df38a2b258e65 100644
--- a/code/modules/mapping/mapping_helpers.dm
+++ b/code/modules/mapping/mapping_helpers.dm
@@ -189,7 +189,6 @@
else
airlock.locked = TRUE
-
/obj/effect/mapping_helpers/airlock/unres
name = "airlock unrestricted side helper"
icon_state = "airlock_unres_helper"
@@ -700,3 +699,35 @@ INITIALIZE_IMMEDIATE(/obj/effect/mapping_helpers/no_lava)
json_cache[json_url] = json_data
query_in_progress = FALSE
return json_data
+
+/obj/effect/mapping_helpers/broken_floor
+ name = "broken floor"
+ icon = 'icons/turf/damaged.dmi'
+ icon_state = "damaged1"
+ late = TRUE
+ layer = ABOVE_NORMAL_TURF_LAYER
+
+/obj/effect/mapping_helpers/broken_floor/Initialize(mapload)
+ .=..()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/effect/mapping_helpers/broken_floor/LateInitialize()
+ var/turf/open/floor/floor = get_turf(src)
+ floor.break_tile()
+ qdel(src)
+
+/obj/effect/mapping_helpers/burnt_floor
+ name = "burnt floor"
+ icon = 'icons/turf/damaged.dmi'
+ icon_state = "floorscorched1"
+ late = TRUE
+ layer = ABOVE_NORMAL_TURF_LAYER
+
+/obj/effect/mapping_helpers/burnt_floor/Initialize(mapload)
+ .=..()
+ return INITIALIZE_HINT_LATELOAD
+
+/obj/effect/mapping_helpers/burnt_floor/LateInitialize()
+ var/turf/open/floor/floor = get_turf(src)
+ floor.burn_tile()
+ qdel(src)
diff --git a/code/modules/mapping/space_management/traits.dm b/code/modules/mapping/space_management/traits.dm
index b53e2ee332a9b..4911d5316f185 100644
--- a/code/modules/mapping/space_management/traits.dm
+++ b/code/modules/mapping/space_management/traits.dm
@@ -57,18 +57,19 @@
/// Attempt to get the turf below the provided one according to Z traits
/datum/controller/subsystem/mapping/proc/get_turf_below(turf/T)
- if (!T)
+ if (!T || !initialized)
return
- var/offset = level_trait(T.z, ZTRAIT_DOWN)
+ var/offset = multiz_levels[T.z]["[DOWN]"]
if (!offset)
return
- return locate(T.x, T.y, T.z + offset)
+ return locate(T.x, T.y, T.z - offset)
/// Attempt to get the turf above the provided one according to Z traits
/datum/controller/subsystem/mapping/proc/get_turf_above(turf/T)
- if (!T)
+ if (!T || !initialized)
return
- var/offset = level_trait(T.z, ZTRAIT_UP)
+
+ var/offset = multiz_levels[T.z]["[UP]"]
if (!offset)
return
return locate(T.x, T.y, T.z + offset)
diff --git a/code/modules/mapping/space_management/zlevel_manager.dm b/code/modules/mapping/space_management/zlevel_manager.dm
index 2ad150ebc4be6..1e5d2aafc88f0 100644
--- a/code/modules/mapping/space_management/zlevel_manager.dm
+++ b/code/modules/mapping/space_management/zlevel_manager.dm
@@ -26,6 +26,8 @@
// TODO: sleep here if the Z level needs to be cleared
var/datum/space_level/S = new z_type(new_z, name, traits)
z_list += S
+ generate_linkages_for_z_level(new_z)
+ calculate_z_level_gravity(new_z)
adding_new_zlevel = FALSE
SEND_GLOBAL_SIGNAL(COMSIG_GLOB_NEW_Z, S)
return S
diff --git a/code/modules/meteors/meteors.dm b/code/modules/meteors/meteors.dm
index 2e843f2303b00..ef7bf322613c0 100644
--- a/code/modules/meteors/meteors.dm
+++ b/code/modules/meteors/meteors.dm
@@ -221,7 +221,7 @@ GLOBAL_LIST_INIT(meteorsC, list(/obj/effect/meteor/dust=1)) //for space dust eve
continue
var/dist = get_dist(M.loc, src.loc)
shake_camera(M, dist > 20 ? 2 : 4, dist > 20 ? 1 : 3)
- M.playsound_local(src.loc, null, 50, 1, random_frequency, 10, S = meteor_sound)
+ M.playsound_local(src.loc, null, 50, 1, random_frequency, 10, sound_to_use = meteor_sound)
///////////////////////
//Meteor types
diff --git a/code/modules/mining/equipment/explorer_gear.dm b/code/modules/mining/equipment/explorer_gear.dm
index 671f46c85ddec..801c3cf7b3c0f 100644
--- a/code/modules/mining/equipment/explorer_gear.dm
+++ b/code/modules/mining/equipment/explorer_gear.dm
@@ -76,7 +76,7 @@
/obj/item/flashlight,
/obj/item/knife/combat/bone,
/obj/item/knife/combat/survival,
- /obj/item/organ/regenerative_core/legion,
+ /obj/item/organ/internal/regenerative_core/legion,
/obj/item/pickaxe,
/obj/item/spear,
/obj/item/tank/internals,
diff --git a/code/modules/mining/equipment/mining_tools.dm b/code/modules/mining/equipment/mining_tools.dm
index f05077134ba91..34801bcff68bd 100644
--- a/code/modules/mining/equipment/mining_tools.dm
+++ b/code/modules/mining/equipment/mining_tools.dm
@@ -7,6 +7,7 @@
slot_flags = ITEM_SLOT_BELT | ITEM_SLOT_BACK
force = 15
throwforce = 10
+ demolition_mod = 1.15
lefthand_file = 'icons/mob/inhands/equipment/mining_lefthand.dmi'
righthand_file = 'icons/mob/inhands/equipment/mining_righthand.dmi'
w_class = WEIGHT_CLASS_BULKY
diff --git a/code/modules/mining/equipment/regenerative_core.dm b/code/modules/mining/equipment/regenerative_core.dm
index b83ea119c4b2e..11688cb2de328 100644
--- a/code/modules/mining/equipment/regenerative_core.dm
+++ b/code/modules/mining/equipment/regenerative_core.dm
@@ -6,21 +6,21 @@
desc = "Inject certain types of monster organs with this stabilizer to preserve their healing powers indefinitely."
w_class = WEIGHT_CLASS_TINY
-/obj/item/hivelordstabilizer/afterattack(obj/item/organ/M, mob/user, proximity)
+/obj/item/hivelordstabilizer/afterattack(obj/item/organ/target_organ, mob/user, proximity)
. = ..()
if(!proximity)
return
- var/obj/item/organ/regenerative_core/C = M
- if(!istype(C, /obj/item/organ/regenerative_core))
+ var/obj/item/organ/internal/regenerative_core/target_core = target_organ
+ if(!istype(target_core, /obj/item/organ/internal/regenerative_core))
to_chat(user, span_warning("The stabilizer only works on certain types of monster organs, generally regenerative in nature."))
return
- C.preserved()
- to_chat(user, span_notice("You inject the [M] with the stabilizer. It will no longer go inert."))
+ target_core.preserved()
+ to_chat(user, span_notice("You inject the [target_organ] with the stabilizer. It will no longer go inert."))
qdel(src)
/************************Hivelord core*******************/
-/obj/item/organ/regenerative_core
+/obj/item/organ/internal/regenerative_core
name = "regenerative core"
desc = "All that remains of a hivelord. It can be used to help keep your body going, but it will rapidly decay into uselessness."
icon_state = "roro core 2"
@@ -33,15 +33,15 @@
var/inert = 0
var/preserved = 0
-/obj/item/organ/regenerative_core/Initialize(mapload)
+/obj/item/organ/internal/regenerative_core/Initialize(mapload)
. = ..()
addtimer(CALLBACK(src, .proc/inert_check), 2400)
-/obj/item/organ/regenerative_core/proc/inert_check()
+/obj/item/organ/internal/regenerative_core/proc/inert_check()
if(!preserved)
go_inert()
-/obj/item/organ/regenerative_core/proc/preserved(implanted = 0)
+/obj/item/organ/internal/regenerative_core/proc/preserved(implanted = 0)
inert = FALSE
preserved = TRUE
update_appearance()
@@ -51,77 +51,77 @@
else
SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "stabilizer"))
-/obj/item/organ/regenerative_core/proc/go_inert()
+/obj/item/organ/internal/regenerative_core/proc/go_inert()
inert = TRUE
name = "decayed regenerative core"
desc = "All that remains of a hivelord. It has decayed, and is completely useless."
SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "inert"))
update_appearance()
-/obj/item/organ/regenerative_core/ui_action_click()
+/obj/item/organ/internal/regenerative_core/ui_action_click()
if(inert)
to_chat(owner, span_notice("[src] breaks down as it tries to activate."))
else
owner.revive(full_heal = TRUE, admin_revive = FALSE)
qdel(src)
-/obj/item/organ/regenerative_core/on_life(delta_time, times_fired)
+/obj/item/organ/internal/regenerative_core/on_life(delta_time, times_fired)
..()
if(owner.health <= owner.crit_threshold)
ui_action_click()
///Handles applying the core, logging and status/mood events.
-/obj/item/organ/regenerative_core/proc/applyto(atom/target, mob/user)
+/obj/item/organ/internal/regenerative_core/proc/applyto(atom/target, mob/user)
if(ishuman(target))
- var/mob/living/carbon/human/H = target
+ var/mob/living/carbon/human/target_human = target
if(inert)
to_chat(user, span_notice("[src] has decayed and can no longer be used to heal."))
return
else
- if(H.stat == DEAD)
+ if(target_human.stat == DEAD)
to_chat(user, span_notice("[src] is useless on the dead."))
return
- if(H != user)
- H.visible_message(span_notice("[user] forces [H] to apply [src]... Black tendrils entangle and reinforce [H.p_them()]!"))
+ if(target_human != user)
+ target_human.visible_message(span_notice("[user] forces [target_human] to apply [src]... Black tendrils entangle and reinforce [target_human.p_them()]!"))
SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "used", "other"))
else
to_chat(user, span_notice("You start to smear [src] on yourself. Disgusting tendrils hold you together and allow you to keep moving, but for how long?"))
SSblackbox.record_feedback("nested tally", "hivelord_core", 1, list("[type]", "used", "self"))
- H.apply_status_effect(/datum/status_effect/regenerative_core)
- SEND_SIGNAL(H, COMSIG_ADD_MOOD_EVENT, "core", /datum/mood_event/healsbadman) //Now THIS is a miner buff (fixed - nerf)
+ target_human.apply_status_effect(/datum/status_effect/regenerative_core)
+ SEND_SIGNAL(target_human, COMSIG_ADD_MOOD_EVENT, "core", /datum/mood_event/healsbadman) //Now THIS is a miner buff (fixed - nerf)
qdel(src)
-/obj/item/organ/regenerative_core/afterattack(atom/target, mob/user, proximity_flag)
+/obj/item/organ/internal/regenerative_core/afterattack(atom/target, mob/user, proximity_flag)
. = ..()
if(proximity_flag)
applyto(target, user)
-/obj/item/organ/regenerative_core/attack_self(mob/user)
+/obj/item/organ/internal/regenerative_core/attack_self(mob/user)
if(user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
applyto(user, user)
-/obj/item/organ/regenerative_core/Insert(mob/living/carbon/M, special = 0, drop_if_replaced = TRUE)
+/obj/item/organ/internal/regenerative_core/Insert(mob/living/carbon/target_carbon, special = 0, drop_if_replaced = TRUE)
. = ..()
if(!preserved && !inert)
preserved(TRUE)
owner.visible_message(span_notice("[src] stabilizes as it's inserted."))
-/obj/item/organ/regenerative_core/Remove(mob/living/carbon/M, special = 0)
+/obj/item/organ/internal/regenerative_core/Remove(mob/living/carbon/target_carbon, special = 0)
if(!inert && !special)
owner.visible_message(span_notice("[src] rapidly decays as it's removed."))
go_inert()
return ..()
/*************************Legion core********************/
-/obj/item/organ/regenerative_core/legion
+/obj/item/organ/internal/regenerative_core/legion
desc = "A strange rock that crackles with power. It can be used to heal completely, but it will rapidly decay into uselessness."
icon_state = "legion_soul"
-/obj/item/organ/regenerative_core/legion/Initialize(mapload)
+/obj/item/organ/internal/regenerative_core/legion/Initialize(mapload)
. = ..()
update_appearance()
-/obj/item/organ/regenerative_core/update_icon_state()
+/obj/item/organ/internal/regenerative_core/update_icon_state()
if (inert)
icon_state = "legion_soul_inert"
if (preserved)
@@ -130,10 +130,10 @@
icon_state = "legion_soul_unstable"
return ..()
-/obj/item/organ/regenerative_core/legion/go_inert()
+/obj/item/organ/internal/regenerative_core/legion/go_inert()
..()
desc = "[src] has become inert. It has decayed, and is completely useless."
-/obj/item/organ/regenerative_core/legion/preserved(implanted = 0)
+/obj/item/organ/internal/regenerative_core/legion/preserved(implanted = 0)
..()
desc = "[src] has been stabilized. It is preserved, allowing you to use it to heal completely without danger of decay."
diff --git a/code/modules/mining/laborcamp/laborshuttle.dm b/code/modules/mining/laborcamp/laborshuttle.dm
index 000be6251d1dc..8bcb1230bd52c 100644
--- a/code/modules/mining/laborcamp/laborshuttle.dm
+++ b/code/modules/mining/laborcamp/laborshuttle.dm
@@ -26,3 +26,14 @@
to_chat(user, span_warning("Shuttle is already at the outpost!"))
return FALSE
return TRUE
+
+/obj/docking_port/stationary/laborcamp_home
+ name = "SS13: Labor Shuttle Dock"
+ id = "laborcamp_home"
+ roundstart_template = /datum/map_template/shuttle/labour/delta
+ width = 9
+ dwidth = 2
+ height = 5
+
+/obj/docking_port/stationary/laborcamp_home/kilo
+ roundstart_template = /datum/map_template/shuttle/labour/kilo
diff --git a/code/modules/mining/lavaland/megafauna_loot.dm b/code/modules/mining/lavaland/megafauna_loot.dm
index 1c0f040d963d6..7dabba56cd6a9 100644
--- a/code/modules/mining/lavaland/megafauna_loot.dm
+++ b/code/modules/mining/lavaland/megafauna_loot.dm
@@ -730,9 +730,8 @@
consumer.set_species(/datum/species/skeleton)
if(3)
to_chat(user, span_danger("Power courses through you! You can now shift your form at will."))
- if(user.mind)
- var/obj/effect/proc_holder/spell/targeted/shapeshift/dragon/dragon_shapeshift = new
- user.mind.AddSpell(dragon_shapeshift)
+ var/datum/action/cooldown/spell/shapeshift/dragon/dragon_shapeshift = new(user.mind || user)
+ dragon_shapeshift.Grant(user)
if(4)
to_chat(user, span_danger("You feel like you could walk straight through lava now."))
ADD_TRAIT(user, TRAIT_LAVA_IMMUNE, type)
diff --git a/code/modules/mining/lavaland/necropolis_chests.dm b/code/modules/mining/lavaland/necropolis_chests.dm
index c3c0ec33b46f6..628bc4f5dcb67 100644
--- a/code/modules/mining/lavaland/necropolis_chests.dm
+++ b/code/modules/mining/lavaland/necropolis_chests.dm
@@ -66,7 +66,7 @@
if(16)
new /obj/item/immortality_talisman(src)
if(17)
- new /obj/item/book/granter/spell/summonitem(src)
+ new /obj/item/book/granter/action/spell/summonitem(src)
if(18)
new /obj/item/book_of_babel(src)
if(19)
@@ -97,7 +97,7 @@
if(2)
new /obj/item/lava_staff(src)
if(3)
- new /obj/item/book/granter/spell/sacredflame(src)
+ new /obj/item/book/granter/action/spell/sacredflame(src)
if(4)
new /obj/item/dragons_blood(src)
diff --git a/code/modules/mining/lavaland/tendril_loot.dm b/code/modules/mining/lavaland/tendril_loot.dm
index 407c6cf5a9bf8..cbfc8ecb545d1 100644
--- a/code/modules/mining/lavaland/tendril_loot.dm
+++ b/code/modules/mining/lavaland/tendril_loot.dm
@@ -605,7 +605,7 @@
body_parts_covered = CHEST|GROIN|LEGS|FEET|ARMS|HANDS
resistance_flags = FIRE_PROOF
clothing_flags = THICKMATERIAL
- allowed = list(/obj/item/flashlight, /obj/item/tank/internals, /obj/item/pickaxe, /obj/item/spear, /obj/item/organ/regenerative_core/legion, /obj/item/knife, /obj/item/kinetic_crusher, /obj/item/resonator, /obj/item/melee/cleaving_saw)
+ allowed = list(/obj/item/flashlight, /obj/item/tank/internals, /obj/item/pickaxe, /obj/item/spear, /obj/item/organ/internal/regenerative_core/legion, /obj/item/knife, /obj/item/kinetic_crusher, /obj/item/resonator, /obj/item/melee/cleaving_saw)
/obj/item/clothing/suit/hooded/berserker/Initialize(mapload)
. = ..()
@@ -716,83 +716,74 @@
lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF
custom_materials = null
- var/obj/effect/proc_holder/scan/scan
+ var/datum/action/cooldown/scan/scan_ability
/obj/item/clothing/glasses/godeye/Initialize(mapload)
. = ..()
- scan = new(src)
+ scan_ability = new(src)
+
+/obj/item/clothing/glasses/godeye/Destroy()
+ QDEL_NULL(scan_ability)
+ return ..()
/obj/item/clothing/glasses/godeye/equipped(mob/living/user, slot)
. = ..()
if(ishuman(user) && slot == ITEM_SLOT_EYES)
ADD_TRAIT(src, TRAIT_NODROP, EYE_OF_GOD_TRAIT)
pain(user)
- user.AddAbility(scan)
+ scan_ability.Grant(user)
/obj/item/clothing/glasses/godeye/dropped(mob/living/user)
. = ..()
// Behead someone, their "glasses" drop on the floor
// and thus, the god eye should no longer be sticky
REMOVE_TRAIT(src, TRAIT_NODROP, EYE_OF_GOD_TRAIT)
- user.RemoveAbility(scan)
+ scan_ability.Remove(user)
/obj/item/clothing/glasses/godeye/proc/pain(mob/living/victim)
to_chat(victim, span_userdanger("You experience blinding pain, as [src] burrows into your skull."))
victim.emote("scream")
victim.flash_act()
-/obj/effect/proc_holder/scan
+/datum/action/cooldown/scan
name = "Scan"
desc = "Scan an enemy, to get their location and stagger them, increasing their time between attacks."
- action_background_icon_state = "bg_clock"
- action_icon = 'icons/mob/actions/actions_items.dmi'
- action_icon_state = "scan"
+ background_icon_state = "bg_clock"
+ icon_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "scan"
+
+ click_to_activate = TRUE
+ cooldown_time = 45 SECONDS
ranged_mousepointer = 'icons/effects/mouse_pointers/scan_target.dmi'
- var/cooldown_time = 45 SECONDS
- COOLDOWN_DECLARE(scan_cooldown)
-/obj/effect/proc_holder/scan/on_lose(mob/living/user)
- remove_ranged_ability()
+/datum/action/cooldown/scan/IsAvailable()
+ return ..() && isliving(owner)
-/obj/effect/proc_holder/scan/Click(location, control, params)
- . = ..()
- if(!isliving(usr))
- return TRUE
- var/mob/living/user = usr
- fire(user)
+/datum/action/cooldown/scan/Activate(atom/scanned)
+ StartCooldown(15 SECONDS)
-/obj/effect/proc_holder/scan/fire(mob/living/carbon/user)
- if(active)
- remove_ranged_ability(span_notice("Your eye relaxes."))
- else
- add_ranged_ability(user, span_notice("Your eye starts spinning fast. Left-click a creature to scan it!"), TRUE)
+ if(owner.stat != CONSCIOUS)
+ return FALSE
+ if(!isliving(scanned) || scanned == owner)
+ owner.balloon_alert(owner, "invalid scanned!")
+ return FALSE
-/obj/effect/proc_holder/scan/InterceptClickOn(mob/living/caller, params, atom/target)
- . = ..()
- if(.)
- return
- if(ranged_ability_user.stat)
- remove_ranged_ability()
- return
- if(!COOLDOWN_FINISHED(src, scan_cooldown))
- balloon_alert(ranged_ability_user, "not ready!")
- return
- if(!isliving(target) || target == ranged_ability_user)
- balloon_alert(ranged_ability_user, "invalid target!")
- return
- var/mob/living/living_target = target
- living_target.apply_status_effect(/datum/status_effect/stagger)
- var/datum/status_effect/agent_pinpointer/scan_pinpointer = ranged_ability_user.apply_status_effect(/datum/status_effect/agent_pinpointer/scan)
- scan_pinpointer.scan_target = living_target
- living_target.set_timed_status_effect(100 SECONDS, /datum/status_effect/jitter, only_if_higher = TRUE)
- to_chat(living_target, span_warning("You've been staggered!"))
- living_target.add_filter("scan", 2, list("type" = "outline", "color" = COLOR_YELLOW, "size" = 1))
- addtimer(CALLBACK(living_target, /atom/.proc/remove_filter, "scan"), 30 SECONDS)
- ranged_ability_user.playsound_local(get_turf(ranged_ability_user), 'sound/magic/smoke.ogg', 50, TRUE)
- balloon_alert(ranged_ability_user, "[living_target] scanned")
- COOLDOWN_START(src, scan_cooldown, cooldown_time)
- addtimer(CALLBACK(src, /atom/.proc/balloon_alert, ranged_ability_user, "scan recharged"), cooldown_time)
- remove_ranged_ability()
+ var/mob/living/living_owner = owner
+ var/mob/living/living_scanned = scanned
+ living_scanned.apply_status_effect(/datum/status_effect/stagger)
+ var/datum/status_effect/agent_pinpointer/scan_pinpointer = living_owner.apply_status_effect(/datum/status_effect/agent_pinpointer/scan)
+ scan_pinpointer.scan_target = living_scanned
+
+ living_scanned.set_timed_status_effect(100 SECONDS, /datum/status_effect/jitter, only_if_higher = TRUE)
+ to_chat(living_scanned, span_warning("You've been staggered!"))
+ living_scanned.add_filter("scan", 2, list("type" = "outline", "color" = COLOR_YELLOW, "size" = 1))
+ addtimer(CALLBACK(living_scanned, /atom/.proc/remove_filter, "scan"), 30 SECONDS)
+
+ owner.playsound_local(get_turf(owner), 'sound/magic/smoke.ogg', 50, TRUE)
+ owner.balloon_alert(owner, "[living_scanned] scanned")
+ addtimer(CALLBACK(src, /atom/.proc/balloon_alert, owner, "scan recharged"), cooldown_time)
+
+ StartCooldown()
return TRUE
/datum/status_effect/agent_pinpointer/scan
diff --git a/code/modules/mining/machine_redemption.dm b/code/modules/mining/machine_redemption.dm
index d535904af02ad..4178d4446ab7d 100644
--- a/code/modules/mining/machine_redemption.dm
+++ b/code/modules/mining/machine_redemption.dm
@@ -250,13 +250,13 @@
var/datum/component/material_container/mat_container = materials.mat_container
switch(action)
if("Claim")
- var/obj/item/card/id/I
+ var/obj/item/card/id/user_id_card
if(isliving(usr))
- var/mob/living/L = usr
- I = L.get_idcard(TRUE)
+ var/mob/living/user = usr
+ user_id_card = user.get_idcard(TRUE)
if(points)
- if(I)
- I.mining_points += points
+ if(user_id_card)
+ user_id_card.mining_points += points
points = 0
else
to_chat(usr, span_warning("No valid ID detected."))
@@ -324,11 +324,11 @@
return
var/alloy_id = params["id"]
var/datum/design/alloy = stored_research.isDesignResearchedID(alloy_id)
- var/obj/item/card/id/I
+ var/obj/item/card/id/user_id_card
if(isliving(usr))
- var/mob/living/L = usr
- I = L.get_idcard(TRUE)
- if((check_access(I) || allowed(usr)) && alloy)
+ var/mob/living/user = usr
+ user_id_card = user.get_idcard(TRUE)
+ if((check_access(user_id_card) || allowed(usr)) && alloy)
var/smelt_amount = can_smelt_alloy(alloy)
var/desired = 0
if (params["sheets"])
@@ -338,6 +338,8 @@
if(!desired || QDELETED(usr) || QDELETED(src) || !usr.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
return
var/amount = round(min(desired,50,smelt_amount))
+ if(amount < 1) //no negative mats
+ return
mat_container.use_materials(alloy.materials, amount)
materials.silo_log(src, "released", -amount, "sheets", alloy.materials)
var/output
diff --git a/code/modules/mining/machine_silo.dm b/code/modules/mining/machine_silo.dm
index 63864b5e9a229..e4313ea35fc3a 100644
--- a/code/modules/mining/machine_silo.dm
+++ b/code/modules/mining/machine_silo.dm
@@ -9,9 +9,12 @@ GLOBAL_LIST_EMPTY(silo_access_logs)
density = TRUE
circuit = /obj/item/circuitboard/machine/ore_silo
- var/list/holds = list()
- var/list/datum/component/remote_materials/connected = list()
+ /// The machine UI's page of logs showing ore history.
var/log_page = 1
+ /// List of all connected components that are on hold from accessing materials.
+ var/list/holds = list()
+ /// List of all components that are sharing ores with this silo.
+ var/list/datum/component/remote_materials/ore_connected_machines = list()
/obj/machinery/ore_silo/Initialize(mapload)
. = ..()
@@ -36,11 +39,10 @@ GLOBAL_LIST_EMPTY(silo_access_logs)
if (GLOB.ore_silo_default == src)
GLOB.ore_silo_default = null
- for(var/C in connected)
- var/datum/component/remote_materials/mats = C
+ for(var/datum/component/remote_materials/mats as anything in ore_connected_machines)
mats.disconnect_from(src)
- connected = null
+ ore_connected_machines = null
var/datum/component/material_container/materials = GetComponent(/datum/component/material_container)
materials.retrieve_all()
@@ -112,14 +114,13 @@ GLOBAL_LIST_EMPTY(silo_access_logs)
ui += "Nothing!"
ui += "
Connected Machines:
"
- for(var/C in connected)
- var/datum/component/remote_materials/mats = C
+ for(var/datum/component/remote_materials/mats as anything in ore_connected_machines)
var/atom/parent = mats.parent
var/hold_key = "[get_area(parent)]/[mats.category]"
ui += "Remove"
ui += "[holds[hold_key] ? "Allow" : "Hold"]"
ui += " [parent.name] in [get_area_name(parent, TRUE)] "
- if(!connected.len)
+ if(!ore_connected_machines.len)
ui += "Nothing!"
ui += "
Access Logs:
"
@@ -153,10 +154,10 @@ GLOBAL_LIST_EMPTY(silo_access_logs)
usr.set_machine(src)
if(href_list["remove"])
- var/datum/component/remote_materials/mats = locate(href_list["remove"]) in connected
+ var/datum/component/remote_materials/mats = locate(href_list["remove"]) in ore_connected_machines
if (mats)
mats.disconnect_from(src)
- connected -= mats
+ ore_connected_machines -= mats
updateUsrDialog()
return TRUE
else if(href_list["hold1"])
diff --git a/code/modules/mining/mine_items.dm b/code/modules/mining/mine_items.dm
index fbaa883b46106..67921c94e0b2d 100644
--- a/code/modules/mining/mine_items.dm
+++ b/code/modules/mining/mine_items.dm
@@ -122,6 +122,26 @@
shuttleId = "mining_common"
possible_destinations = "commonmining_home;lavaland_common_away;landing_zone_dock;mining_public"
+/obj/docking_port/stationary/mining_home
+ name = "SS13: Mining Dock"
+ id = "mining_home"
+ roundstart_template = /datum/map_template/shuttle/mining/delta
+ width = 7
+ dwidth = 3
+ height = 5
+
+/obj/docking_port/stationary/mining_home/kilo
+ roundstart_template = /datum/map_template/shuttle/mining/kilo
+ height = 10
+
+/obj/docking_port/stationary/mining_home/common
+ name = "SS13: Common Mining Dock"
+ id = "commonmining_home"
+ roundstart_template = /datum/map_template/shuttle/mining_common/meta
+
+/obj/docking_port/stationary/mining_home/common/kilo
+ roundstart_template = /datum/map_template/shuttle/mining_common/kilo
+
/**********************Mining car (Crate like thing, not the rail car)**************************/
/obj/structure/closet/crate/miningcar
diff --git a/code/modules/mob/dead/dead.dm b/code/modules/mob/dead/dead.dm
index 04055c8ecd18a..41bb077153bce 100644
--- a/code/modules/mob/dead/dead.dm
+++ b/code/modules/mob/dead/dead.dm
@@ -55,7 +55,7 @@ INITIALIZE_IMMEDIATE(/mob/dead)
/mob/dead/proc/server_hop()
set category = "OOC"
- set name = "Server Hop!"
+ set name = "Server Hop"
set desc= "Jump to the other server"
if(notransform)
return
diff --git a/code/modules/mob/dead/new_player/login.dm b/code/modules/mob/dead/new_player/login.dm
index 5ec50107f57c2..08969aeef4ba3 100644
--- a/code/modules/mob/dead/new_player/login.dm
+++ b/code/modules/mob/dead/new_player/login.dm
@@ -10,6 +10,13 @@
mind.active = TRUE
mind.set_current(src)
+ // Check if user should be added to interview queue
+ if (!client.holder && CONFIG_GET(flag/panic_bunker) && CONFIG_GET(flag/panic_bunker_interview) && !(client.ckey in GLOB.interviews.approved_ckeys))
+ var/required_living_minutes = CONFIG_GET(number/panic_bunker_living)
+ var/living_minutes = client.get_exp_living(TRUE)
+ if (required_living_minutes >= living_minutes)
+ client.interviewee = TRUE
+
. = ..()
if(!. || !client)
return FALSE
@@ -32,15 +39,15 @@
var/datum/asset/asset_datum = get_asset_datum(/datum/asset/simple/lobby)
asset_datum.send(client)
- // Check if user should be added to interview queue
- if (!client.holder && CONFIG_GET(flag/panic_bunker) && CONFIG_GET(flag/panic_bunker_interview) && !(client.ckey in GLOB.interviews.approved_ckeys))
- var/required_living_minutes = CONFIG_GET(number/panic_bunker_living)
- var/living_minutes = client.get_exp_living(TRUE)
- if (required_living_minutes >= living_minutes)
- client.interviewee = TRUE
- register_for_interview()
- return
+ // The parent call for Login() may do a bunch of stuff, like add verbs.
+ // Delaying the register_for_interview until the very end makes sure it can clean everything up
+ // and set the player's client up for interview.
+ if(client.interviewee)
+ register_for_interview()
+ return
if(SSticker.current_state < GAME_STATE_SETTING_UP)
var/tl = SSticker.GetTimeLeft()
to_chat(src, "Please set up your character and select \"Ready\". The game will start [tl > 0 ? "in about [DisplayTimeText(tl)]" : "soon"].")
+
+
diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm
index 045990a2d8a6d..a05cf24d5b108 100644
--- a/code/modules/mob/dead/new_player/new_player.dm
+++ b/code/modules/mob/dead/new_player/new_player.dm
@@ -439,5 +439,6 @@
if (I)
I.ui_interact(src)
- // Add verb for re-opening the interview panel, and re-init the verbs for the stat panel
+ // Add verb for re-opening the interview panel, fixing chat and re-init the verbs for the stat panel
add_verb(src, /mob/dead/new_player/proc/open_interview)
+ add_verb(client, /client/verb/fix_tgui_panel)
diff --git a/code/modules/mob/living/basic/farm_animals/cows.dm b/code/modules/mob/living/basic/farm_animals/cows.dm
index 2f66d4601cbb9..130a43e4342e4 100644
--- a/code/modules/mob/living/basic/farm_animals/cows.dm
+++ b/code/modules/mob/living/basic/farm_animals/cows.dm
@@ -50,7 +50,6 @@
AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/grown/wheat), tame_chance = 25, bonus_tame_chance = 15, after_tame = CALLBACK(src, .proc/tamed))
/mob/living/basic/cow/proc/tamed(mob/living/tamer)
- can_buckle = TRUE
buckle_lying = 0
AddElement(/datum/element/ridable, /datum/component/riding/creature/cow)
@@ -109,7 +108,7 @@
if(!stat && !user.combat_mode)
to_chat(user, span_nicegreen("[src] whispers you some intense wisdoms and then disappears!"))
user.mind?.adjust_experience(pick(GLOB.skill_types), 500)
- do_smoke(DIAMOND_AREA(1), get_turf(src))
+ do_smoke(1, holder = src, location = get_turf(src))
qdel(src)
return
return ..()
diff --git a/code/modules/mob/living/bloodcrawl.dm b/code/modules/mob/living/bloodcrawl.dm
deleted file mode 100644
index 395d6a73d9612..0000000000000
--- a/code/modules/mob/living/bloodcrawl.dm
+++ /dev/null
@@ -1,160 +0,0 @@
-/mob/living/proc/phaseout(obj/effect/decal/cleanable/B)
- if(iscarbon(src))
- var/mob/living/carbon/C = src
- for(var/obj/item/I in C.held_items)
- //TODO make it toggleable to either forcedrop the items, or deny
- //entry when holding them
- // literally only an option for carbons though
- to_chat(C, span_warning("You may not hold items while blood crawling!"))
- return FALSE
- var/obj/item/bloodcrawl/B1 = new(C)
- var/obj/item/bloodcrawl/B2 = new(C)
- B1.icon_state = "bloodhand_left"
- B2.icon_state = "bloodhand_right"
- C.put_in_hands(B1)
- C.put_in_hands(B2)
- C.regenerate_icons()
-
- notransform = TRUE
- INVOKE_ASYNC(src, .proc/bloodpool_sink, B)
-
- return TRUE
-
-/mob/living/proc/bloodpool_sink(obj/effect/decal/cleanable/B)
- var/turf/mobloc = get_turf(loc)
-
- visible_message(span_warning("[src] sinks into the pool of blood!"))
- playsound(get_turf(src), 'sound/magic/enter_blood.ogg', 50, TRUE, -1)
- // Extinguish, unbuckle, stop being pulled, set our location into the
- // dummy object
- var/obj/effect/dummy/phased_mob/holder = new /obj/effect/dummy/phased_mob(mobloc)
- extinguish_mob()
-
- // Keep a reference to whatever we're pulling, because forceMove()
- // makes us stop pulling
- var/pullee = pulling
-
- holder = holder
- forceMove(holder)
-
- // if we're not pulling anyone, or we can't eat anyone
- if(!pullee || !HAS_TRAIT(src, TRAIT_BLOODCRAWL_EAT))
- notransform = FALSE
- return
-
- // if the thing we're pulling isn't alive
- if(!isliving(pullee))
- notransform = FALSE
- return
-
- var/mob/living/victim = pullee
- var/kidnapped = FALSE
-
- if(victim.stat == CONSCIOUS)
- visible_message(span_warning("[victim] kicks free of the blood pool just before entering it!"), null, span_notice("You hear splashing and struggling."))
- else if(victim.reagents?.has_reagent(/datum/reagent/consumable/ethanol/demonsblood, needs_metabolizing = TRUE))
- visible_message(span_warning("Something prevents [victim] from entering the pool!"), span_warning("A strange force is blocking [victim] from entering!"), span_notice("You hear a splash and a thud."))
- else
- victim.forceMove(src)
- victim.emote("scream")
- visible_message(span_warning("[src] drags [victim] into the pool of blood!"), null, span_notice("You hear a splash."))
- kidnapped = TRUE
-
- if(kidnapped)
- var/success = bloodcrawl_consume(victim)
- if(!success)
- to_chat(src, span_danger("You happily devour... nothing? Your meal vanished at some point!"))
-
- notransform = FALSE
- return TRUE
-
-/mob/living/proc/bloodcrawl_consume(mob/living/victim)
- to_chat(src, span_danger("You begin to feast on [victim]... You can not move while you are doing this."))
-
- var/sound
- if(istype(src, /mob/living/simple_animal/hostile/imp/slaughter))
- var/mob/living/simple_animal/hostile/imp/slaughter/SD = src
- sound = SD.feast_sound
- else
- sound = 'sound/magic/demon_consume.ogg'
-
- for(var/i in 1 to 3)
- playsound(get_turf(src),sound, 50, TRUE)
- sleep(30)
-
- if(!victim)
- return FALSE
-
- if(victim.reagents?.has_reagent(/datum/reagent/consumable/ethanol/devilskiss, needs_metabolizing = TRUE))
- to_chat(src, span_warning("AAH! THEIR FLESH! IT BURNS!"))
- adjustBruteLoss(25) //I can't use adjustHealth() here because bloodcrawl affects /mob/living and adjustHealth() only affects simple mobs
- var/found_bloodpool = FALSE
- for(var/obj/effect/decal/cleanable/target in range(1,get_turf(victim)))
- if(target.can_bloodcrawl_in())
- victim.forceMove(get_turf(target))
- victim.visible_message(span_warning("[target] violently expels [victim]!"))
- victim.exit_blood_effect(target)
- found_bloodpool = TRUE
- break
-
- if(!found_bloodpool)
- // Fuck it, just eject them, thanks to some split second cleaning
- victim.forceMove(get_turf(victim))
- victim.visible_message(span_warning("[victim] appears from nowhere, covered in blood!"))
- victim.exit_blood_effect()
- return TRUE
-
- to_chat(src, span_danger("You devour [victim]. Your health is fully restored."))
- revive(full_heal = TRUE, admin_revive = FALSE)
-
- // No defib possible after laughter
- victim.adjustBruteLoss(1000)
- victim.death()
- bloodcrawl_swallow(victim)
- return TRUE
-
-/mob/living/proc/bloodcrawl_swallow(mob/living/victim)
- qdel(victim)
-
-/obj/item/bloodcrawl
- name = "blood crawl"
- desc = "You are unable to hold anything while in this form."
- icon = 'icons/effects/blood.dmi'
- item_flags = ABSTRACT | DROPDEL
-
-/obj/item/bloodcrawl/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT)
-
-/mob/living/proc/exit_blood_effect(obj/effect/decal/cleanable/B)
- playsound(get_turf(src), 'sound/magic/exit_blood.ogg', 50, TRUE, -1)
- //Makes the mob have the color of the blood pool it came out of
- var/newcolor = rgb(149, 10, 10)
- if(istype(B, /obj/effect/decal/cleanable/xenoblood))
- newcolor = rgb(43, 186, 0)
- add_atom_colour(newcolor, TEMPORARY_COLOUR_PRIORITY)
- // but only for a few seconds
- addtimer(CALLBACK(src, /atom/.proc/remove_atom_colour, TEMPORARY_COLOUR_PRIORITY, newcolor), 6 SECONDS)
-
-/mob/living/proc/phasein(atom/target, forced = FALSE)
- if(!forced)
- if(notransform)
- to_chat(src, span_warning("Finish eating first!"))
- return FALSE
- target.visible_message(span_warning("[target] starts to bubble..."))
- if(!do_after(src, 20, target = target))
- return FALSE
- forceMove(get_turf(target))
- client.eye = src
- SEND_SIGNAL(src, COMSIG_LIVING_AFTERPHASEIN, target)
- visible_message(span_boldwarning("[src] rises out of the pool of blood!"))
- exit_blood_effect(target)
- return TRUE
-
-/mob/living/carbon/phasein(atom/target, forced = FALSE)
- . = ..()
- if(!.)
- return
- for(var/obj/item/bloodcrawl/blood_hand in held_items)
- blood_hand.flags_1 = null
- qdel(blood_hand)
diff --git a/code/modules/mob/living/brain/brain.dm b/code/modules/mob/living/brain/brain.dm
index aa13a0f59a372..9be5ba567433a 100644
--- a/code/modules/mob/living/brain/brain.dm
+++ b/code/modules/mob/living/brain/brain.dm
@@ -86,8 +86,6 @@
var/obj/vehicle/sealed/mecha/M = container.mecha
if(M.mouse_pointer)
client.mouse_pointer_icon = M.mouse_pointer
- if (client && ranged_ability?.ranged_mousepointer)
- client.mouse_pointer_icon = ranged_ability.ranged_mousepointer
/mob/living/brain/proc/get_traumas()
. = list()
diff --git a/code/modules/mob/living/carbon/alien/alien.dm b/code/modules/mob/living/carbon/alien/alien.dm
index 22d263e7c63b0..3a78e70ab362e 100644
--- a/code/modules/mob/living/carbon/alien/alien.dm
+++ b/code/modules/mob/living/carbon/alien/alien.dm
@@ -117,8 +117,10 @@ Des: Removes all infected images from the alien.
return TRUE
/mob/living/carbon/alien/proc/alien_evolve(mob/living/carbon/alien/new_xeno)
- to_chat(src, span_noticealien("You begin to evolve!"))
- visible_message(span_alertalien("[src] begins to twist and contort!"))
+ visible_message(
+ span_alertalien("[src] begins to twist and contort!"),
+ span_noticealien("You begin to evolve!"),
+ )
new_xeno.setDir(dir)
if(numba && unique_name)
new_xeno.numba = numba
diff --git a/code/modules/mob/living/carbon/alien/humanoid/alien_powers.dm b/code/modules/mob/living/carbon/alien/humanoid/alien_powers.dm
index 268f0664229fe..1ff88c419d048 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/alien_powers.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/alien_powers.dm
@@ -6,341 +6,380 @@ These are general powers. Specific powers are stored under the appropriate alien
Doesn't work on other aliens/AI.*/
-/obj/effect/proc_holder/alien
+/datum/action/cooldown/alien
name = "Alien Power"
panel = "Alien"
+ background_icon_state = "bg_alien"
+ icon_icon = 'icons/mob/actions/actions_xeno.dmi'
+ button_icon_state = "spell_default"
+ check_flags = AB_CHECK_CONSCIOUS
+ /// How much plasma this action uses.
var/plasma_cost = 0
- var/check_turf = FALSE
- has_action = TRUE
- base_action = /datum/action/spell_action/alien
- action_icon = 'icons/mob/actions/actions_xeno.dmi'
- action_icon_state = "spell_default"
- action_background_icon_state = "bg_alien"
-
-/obj/effect/proc_holder/alien/Click()
- if(!iscarbon(usr))
- return 1
- var/mob/living/carbon/user = usr
- if(cost_check(check_turf,user))
- if(fire(user) && user) // Second check to prevent runtimes when evolving
- user.adjustPlasma(-plasma_cost)
- return 1
-
-/obj/effect/proc_holder/alien/on_gain(mob/living/carbon/user)
- return
-
-/obj/effect/proc_holder/alien/on_lose(mob/living/carbon/user)
- return
-
-/obj/effect/proc_holder/alien/fire(mob/living/carbon/user)
- return 1
-
-/obj/effect/proc_holder/alien/get_panel_text()
+
+/datum/action/cooldown/alien/IsAvailable()
. = ..()
- if(plasma_cost > 0)
- return "[plasma_cost]"
+ if(!.)
+ return FALSE
+ if(!iscarbon(owner))
+ return FALSE
+ var/mob/living/carbon/carbon_owner = owner
+ if(carbon_owner.getPlasma() < plasma_cost)
+ return FALSE
-/obj/effect/proc_holder/alien/proc/cost_check(check_turf = FALSE, mob/living/carbon/user, silent = FALSE)
- if(user.stat)
- if(!silent)
- to_chat(user, span_noticealien("You must be conscious to do this."))
+ return TRUE
+
+/datum/action/cooldown/alien/PreActivate(atom/target)
+ // Parent calls Activate(), so if parent returns TRUE,
+ // it means the activation happened successfuly by this point
+ . = ..()
+ if(!.)
return FALSE
- if(user.getPlasma() < plasma_cost)
- if(!silent)
- to_chat(user, span_noticealien("Not enough plasma stored."))
+ // Xeno actions like "evolve" may result in our action (or our alien) being deleted
+ // In that case, we can just exit now as a "success"
+ if(QDELETED(src) || QDELETED(owner))
+ return TRUE
+
+ var/mob/living/carbon/carbon_owner = owner
+ carbon_owner.adjustPlasma(-plasma_cost)
+ // It'd be really annoying if click-to-fire actions stayed active,
+ // even if our plasma amount went under the required amount.
+ if(click_to_activate && carbon_owner.getPlasma() < plasma_cost)
+ unset_click_ability(owner, refund_cooldown = FALSE)
+
+ return TRUE
+
+/datum/action/cooldown/alien/set_statpanel_format()
+ . = ..()
+ if(!islist(.))
+ return
+
+ .[PANEL_DISPLAY_STATUS] = "PLASMA - [plasma_cost]"
+
+/datum/action/cooldown/alien/make_structure
+ /// The type of structure the action makes on use
+ var/obj/structure/made_structure_type
+
+/datum/action/cooldown/alien/make_structure/IsAvailable()
+ . = ..()
+ if(!.)
return FALSE
- if(check_turf && (!isturf(user.loc) || isspaceturf(user.loc)))
- if(!silent)
- to_chat(user, span_noticealien("Bad place for a garden!"))
+ if(!isturf(owner.loc) || isspaceturf(owner.loc))
return FALSE
+
return TRUE
-/obj/effect/proc_holder/alien/proc/check_vent_block(mob/living/user)
- var/obj/machinery/atmospherics/components/unary/atmos_thing = locate() in user.loc
+/datum/action/cooldown/alien/make_structure/PreActivate(atom/target)
+ if(!check_for_duplicate())
+ return FALSE
+
+ if(!check_for_vents())
+ return FALSE
+
+ return ..()
+
+/datum/action/cooldown/alien/make_structure/Activate(atom/target)
+ new made_structure_type(owner.loc)
+ return TRUE
+
+/// Checks if there's a duplicate structure in the owner's turf
+/datum/action/cooldown/alien/make_structure/proc/check_for_duplicate()
+ var/obj/structure/existing_thing = locate(made_structure_type) in owner.loc
+ if(existing_thing)
+ to_chat(owner, span_warning("There is already \a [existing_thing] here!"))
+ return FALSE
+
+ return TRUE
+
+/// Checks if there's an atmos machine (vent) in the owner's turf
+/datum/action/cooldown/alien/make_structure/proc/check_for_vents()
+ var/obj/machinery/atmospherics/components/unary/atmos_thing = locate() in owner.loc
if(atmos_thing)
- var/rusure = tgui_alert(user, "Laying eggs and shaping resin here would block access to [atmos_thing]. Do you want to continue?", "Blocking Atmospheric Component", list("Yes", "No"))
- if(rusure != "Yes")
+ var/are_you_sure = tgui_alert(owner, "Laying eggs and shaping resin here would block access to [atmos_thing]. Do you want to continue?", "Blocking Atmospheric Component", list("Yes", "No"))
+ if(are_you_sure != "Yes")
return FALSE
+ if(QDELETED(src) || QDELETED(owner) || !check_for_duplicate())
+ return FALSE
+
return TRUE
-/obj/effect/proc_holder/alien/plant
+/datum/action/cooldown/alien/make_structure/plant_weeds
name = "Plant Weeds"
desc = "Plants some alien weeds."
+ button_icon_state = "alien_plant"
plasma_cost = 50
- check_turf = TRUE
- action_icon_state = "alien_plant"
+ made_structure_type = /obj/structure/alien/weeds/node
-/obj/effect/proc_holder/alien/plant/fire(mob/living/carbon/user)
- if(locate(/obj/structure/alien/weeds/node) in get_turf(user))
- to_chat(user, span_warning("There's already a weed node here!"))
- return FALSE
- user.visible_message(span_alertalien("[user] plants some alien weeds!"))
- new/obj/structure/alien/weeds/node(user.loc)
- return TRUE
+/datum/action/cooldown/alien/make_structure/plant_weeds/Activate(atom/target)
+ owner.visible_message(span_alertalien("[owner] plants some alien weeds!"))
+ return ..()
-/obj/effect/proc_holder/alien/whisper
+/datum/action/cooldown/alien/whisper
name = "Whisper"
desc = "Whisper to someone."
+ button_icon_state = "alien_whisper"
plasma_cost = 10
- action_icon_state = "alien_whisper"
-/obj/effect/proc_holder/alien/whisper/fire(mob/living/carbon/user)
+/datum/action/cooldown/alien/whisper/Activate(atom/target)
var/list/possible_recipients = list()
- for(var/mob/living/recipient in oview(user))
- possible_recipients.Add(recipient)
+ for(var/mob/living/recipient in oview(owner))
+ possible_recipients += recipient
+
if(!length(possible_recipients))
- to_chat(user, span_noticealien("There's no one around to whisper to."))
+ to_chat(owner, span_noticealien("There's no one around to whisper to."))
return FALSE
- var/mob/living/chosen_recipient = tgui_input_list(user, "Select whisper recipient", "Whisper", sort_names(possible_recipients))
- if(isnull(chosen_recipient))
- return FALSE
- if(chosen_recipient.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0))
- to_chat(user, span_warning("As you reach into [chosen_recipient]'s mind, you are stopped by a mental blockage. It seems you've been foiled."))
+
+ var/mob/living/chosen_recipient = tgui_input_list(owner, "Select whisper recipient", "Whisper", sort_names(possible_recipients))
+ if(!chosen_recipient)
return FALSE
- var/msg = tgui_input_text(user, title = "Alien Whisper")
- if(isnull(msg))
+
+ var/to_whisper = tgui_input_text(owner, title = "Alien Whisper")
+ if(QDELETED(chosen_recipient) || QDELETED(src) || QDELETED(owner) || !IsAvailable() || !to_whisper)
return FALSE
if(chosen_recipient.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0))
- to_chat(user, span_warning("As you reach into [chosen_recipient]'s mind, you are stopped by a mental blockage. It seems you've been foiled."))
- return
- log_directed_talk(user, chosen_recipient, msg, LOG_SAY, tag="alien whisper")
- to_chat(chosen_recipient, "[span_noticealien("You hear a strange, alien voice in your head...")][msg]")
- to_chat(user, span_noticealien("You said: \"[msg]\" to [chosen_recipient]"))
- for(var/ded in GLOB.dead_mob_list)
- if(!isobserver(ded))
+ to_chat(owner, span_warning("As you reach into [chosen_recipient]'s mind, you are stopped by a mental blockage. It seems you've been foiled."))
+ return FALSE
+
+ log_directed_talk(owner, chosen_recipient, to_whisper, LOG_SAY, tag = "alien whisper")
+ to_chat(chosen_recipient, "[span_noticealien("You hear a strange, alien voice in your head...")][to_whisper]")
+ to_chat(owner, span_noticealien("You said: \"[to_whisper]\" to [chosen_recipient]"))
+ for(var/mob/dead_mob as anything in GLOB.dead_mob_list)
+ if(!isobserver(dead_mob))
continue
- var/follow_link_user = FOLLOW_LINK(ded, user)
- var/follow_link_whispee = FOLLOW_LINK(ded, chosen_recipient)
- to_chat(ded, "[follow_link_user] [span_name("[user]")] [span_alertalien("Alien Whisper --> ")] [follow_link_whispee] [span_name("[chosen_recipient]")] [span_noticealien("[msg]")]")
+ var/follow_link_user = FOLLOW_LINK(dead_mob, owner)
+ var/follow_link_whispee = FOLLOW_LINK(dead_mob, chosen_recipient)
+ to_chat(dead_mob, "[follow_link_user] [span_name("[owner]")] [span_alertalien("Alien Whisper --> ")] [follow_link_whispee] [span_name("[chosen_recipient]")] [span_noticealien("[to_whisper]")]")
+
return TRUE
-/obj/effect/proc_holder/alien/transfer
+/datum/action/cooldown/alien/transfer
name = "Transfer Plasma"
desc = "Transfer Plasma to another alien."
plasma_cost = 0
- action_icon_state = "alien_transfer"
+ button_icon_state = "alien_transfer"
-/obj/effect/proc_holder/alien/transfer/fire(mob/living/carbon/user)
+/datum/action/cooldown/alien/transfer/Activate(atom/target)
+ var/mob/living/carbon/carbon_owner = owner
var/list/mob/living/carbon/aliens_around = list()
- for(var/mob/living/carbon/alien in oview(user))
- if(isalien(alien))
- aliens_around.Add(alien)
+ for(var/mob/living/carbon/alien in view(owner))
+ if(alien.getPlasma() == -1 || alien == owner)
+ continue
+ aliens_around += alien
+
if(!length(aliens_around))
- to_chat(user, span_noticealien("There are no other aliens around."))
+ to_chat(owner, span_noticealien("There are no other aliens around."))
+ return FALSE
+
+ var/mob/living/carbon/donation_target = tgui_input_list(owner, "Target to transfer to", "Plasma Donation", sort_names(aliens_around))
+ if(!donation_target)
return FALSE
- var/mob/living/carbon/donation_target = tgui_input_list(user, "Target to transfer to", "Plasma Donation", sort_names(aliens_around))
- if(isnull(donation_target))
+
+ var/amount = tgui_input_number(owner, "Amount", "Transfer Plasma to [donation_target]", max_value = carbon_owner.getPlasma())
+ if(QDELETED(donation_target) || QDELETED(src) || QDELETED(owner) || !IsAvailable() || isnull(amount) || amount <= 0)
return FALSE
- var/amount = tgui_input_number(user, "Amount", "Transfer Plasma to [donation_target]", max_value = user.getPlasma())
- if(!amount || QDELETED(user) || QDELETED(donation_target))
+
+ if(get_dist(owner, donation_target) > 1)
+ to_chat(owner, span_noticealien("You need to be closer!"))
return FALSE
- if (get_dist(user, donation_target) <= 1)
- donation_target.adjustPlasma(amount)
- user.adjustPlasma(-amount)
- to_chat(donation_target, span_noticealien("[user] has transferred [amount] plasma to you."))
- to_chat(user, span_noticealien("You transfer [amount] plasma to [donation_target]."))
- else
- to_chat(user, span_noticealien("You need to be closer!"))
+ donation_target.adjustPlasma(amount)
+ carbon_owner.adjustPlasma(-amount)
-/obj/effect/proc_holder/alien/acid
+ to_chat(donation_target, span_noticealien("[owner] has transferred [amount] plasma to you."))
+ to_chat(owner, span_noticealien("You transfer [amount] plasma to [donation_target]."))
+ return TRUE
+
+/datum/action/cooldown/alien/acid
+ click_to_activate = TRUE
+ unset_after_click = FALSE
+
+/datum/action/cooldown/alien/acid/corrosion
name = "Corrosive Acid"
desc = "Drench an object in acid, destroying it over time."
+ button_icon_state = "alien_acid"
plasma_cost = 200
- action_icon_state = "alien_acid"
-/obj/effect/proc_holder/alien/acid/on_gain(mob/living/carbon/user)
- add_verb(user, /mob/living/carbon/proc/corrosive_acid)
+/datum/action/cooldown/alien/acid/corrosion/set_click_ability(mob/on_who)
+ . = ..()
+ if(!.)
+ return
-/obj/effect/proc_holder/alien/acid/on_lose(mob/living/carbon/user)
- remove_verb(user, /mob/living/carbon/proc/corrosive_acid)
+ to_chat(on_who, span_noticealien("You prepare to vomit acid. Click a target to acid it!"))
+ on_who.update_icons()
-/obj/effect/proc_holder/alien/acid/proc/corrode(atom/target, mob/living/carbon/user = usr)
- if(!(target in oview(1,user)))
- to_chat(src, span_noticealien("[target] is too far away."))
- return FALSE
- if(target.acid_act(200, 1000))
- user.visible_message(span_alertalien("[user] vomits globs of vile stuff all over [target]. It begins to sizzle and melt under the bubbling mess of acid!"))
- return TRUE
- else
- to_chat(user, span_noticealien("You cannot dissolve this object."))
- return FALSE
+/datum/action/cooldown/alien/acid/corrosion/unset_click_ability(mob/on_who, refund_cooldown = TRUE)
+ . = ..()
+ if(!.)
+ return
-/obj/effect/proc_holder/alien/acid/fire(mob/living/carbon/alien/user)
- var/list/nearby_targets = list()
- for(var/atom/target in oview(1, user))
- nearby_targets.Add(target)
- if(!length(nearby_targets))
- to_chat(user, span_noticealien("There's nothing to corrode."))
- return FALSE
- var/atom/dissolve_target = tgui_input_list(user, "Select a target to dissolve", "Dissolve", nearby_targets)
- if(isnull(dissolve_target))
- return FALSE
- if(QDELETED(dissolve_target) || user.incapacitated())
+ if(refund_cooldown)
+ to_chat(on_who, span_noticealien("You empty your corrosive acid glands."))
+ on_who.update_icons()
+
+/datum/action/cooldown/alien/acid/corrosion/PreActivate(atom/target)
+ if(get_dist(owner, target) > 1)
return FALSE
- return corrode(dissolve_target, user)
+ return ..()
-/mob/living/carbon/proc/corrosive_acid(O as obj|turf in oview(1)) // right click menu verb ugh
- set name = "Corrosive Acid"
+/datum/action/cooldown/alien/acid/corrosion/Activate(atom/target)
+ if(!target.acid_act(200, 1000))
+ to_chat(owner, span_noticealien("You cannot dissolve this object."))
+ return FALSE
- if(!iscarbon(usr))
- return
- var/mob/living/carbon/user = usr
- var/obj/effect/proc_holder/alien/acid/A = locate() in user.abilities
- if(!A)
- return
- if(user.getPlasma() > A.plasma_cost && A.corrode(O))
- user.adjustPlasma(-A.plasma_cost)
+ owner.visible_message(
+ span_alertalien("[owner] vomits globs of vile stuff all over [target]. It begins to sizzle and melt under the bubbling mess of acid!"),
+ span_noticealien("You vomit globs of acid over [target]. It begins to sizzle and melt."),
+ )
+ return TRUE
-/obj/effect/proc_holder/alien/neurotoxin
+/datum/action/cooldown/alien/acid/neurotoxin
name = "Spit Neurotoxin"
desc = "Spits neurotoxin at someone, paralyzing them for a short time."
- action_icon_state = "alien_neurotoxin_0"
- active = FALSE
-
-/obj/effect/proc_holder/alien/neurotoxin/fire(mob/living/carbon/user)
- var/message
- if(active)
- message = span_notice("You empty your neurotoxin gland.")
- remove_ranged_ability(message)
- else
- message = span_notice("You prepare your neurotoxin gland. Left-click to fire at a target!")
- add_ranged_ability(user, message, TRUE)
+ button_icon_state = "alien_neurotoxin_0"
+ plasma_cost = 50
-/obj/effect/proc_holder/alien/neurotoxin/update_icon()
- action.button_icon_state = "alien_neurotoxin_[active]"
- action.UpdateButtons()
- return ..()
+/datum/action/cooldown/alien/acid/neurotoxin/IsAvailable()
+ return ..() && isturf(owner.loc)
-/obj/effect/proc_holder/alien/neurotoxin/InterceptClickOn(mob/living/caller, params, atom/target)
+/datum/action/cooldown/alien/acid/neurotoxin/set_click_ability(mob/on_who)
. = ..()
- if(.)
+ if(!.)
+ return
+
+ to_chat(on_who, span_notice("You prepare your neurotoxin gland. Left-click to fire at a target!"))
+
+ button_icon_state = "alien_neurotoxin_1"
+ UpdateButtons()
+ on_who.update_icons()
+
+/datum/action/cooldown/alien/acid/neurotoxin/unset_click_ability(mob/on_who, refund_cooldown = TRUE)
+ . = ..()
+ if(!.)
return
- var/p_cost = 50
- if(!iscarbon(ranged_ability_user) || ranged_ability_user.stat)
- remove_ranged_ability()
- return FALSE
- var/mob/living/carbon/user = ranged_ability_user
+ if(refund_cooldown)
+ to_chat(on_who, span_notice("You empty your neurotoxin gland."))
- if(user.getPlasma() < p_cost)
- to_chat(user, span_warning("You need at least [p_cost] plasma to spit."))
- remove_ranged_ability()
+ button_icon_state = "alien_neurotoxin_0"
+ UpdateButtons()
+ on_who.update_icons()
+
+/datum/action/cooldown/alien/acid/neurotoxin/InterceptClickOn(mob/living/caller, params, atom/target)
+ . = ..()
+ if(!.)
+ unset_click_ability(caller, refund_cooldown = FALSE)
return FALSE
- var/turf/T = user.loc
- var/turf/U = get_step(user, user.dir) // Get the tile infront of the move, based on their direction
- if(!isturf(U) || !isturf(T))
+ // We do this in InterceptClickOn() instead of Activate()
+ // because we use the click parameters for aiming the projectile
+ // (or something like that)
+ var/turf/user_turf = caller.loc
+ var/turf/target_turf = get_step(caller, target.dir) // Get the tile infront of the move, based on their direction
+ if(!isturf(target_turf))
return FALSE
var/modifiers = params2list(params)
- user.visible_message(span_danger("[user] spits neurotoxin!"), span_alertalien("You spit neurotoxin."))
- var/obj/projectile/neurotoxin/neurotoxin = new /obj/projectile/neurotoxin(user.loc)
- neurotoxin.preparePixelProjectile(target, user, modifiers)
- neurotoxin.firer = user
+ caller.visible_message(
+ span_danger("[caller] spits neurotoxin!"),
+ span_alertalien("You spit neurotoxin."),
+ )
+ var/obj/projectile/neurotoxin/neurotoxin = new /obj/projectile/neurotoxin(caller.loc)
+ neurotoxin.preparePixelProjectile(target, caller, modifiers)
+ neurotoxin.firer = caller
neurotoxin.fire()
- user.newtonian_move(get_dir(U, T))
- user.adjustPlasma(-p_cost)
-
+ caller.newtonian_move(get_dir(target_turf, user_turf))
return TRUE
-/obj/effect/proc_holder/alien/neurotoxin/on_lose(mob/living/carbon/user)
- remove_ranged_ability()
-
-/obj/effect/proc_holder/alien/neurotoxin/add_ranged_ability(mob/living/user,msg,forced)
- ..()
- if(isalienadult(user))
- var/mob/living/carbon/alien/humanoid/A = user
- A.drooling = 1
- A.update_icons()
-
-/obj/effect/proc_holder/alien/neurotoxin/remove_ranged_ability(msg)
- if(isalienadult(ranged_ability_user))
- var/mob/living/carbon/alien/humanoid/A = ranged_ability_user
- A.drooling = 0
- A.update_icons()
- ..()
+// Has to return TRUE, otherwise is skipped.
+/datum/action/cooldown/alien/acid/neurotoxin/Activate(atom/target)
+ return TRUE
-/obj/effect/proc_holder/alien/resin
+/datum/action/cooldown/alien/make_structure/resin
name = "Secrete Resin"
desc = "Secrete tough malleable resin."
+ button_icon_state = "alien_resin"
plasma_cost = 55
- check_turf = TRUE
- var/list/structures = list(
+ /// A list of all structures we can make.
+ var/static/list/structures = list(
"resin wall" = /obj/structure/alien/resin/wall,
"resin membrane" = /obj/structure/alien/resin/membrane,
- "resin nest" = /obj/structure/bed/nest)
+ "resin nest" = /obj/structure/bed/nest,
+ )
+
+// Snowflake to check for multiple types of alien resin structures
+/datum/action/cooldown/alien/make_structure/resin/check_for_duplicate()
+ for(var/blocker_name in structures)
+ var/obj/structure/blocker_type = structures[blocker_name]
+ if(locate(blocker_type) in owner.loc)
+ to_chat(owner, span_warning("There is already a resin structure there!"))
+ return FALSE
- action_icon_state = "alien_resin"
+ return TRUE
-/obj/effect/proc_holder/alien/resin/fire(mob/living/carbon/user)
- if(locate(/obj/structure/alien/resin) in user.loc)
- to_chat(user, span_warning("There is already a resin structure there!"))
+/datum/action/cooldown/alien/make_structure/resin/Activate(atom/target)
+ var/choice = tgui_input_list(owner, "Select a shape to build", "Resin building", structures)
+ if(isnull(choice) || QDELETED(src) || QDELETED(owner) || !check_for_duplicate() || !IsAvailable())
return FALSE
- if(!check_vent_block(user))
+ var/obj/structure/choice_path = structures[choice]
+ if(!ispath(choice_path))
return FALSE
- var/choice = tgui_input_list(user, "Select a shape to build", "Resin building", structures)
- if(isnull(choice))
- return FALSE
- if(isnull(structures[choice]))
- return FALSE
- if (!cost_check(check_turf,user))
- return FALSE
- to_chat(user, span_notice("You shape a [choice]."))
- user.visible_message(span_notice("[user] vomits up a thick purple substance and begins to shape it."))
+ owner.visible_message(
+ span_notice("[owner] vomits up a thick purple substance and begins to shape it."),
+ span_notice("You shape a [choice] out of resin."),
+ )
- choice = structures[choice]
- new choice(user.loc)
+ new choice_path(owner.loc)
return TRUE
-/obj/effect/proc_holder/alien/sneak
+/datum/action/cooldown/alien/sneak
name = "Sneak"
desc = "Blend into the shadows to stalk your prey."
- active = 0
+ button_icon_state = "alien_sneak"
+ /// The alpha we go to when sneaking.
+ var/sneak_alpha = 75
+
+/datum/action/cooldown/alien/sneak/Remove(mob/living/remove_from)
+ if(HAS_TRAIT(remove_from, TRAIT_ALIEN_SNEAK))
+ remove_from.alpha = initial(remove_from.alpha)
+ REMOVE_TRAIT(remove_from, TRAIT_ALIEN_SNEAK, name)
- action_icon_state = "alien_sneak"
+ return ..()
+
+/datum/action/cooldown/alien/sneak/Activate(atom/target)
+ if(HAS_TRAIT(owner, TRAIT_ALIEN_SNEAK))
+ // It's safest to go to the initial alpha of the mob.
+ // Otherwise we get permanent invisbility exploits.
+ owner.alpha = initial(owner.alpha)
+ to_chat(owner, span_noticealien("You reveal yourself!"))
+ REMOVE_TRAIT(owner, TRAIT_ALIEN_SNEAK, name)
-/obj/effect/proc_holder/alien/sneak/fire(mob/living/carbon/alien/humanoid/user)
- if(!active)
- user.alpha = 75 //Still easy to see in lit areas with bright tiles, almost invisible on resin.
- user.sneaking = 1
- active = 1
- to_chat(user, span_noticealien("You blend into the shadows..."))
else
- user.alpha = initial(user.alpha)
- user.sneaking = 0
- active = 0
- to_chat(user, span_noticealien("You reveal yourself!"))
+ owner.alpha = sneak_alpha
+ to_chat(owner, span_noticealien("You blend into the shadows..."))
+ ADD_TRAIT(owner, TRAIT_ALIEN_SNEAK, name)
+ return TRUE
+/// Gets the plasma level of this carbon's plasma vessel, or -1 if they don't have one
/mob/living/carbon/proc/getPlasma()
var/obj/item/organ/internal/alien/plasmavessel/vessel = getorgan(/obj/item/organ/internal/alien/plasmavessel)
if(!vessel)
- return 0
- return vessel.storedPlasma
-
+ return -1
+ return vessel.stored_plasma
+/// Adjusts the plasma level of the carbon's plasma vessel if they have one
/mob/living/carbon/proc/adjustPlasma(amount)
var/obj/item/organ/internal/alien/plasmavessel/vessel = getorgan(/obj/item/organ/internal/alien/plasmavessel)
if(!vessel)
return FALSE
- vessel.storedPlasma = max(vessel.storedPlasma + amount,0)
- vessel.storedPlasma = min(vessel.storedPlasma, vessel.max_plasma) //upper limit of max_plasma, lower limit of 0
- for(var/X in abilities)
- var/obj/effect/proc_holder/alien/APH = X
- if(APH.has_action)
- APH.action.UpdateButtons()
+ vessel.stored_plasma = max(vessel.stored_plasma + amount,0)
+ vessel.stored_plasma = min(vessel.stored_plasma, vessel.max_plasma) //upper limit of max_plasma, lower limit of 0
+ for(var/datum/action/cooldown/alien/ability in actions)
+ ability.UpdateButtons()
return TRUE
/mob/living/carbon/alien/adjustPlasma(amount)
. = ..()
updatePlasmaDisplay()
-
-/mob/living/carbon/proc/usePlasma(amount)
- if(getPlasma() >= amount)
- adjustPlasma(-amount)
- return TRUE
- return FALSE
diff --git a/code/modules/mob/living/carbon/alien/humanoid/caste/drone.dm b/code/modules/mob/living/carbon/alien/humanoid/caste/drone.dm
index ba7a76c8b0a37..7d92ace5be285 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/caste/drone.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/caste/drone.dm
@@ -6,38 +6,45 @@
icon_state = "aliend"
/mob/living/carbon/alien/humanoid/drone/Initialize(mapload)
- AddAbility(new/obj/effect/proc_holder/alien/evolve(null))
- . = ..()
+ var/datum/action/cooldown/alien/evolve_to_praetorian/evolution = new(src)
+ evolution.Grant(src)
+ return ..()
/mob/living/carbon/alien/humanoid/drone/create_internal_organs()
internal_organs += new /obj/item/organ/internal/alien/plasmavessel/large
internal_organs += new /obj/item/organ/internal/alien/resinspinner
internal_organs += new /obj/item/organ/internal/alien/acid
- ..()
+ return ..()
-/obj/effect/proc_holder/alien/evolve
+/datum/action/cooldown/alien/evolve_to_praetorian
name = "Evolve to Praetorian"
desc = "Praetorian"
+ button_icon_state = "alien_evolve_drone"
plasma_cost = 500
- action_icon_state = "alien_evolve_drone"
-
-/obj/effect/proc_holder/alien/evolve/fire(mob/living/carbon/alien/humanoid/user)
- var/obj/item/organ/internal/alien/hivenode/node = user.getorgan(/obj/item/organ/internal/alien/hivenode)
- if(!node) //Players are Murphy's Law. We may not expect there to ever be a living xeno with no hivenode, but they _WILL_ make it happen.
- to_chat(user, span_danger("Without the hivemind, you can't possibly hold the responsibility of leadership!"))
+/datum/action/cooldown/alien/evolve_to_praetorian/IsAvailable()
+ . = ..()
+ if(!.)
return FALSE
- if(node.recent_queen_death)
- to_chat(user, span_danger("Your thoughts are still too scattered to take up the position of leadership."))
+
+ if(!isturf(owner.loc))
return FALSE
- if(!isturf(user.loc))
- to_chat(user, span_warning("You can't evolve here!"))
+ if(get_alien_type(/mob/living/carbon/alien/humanoid/royal))
return FALSE
- if(!get_alien_type(/mob/living/carbon/alien/humanoid/royal))
- var/mob/living/carbon/alien/humanoid/royal/praetorian/new_xeno = new (user.loc)
- user.alien_evolve(new_xeno)
- return TRUE
- else
- to_chat(user, span_warning("We already have a living royal!"))
+
+ var/mob/living/carbon/alien/humanoid/royal/evolver = owner
+ var/obj/item/organ/internal/alien/hivenode/node = evolver.getorgan(/obj/item/organ/internal/alien/hivenode)
+ // Players are Murphy's Law. We may not expect
+ // there to ever be a living xeno with no hivenode,
+ // but they _WILL_ make it happen.
+ if(!node || node.recent_queen_death)
return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/alien/evolve_to_praetorian/Activate(atom/target)
+ var/mob/living/carbon/alien/humanoid/evolver = owner
+ var/mob/living/carbon/alien/humanoid/royal/praetorian/new_xeno = new(owner.loc)
+ evolver.alien_evolve(new_xeno)
+ return TRUE
diff --git a/code/modules/mob/living/carbon/alien/humanoid/caste/praetorian.dm b/code/modules/mob/living/carbon/alien/humanoid/caste/praetorian.dm
index c4dd26aa62e4a..28fb151d83ba8 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/caste/praetorian.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/caste/praetorian.dm
@@ -7,36 +7,48 @@
/mob/living/carbon/alien/humanoid/royal/praetorian/Initialize(mapload)
real_name = name
- AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/repulse/xeno(src))
- AddAbility(new /obj/effect/proc_holder/alien/royal/praetorian/evolve())
- . = ..()
+
+ var/datum/action/cooldown/spell/aoe/repulse/xeno/tail_whip = new(src)
+ tail_whip.Grant(src)
+
+ var/datum/action/cooldown/alien/evolve_to_queen/evolution = new(src)
+ evolution.Grant(src)
+
+ return ..()
/mob/living/carbon/alien/humanoid/royal/praetorian/create_internal_organs()
internal_organs += new /obj/item/organ/internal/alien/plasmavessel/large
internal_organs += new /obj/item/organ/internal/alien/resinspinner
internal_organs += new /obj/item/organ/internal/alien/acid
internal_organs += new /obj/item/organ/internal/alien/neurotoxin
- ..()
+ return ..()
-/obj/effect/proc_holder/alien/royal/praetorian/evolve
+/datum/action/cooldown/alien/evolve_to_queen
name = "Evolve"
desc = "Produce an internal egg sac capable of spawning children. Only one queen can exist at a time."
+ button_icon_state = "alien_evolve_praetorian"
plasma_cost = 500
- action_icon_state = "alien_evolve_praetorian"
+/datum/action/cooldown/alien/evolve_to_queen/IsAvailable()
+ . = ..()
+ if(!.)
+ return FALSE
-/obj/effect/proc_holder/alien/royal/praetorian/evolve/fire(mob/living/carbon/alien/humanoid/user)
- var/obj/item/organ/internal/alien/hivenode/node = user.getorgan(/obj/item/organ/internal/alien/hivenode)
- if(!node) //Just in case this particular Praetorian gets violated and kept by the RD as a replacement for Lamarr.
- to_chat(user, span_warning("Without the hivemind, you would be unfit to rule as queen!"))
+ if(!isturf(owner.loc))
return FALSE
- if(node.recent_queen_death)
- to_chat(user, span_warning("You are still too burdened with guilt to evolve into a queen."))
+
+ if(get_alien_type(/mob/living/carbon/alien/humanoid/royal/queen))
return FALSE
- if(!get_alien_type(/mob/living/carbon/alien/humanoid/royal/queen))
- var/mob/living/carbon/alien/humanoid/royal/queen/new_xeno = new (user.loc)
- user.alien_evolve(new_xeno)
- return TRUE
- else
- to_chat(user, span_warning("We already have an alive queen!"))
+
+ var/mob/living/carbon/alien/humanoid/royal/evolver = owner
+ var/obj/item/organ/internal/alien/hivenode/node = evolver.getorgan(/obj/item/organ/internal/alien/hivenode)
+ if(!node || node.recent_queen_death)
return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/alien/evolve_to_queen/Activate(atom/target)
+ var/mob/living/carbon/alien/humanoid/royal/evolver = owner
+ var/mob/living/carbon/alien/humanoid/royal/queen/new_queen = new(owner.loc)
+ evolver.alien_evolve(new_queen)
+ return TRUE
diff --git a/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm b/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm
index df449a1d79048..b86891a8dcab5 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/caste/sentinel.dm
@@ -6,8 +6,9 @@
icon_state = "aliens"
/mob/living/carbon/alien/humanoid/sentinel/Initialize(mapload)
- AddAbility(new /obj/effect/proc_holder/alien/sneak)
- . = ..()
+ var/datum/action/cooldown/alien/sneak/sneaky_beaky = new(src)
+ sneaky_beaky.Grant(src)
+ return ..()
/mob/living/carbon/alien/humanoid/sentinel/create_internal_organs()
internal_organs += new /obj/item/organ/internal/alien/plasmavessel
diff --git a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
index c7c0258cd01ed..174d06bf4c9cc 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/humanoid.dm
@@ -12,8 +12,6 @@
var/leap_on_click = 0
var/pounce_cooldown = 0
var/pounce_cooldown_time = 30
- var/sneaking = 0 //For sneaky-sneaky mode and appropriate slowdown
- var/drooling = 0 //For Neruotoxic spit overlays
deathsound = 'sound/voice/hiss6.ogg'
bodyparts = list(
/obj/item/bodypart/chest/alien,
@@ -61,11 +59,10 @@ GLOBAL_LIST_INIT(strippable_alien_humanoid_items, create_strippable_list(list(
return A
return FALSE
-
/mob/living/carbon/alien/humanoid/check_breath(datum/gas_mixture/breath)
- if(breath && breath.total_moles() > 0 && !sneaking)
+ if(breath?.total_moles() > 0 && !HAS_TRAIT(src, TRAIT_ALIEN_SNEAK))
playsound(get_turf(src), pick('sound/voice/lowHiss2.ogg', 'sound/voice/lowHiss3.ogg', 'sound/voice/lowHiss4.ogg'), 50, FALSE, -5)
- ..()
+ return ..()
/mob/living/carbon/alien/humanoid/set_name()
if(numba)
diff --git a/code/modules/mob/living/carbon/alien/humanoid/humanoid_update_icons.dm b/code/modules/mob/living/carbon/alien/humanoid/humanoid_update_icons.dm
index 75268b053031f..ff6302b569f9b 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/humanoid_update_icons.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/humanoid_update_icons.dm
@@ -4,7 +4,8 @@
for(var/I in overlays_standing)
add_overlay(I)
- var/asleep = IsSleeping()
+ var/are_we_drooling = istype(click_intercept, /datum/action/cooldown/alien/acid)
+
if(stat == DEAD)
//If we mostly took damage from fire
if(getFireLoss() > 125)
@@ -12,7 +13,7 @@
else
icon_state = "alien[caste]_dead"
- else if((stat == UNCONSCIOUS && !asleep) || stat == HARD_CRIT || stat == SOFT_CRIT || IsParalyzed())
+ else if((stat == UNCONSCIOUS && !IsSleeping()) || stat == HARD_CRIT || stat == SOFT_CRIT || IsParalyzed())
icon_state = "alien[caste]_unconscious"
else if(leap_on_click)
icon_state = "alien[caste]_pounce"
@@ -21,11 +22,11 @@
icon_state = "alien[caste]_sleep"
else if(mob_size == MOB_SIZE_LARGE)
icon_state = "alien[caste]"
- if(drooling)
+ if(are_we_drooling)
add_overlay("alienspit_[caste]")
else
icon_state = "alien[caste]"
- if(drooling)
+ if(are_we_drooling)
add_overlay("alienspit")
if(leaping)
diff --git a/code/modules/mob/living/carbon/alien/humanoid/queen.dm b/code/modules/mob/living/carbon/alien/humanoid/queen.dm
index dccce5375429c..9bf9a243e2e1b 100644
--- a/code/modules/mob/living/carbon/alien/humanoid/queen.dm
+++ b/code/modules/mob/living/carbon/alien/humanoid/queen.dm
@@ -37,7 +37,6 @@
maxHealth = 400
health = 400
icon_state = "alienq"
- var/datum/action/small_sprite/smallsprite = new/datum/action/small_sprite/queen()
/mob/living/carbon/alien/humanoid/royal/queen/Initialize(mapload)
//there should only be one queen
@@ -52,9 +51,15 @@
real_name = src.name
- AddSpell(new /obj/effect/proc_holder/spell/aoe_turf/repulse/xeno(src))
- AddAbility(new/obj/effect/proc_holder/alien/royal/queen/promote())
+ var/datum/action/cooldown/spell/aoe/repulse/xeno/tail_whip = new(src)
+ tail_whip.Grant(src)
+
+ var/datum/action/small_sprite/queen/smallsprite = new(src)
smallsprite.Grant(src)
+
+ var/datum/action/cooldown/alien/promote/promotion = new(src)
+ promotion.Grant(src)
+
return ..()
/mob/living/carbon/alien/humanoid/royal/queen/create_internal_organs()
@@ -63,92 +68,116 @@
internal_organs += new /obj/item/organ/internal/alien/acid
internal_organs += new /obj/item/organ/internal/alien/neurotoxin
internal_organs += new /obj/item/organ/internal/alien/eggsac
- ..()
+ return ..()
//Queen verbs
-/obj/effect/proc_holder/alien/lay_egg
+/datum/action/cooldown/alien/make_structure/lay_egg
name = "Lay Egg"
desc = "Lay an egg to produce huggers to impregnate prey with."
+ button_icon_state = "alien_egg"
plasma_cost = 75
- check_turf = TRUE
- action_icon_state = "alien_egg"
+ made_structure_type = /obj/structure/alien/egg
+
+/datum/action/cooldown/alien/make_structure/lay_egg/Activate(atom/target)
+ . = ..()
+ owner.visible_message(span_alertalien("[owner] lays an egg!"))
+
+//Button to let queen choose her praetorian.
+/datum/action/cooldown/alien/promote
+ name = "Create Royal Parasite"
+ desc = "Produce a royal parasite to grant one of your children the honor of being your Praetorian."
+ button_icon_state = "alien_queen_promote"
+ /// The promotion only takes plasma when completed, not on activation.
+ var/promotion_plasma_cost = 500
+
+/datum/action/cooldown/alien/promote/set_statpanel_format()
+ . = ..()
+ if(!islist(.))
+ return
+
+ .[PANEL_DISPLAY_STATUS] = "PLASMA - [promotion_plasma_cost]"
-/obj/effect/proc_holder/alien/lay_egg/fire(mob/living/carbon/user)
- if(!check_vent_block(user))
+/datum/action/cooldown/alien/promote/IsAvailable()
+ . = ..()
+ if(!.)
+ return FALSE
+
+ var/mob/living/carbon/carbon_owner = owner
+ if(carbon_owner.getPlasma() < promotion_plasma_cost)
return FALSE
- if(locate(/obj/structure/alien/egg) in get_turf(user))
- to_chat(user, span_alertalien("There's already an egg here."))
+ if(get_alien_type(/mob/living/carbon/alien/humanoid/royal/praetorian))
return FALSE
- user.visible_message(span_alertalien("[user] lays an egg!"))
- new /obj/structure/alien/egg(user.loc)
return TRUE
-//Button to let queen choose her praetorian.
-/obj/effect/proc_holder/alien/royal/queen/promote
- name = "Create Royal Parasite"
- desc = "Produce a royal parasite to grant one of your children the honor of being your Praetorian."
- plasma_cost = 500 //Plasma cost used on promotion, not spawning the parasite.
+/datum/action/cooldown/alien/promote/Activate(atom/target)
+ var/obj/item/queen_promotion/existing_promotion = locate() in owner.held_items
+ if(existing_promotion)
+ to_chat(owner, span_noticealien("You discard [existing_promotion]."))
+ owner.temporarilyRemoveItemFromInventory(existing_promotion)
+ qdel(existing_promotion)
+ return TRUE
- action_icon_state = "alien_queen_promote"
+ if(!owner.get_empty_held_indexes())
+ to_chat(owner, span_warning("You must have an empty hand before preparing the parasite."))
+ return FALSE
+ var/obj/item/queen_promotion/new_promotion = new(owner.loc)
+ if(!owner.put_in_hands(new_promotion, del_on_fail = TRUE))
+ to_chat(owner, span_noticealien("You fail to prepare a parasite."))
+ return FALSE
+ to_chat(owner, span_noticealien("Use [new_promotion] on one of your children to promote her to a Praetorian!"))
+ return TRUE
-/obj/effect/proc_holder/alien/royal/queen/promote/fire(mob/living/carbon/alien/user)
- var/obj/item/queenpromote/prom
- if(get_alien_type(/mob/living/carbon/alien/humanoid/royal/praetorian/))
- to_chat(user, span_noticealien("You already have a Praetorian!"))
- return
- else
- for(prom in user)
- to_chat(user, span_noticealien("You discard [prom]."))
- qdel(prom)
- return
-
- prom = new (user.loc)
- if(!user.put_in_active_hand(prom, 1))
- to_chat(user, span_warning("You must empty your hands before preparing the parasite."))
- return
- else //Just in case telling the player only once is not enough!
- to_chat(user, span_noticealien("Use the royal parasite on one of your children to promote her to Praetorian!"))
- return
-
-/obj/item/queenpromote
+/obj/item/queen_promotion
name = "\improper royal parasite"
desc = "Inject this into one of your grown children to promote her to a Praetorian!"
icon_state = "alien_medal"
- item_flags = ABSTRACT | DROPDEL
+ item_flags = NOBLUDGEON | ABSTRACT | DROPDEL
icon = 'icons/mob/alien.dmi'
-/obj/item/queenpromote/Initialize(mapload)
+/obj/item/queen_promotion/attack(mob/living/to_promote, mob/living/carbon/alien/humanoid/queen)
. = ..()
- ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT)
+ if(.)
+ return
-/obj/item/queenpromote/attack(mob/living/M, mob/living/carbon/alien/humanoid/user)
- if(!isalienadult(M) || isalienroyal(M))
- to_chat(user, span_noticealien("You may only use this with your adult, non-royal children!"))
+ var/datum/action/cooldown/alien/promote/promotion = locate() in queen.actions
+ if(!promotion)
+ CRASH("[type] was created and handled by a mob ([queen]) that didn't have a promotion action associated.")
+
+ if(!isalienadult(to_promote) || isalienroyal(to_promote))
+ to_chat(queen, span_noticealien("You may only use this with your adult, non-royal children!"))
return
- if(get_alien_type(/mob/living/carbon/alien/humanoid/royal/praetorian/))
- to_chat(user, span_noticealien("You already have a Praetorian!"))
+
+ if(!promotion.IsAvailable())
+ to_chat(queen, span_noticealien("You cannot promote a child right now!"))
return
- var/mob/living/carbon/alien/humanoid/A = M
- if(A.stat == CONSCIOUS && A.mind && A.key)
- if(!user.usePlasma(500))
- to_chat(user, span_noticealien("You must have 500 plasma stored to use this!"))
- return
-
- to_chat(A, span_noticealien("The queen has granted you a promotion to Praetorian!"))
- user.visible_message(span_alertalien("[A] begins to expand, twist and contort!"))
- var/mob/living/carbon/alien/humanoid/royal/praetorian/new_prae = new (A.loc)
- A.mind.transfer_to(new_prae)
- qdel(A)
- qdel(src)
+ if(to_promote.stat != CONSCIOUS || !to_promote.mind || !to_promote.key)
return
- else
- to_chat(user, span_warning("This child must be alert and responsive to become a Praetorian!"))
-/obj/item/queenpromote/attack_self(mob/user)
+ queen.adjustPlasma(-promotion.promotion_plasma_cost)
+
+ to_chat(queen, span_noticealien("You have promoted [to_promote] to a Praetorian!"))
+ to_promote.visible_message(
+ span_alertalien("[to_promote] begins to expand, twist and contort!"),
+ span_noticealien("The queen has granted you a promotion to Praetorian!"),
+ )
+
+ var/mob/living/carbon/alien/humanoid/royal/praetorian/new_prae = new(to_promote.loc)
+ to_promote.mind.transfer_to(new_prae)
+
+ qdel(to_promote)
+ qdel(src)
+ return TRUE
+
+/obj/item/queen_promotion/attack_self(mob/user)
to_chat(user, span_noticealien("You discard [src]."))
qdel(src)
+
+/obj/item/queen_promotion/dropped(mob/user, silent)
+ if(!silent)
+ to_chat(user, span_noticealien("You discard [src]."))
+ return ..()
diff --git a/code/modules/mob/living/carbon/alien/larva/larva.dm b/code/modules/mob/living/carbon/alien/larva/larva.dm
index 734a5ef3e3ca6..a109eb3a77068 100644
--- a/code/modules/mob/living/carbon/alien/larva/larva.dm
+++ b/code/modules/mob/living/carbon/alien/larva/larva.dm
@@ -31,16 +31,18 @@
//This is fine right now, if we're adding organ specific damage this needs to be updated
/mob/living/carbon/alien/larva/Initialize(mapload)
-
- AddAbility(new/obj/effect/proc_holder/alien/hide(null))
- AddAbility(new/obj/effect/proc_holder/alien/larva_evolve(null))
- . = ..()
+ var/datum/action/cooldown/alien/larva_evolve/evolution = new(src)
+ evolution.Grant(src)
+ var/datum/action/cooldown/alien/hide/hide = new(src)
+ hide.Grant(src)
+ return ..()
/mob/living/carbon/alien/larva/create_internal_organs()
internal_organs += new /obj/item/organ/internal/alien/plasmavessel/small/tiny
..()
//This needs to be fixed
+// This comment is 12 years old I hope it's fixed by now
/mob/living/carbon/alien/larva/get_status_tab_items()
. = ..()
. += "Progress: [amount_grown]/[max_grown]"
diff --git a/code/modules/mob/living/carbon/alien/larva/powers.dm b/code/modules/mob/living/carbon/alien/larva/powers.dm
index 4b6774a7dd506..a248173be246a 100644
--- a/code/modules/mob/living/carbon/alien/larva/powers.dm
+++ b/code/modules/mob/living/carbon/alien/larva/powers.dm
@@ -1,57 +1,92 @@
-/obj/effect/proc_holder/alien/hide
+/datum/action/cooldown/alien/hide
name = "Hide"
- desc = "Allows aliens to hide beneath tables or certain items. Toggled on or off."
+ desc = "Allows you to hide beneath tables and certain objects."
+ button_icon_state = "alien_hide"
plasma_cost = 0
+ /// The layer we are on while hiding
+ var/hide_layer = ABOVE_NORMAL_TURF_LAYER
- action_icon_state = "alien_hide"
+/datum/action/cooldown/alien/hide/Activate(atom/target)
+ if(owner.layer == hide_layer)
+ owner.layer = initial(owner.layer)
+ owner.visible_message(
+ span_notice("[owner] slowly peeks up from the ground..."),
+ span_noticealien("You stop hiding."),
+ )
-/obj/effect/proc_holder/alien/hide/fire(mob/living/carbon/alien/user)
- if(user.stat != CONSCIOUS)
- return
-
- if (user.layer != ABOVE_NORMAL_TURF_LAYER)
- user.layer = ABOVE_NORMAL_TURF_LAYER
- user.visible_message(span_name("[user] scurries to the ground!"), \
- span_noticealien("You are now hiding."))
else
- user.layer = MOB_LAYER
- user.visible_message(span_notice("[user] slowly peeks up from the ground..."), \
- span_noticealien("You stop hiding."))
- return 1
+ owner.layer = hide_layer
+ owner.visible_message(
+ span_name("[owner] scurries to the ground!"),
+ span_noticealien("You are now hiding."),
+ )
+ return TRUE
-/obj/effect/proc_holder/alien/larva_evolve
+/datum/action/cooldown/alien/larva_evolve
name = "Evolve"
desc = "Evolve into a higher alien caste."
+ button_icon_state = "alien_evolve_larva"
plasma_cost = 0
- action_icon_state = "alien_evolve_larva"
-
-/obj/effect/proc_holder/alien/larva_evolve/fire(mob/living/carbon/alien/user)
- if(!islarva(user))
- return
- var/mob/living/carbon/alien/larva/larva = user
+/datum/action/cooldown/alien/larva_evolve/IsAvailable()
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!islarva(owner))
+ return FALSE
+ var/mob/living/carbon/alien/larva/larva = owner
if(larva.handcuffed || larva.legcuffed) // Cuffing larvas ? Eh ?
- to_chat(user, span_warning("You cannot evolve when you are cuffed!"))
- return
-
+ return FALSE
if(larva.amount_grown < larva.max_grown)
- to_chat(user, span_warning("You are not fully grown!"))
- return
+ return FALSE
+ if(larva.movement_type & VENTCRAWLING)
+ return FALSE
- to_chat(larva, span_name("You are growing into a beautiful alien! It is time to choose a caste."))
- to_chat(larva, span_info("There are three to choose from:"))
- to_chat(larva, span_name("Hunters are the most agile caste, tasked with hunting for hosts. They are faster than a human and can even pounce, but are not much tougher than a drone."))
- to_chat(larva, span_name("Sentinelsare tasked with protecting the hive. With their ranged spit, invisibility, and high health, they make formidable guardians and acceptable secondhand hunters."))
- to_chat(larva, span_name("Dronesare the weakest and slowest of the castes, but can grow into a praetorian and then queen if no queen exists, and are vital to maintaining a hive with their resin secretion abilities."))
- var/alien_caste = tgui_input_list(larva, "Please choose which alien caste you shall belong to.",,list("Hunter","Sentinel","Drone"))
+ return TRUE
- if(larva.movement_type & VENTCRAWLING)
- to_chat(user, span_warning("You cannot evolve while ventcrawling!"))
+/datum/action/cooldown/alien/larva_evolve/Activate(atom/target)
+ var/mob/living/carbon/alien/larva/larva = owner
+ var/static/list/caste_options
+ if(!caste_options)
+ caste_options = list()
+
+ // This can probably be genericized in the future.
+ var/mob/hunter_path = /mob/living/carbon/alien/humanoid/hunter
+ var/datum/radial_menu_choice/hunter = new()
+ hunter.name = "Hunter"
+ hunter.image = image(icon = initial(hunter_path.icon), icon_state = initial(hunter_path.icon_state))
+ hunter.info = span_info("Hunters are the most agile caste, tasked with hunting for hosts. \
+ They are faster than a human and can even pounce, but are not much tougher than a drone.")
+
+ caste_options["Hunter"] = hunter
+
+ var/mob/sentinel_path = /mob/living/carbon/alien/humanoid/sentinel
+ var/datum/radial_menu_choice/sentinel = new()
+ sentinel.name = "Sentinel"
+ sentinel.image = image(icon = initial(sentinel_path.icon), icon_state = initial(sentinel_path.icon_state))
+ sentinel.info = span_info("Sentinels are tasked with protecting the hive. \
+ With their ranged spit, invisibility, and high health, they make formidable guardians \
+ and acceptable secondhand hunters.")
+
+ caste_options["Sentinel"] = sentinel
+
+ var/mob/drone_path = /mob/living/carbon/alien/humanoid/drone
+ var/datum/radial_menu_choice/drone = new()
+ drone.name = "Drone"
+ drone.image = image(icon = initial(drone_path.icon), icon_state = initial(drone_path.icon_state))
+ drone.info = span_info("Drones are the weakest and slowest of the castes, \
+ but can grow into a praetorian and then queen if no queen exists, \
+ and are vital to maintaining a hive with their resin secretion abilities.")
+
+ caste_options["Drone"] = drone
+
+ var/alien_caste = show_radial_menu(owner, owner, caste_options, radius = 38, require_near = TRUE, tooltips = TRUE)
+ if(QDELETED(src) || QDELETED(owner) || !IsAvailable() || !alien_caste)
return
- if(user.incapacitated()) //something happened to us while we were choosing.
+ if(alien_caste == null)
return
var/mob/living/carbon/alien/humanoid/new_xeno
@@ -62,7 +97,8 @@
new_xeno = new /mob/living/carbon/alien/humanoid/sentinel(larva.loc)
if("Drone")
new_xeno = new /mob/living/carbon/alien/humanoid/drone(larva.loc)
+ else
+ CRASH("Alien evolve was given an invalid / incorrect alien cast type. Got: [alien_caste]")
larva.alien_evolve(new_xeno)
- return
-
+ return TRUE
diff --git a/code/modules/mob/living/carbon/alien/organs.dm b/code/modules/mob/living/carbon/alien/organs.dm
index 16be0609d14cf..33cb540787afd 100644
--- a/code/modules/mob/living/carbon/alien/organs.dm
+++ b/code/modules/mob/living/carbon/alien/organs.dm
@@ -2,30 +2,6 @@
icon_state = "xgibmid2"
visual = FALSE
food_reagents = list(/datum/reagent/consumable/nutriment = 5, /datum/reagent/toxin/acid = 10)
- var/list/alien_powers = list()
-
-/obj/item/organ/internal/alien/Initialize(mapload)
- . = ..()
- for(var/A in alien_powers)
- if(ispath(A))
- alien_powers -= A
- alien_powers += new A(src)
-
-/obj/item/organ/internal/alien/Destroy()
- QDEL_LIST(alien_powers)
- return ..()
-
-/obj/item/organ/internal/alien/Insert(mob/living/carbon/M, special = 0)
- ..()
- for(var/obj/effect/proc_holder/alien/P in alien_powers)
- M.AddAbility(P)
-
-
-/obj/item/organ/internal/alien/Remove(mob/living/carbon/M, special = 0)
- for(var/obj/effect/proc_holder/alien/P in alien_powers)
- M.RemoveAbility(P)
- ..()
-
/obj/item/organ/internal/alien/plasmavessel
name = "plasma vessel"
@@ -33,11 +9,14 @@
w_class = WEIGHT_CLASS_NORMAL
zone = BODY_ZONE_CHEST
slot = ORGAN_SLOT_XENO_PLASMAVESSEL
- alien_powers = list(/obj/effect/proc_holder/alien/plant, /obj/effect/proc_holder/alien/transfer)
food_reagents = list(/datum/reagent/consumable/nutriment = 5, /datum/reagent/toxin/plasma = 10)
+ actions_types = list(
+ /datum/action/cooldown/alien/make_structure/plant_weeds,
+ /datum/action/cooldown/alien/transfer,
+ )
/// The current amount of stored plasma.
- var/storedPlasma = 100
+ var/stored_plasma = 100
/// The maximum plasma this organ can store.
var/max_plasma = 250
/// The rate this organ regenerates its owners health at per damage type per second.
@@ -49,7 +28,7 @@
name = "large plasma vessel"
icon_state = "plasma_large"
w_class = WEIGHT_CLASS_BULKY
- storedPlasma = 200
+ stored_plasma = 200
max_plasma = 500
plasma_rate = 7.5
@@ -60,7 +39,7 @@
name = "small plasma vessel"
icon_state = "plasma_small"
w_class = WEIGHT_CLASS_SMALL
- storedPlasma = 100
+ stored_plasma = 100
max_plasma = 150
plasma_rate = 2.5
@@ -69,7 +48,7 @@
icon_state = "plasma_tiny"
w_class = WEIGHT_CLASS_TINY
max_plasma = 100
- alien_powers = list(/obj/effect/proc_holder/alien/transfer)
+ actions_types = list(/datum/action/cooldown/alien/transfer)
/obj/item/organ/internal/alien/plasmavessel/on_life(delta_time, times_fired)
//If there are alien weeds on the ground then heal if needed or give some plasma
@@ -108,9 +87,9 @@
zone = BODY_ZONE_HEAD
slot = ORGAN_SLOT_XENO_HIVENODE
w_class = WEIGHT_CLASS_TINY
- ///Indicates if the queen died recently, aliens are heavily weakened while this is active.
+ actions_types = list(/datum/action/cooldown/alien/whisper)
+ /// Indicates if the queen died recently, aliens are heavily weakened while this is active.
var/recent_queen_death = FALSE
- alien_powers = list(/obj/effect/proc_holder/alien/whisper)
/obj/item/organ/internal/alien/hivenode/Insert(mob/living/carbon/M, special = 0)
..()
@@ -162,7 +141,7 @@
icon_state = "stomach-x"
zone = BODY_ZONE_PRECISE_MOUTH
slot = ORGAN_SLOT_XENO_RESINSPINNER
- alien_powers = list(/obj/effect/proc_holder/alien/resin)
+ actions_types = list(/datum/action/cooldown/alien/make_structure/resin)
/obj/item/organ/internal/alien/acid
@@ -170,7 +149,7 @@
icon_state = "acid"
zone = BODY_ZONE_PRECISE_MOUTH
slot = ORGAN_SLOT_XENO_ACIDGLAND
- alien_powers = list(/obj/effect/proc_holder/alien/acid)
+ actions_types = list(/datum/action/cooldown/alien/acid/corrosion)
/obj/item/organ/internal/alien/neurotoxin
@@ -178,7 +157,7 @@
icon_state = "neurotox"
zone = BODY_ZONE_PRECISE_MOUTH
slot = ORGAN_SLOT_XENO_NEUROTOXINGLAND
- alien_powers = list(/obj/effect/proc_holder/alien/neurotoxin)
+ actions_types = list(/datum/action/cooldown/alien/acid/neurotoxin)
/obj/item/organ/internal/alien/eggsac
@@ -187,4 +166,4 @@
zone = BODY_ZONE_PRECISE_GROIN
slot = ORGAN_SLOT_XENO_EGGSAC
w_class = WEIGHT_CLASS_BULKY
- alien_powers = list(/obj/effect/proc_holder/alien/lay_egg)
+ actions_types = list(/datum/action/cooldown/alien/make_structure/lay_egg)
diff --git a/code/modules/mob/living/carbon/alien/special/alien_embryo.dm b/code/modules/mob/living/carbon/alien/special/alien_embryo.dm
index c2eeb6947f969..85a8804eea2a0 100644
--- a/code/modules/mob/living/carbon/alien/special/alien_embryo.dm
+++ b/code/modules/mob/living/carbon/alien/special/alien_embryo.dm
@@ -60,7 +60,11 @@
return
if(++stage < 6)
INVOKE_ASYNC(src, .proc/RefreshInfectionImage)
- addtimer(CALLBACK(src, .proc/advance_embryo_stage), growth_time)
+ var/slowdown = 1
+ if(ishuman(owner))
+ var/mob/living/carbon/human/baby_momma = owner
+ slowdown = baby_momma.reagents.has_reagent(/datum/reagent/medicine/spaceacillin) ? 2 : 1 // spaceacillin doubles the time it takes to grow
+ addtimer(CALLBACK(src, .proc/advance_embryo_stage), growth_time*slowdown)
/obj/item/organ/internal/body_egg/alien_embryo/egg_process()
if(stage == 6 && prob(50))
diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm
index df647f214ef3c..8a6d125c7ea28 100644
--- a/code/modules/mob/living/carbon/carbon.dm
+++ b/code/modules/mob/living/carbon/carbon.dm
@@ -400,17 +400,13 @@
. = ..()
var/obj/item/organ/internal/alien/plasmavessel/vessel = getorgan(/obj/item/organ/internal/alien/plasmavessel)
if(vessel)
- . += "Plasma Stored: [vessel.storedPlasma]/[vessel.max_plasma]"
+ . += "Plasma Stored: [vessel.stored_plasma]/[vessel.max_plasma]"
var/obj/item/organ/internal/heart/vampire/darkheart = getorgan(/obj/item/organ/internal/heart/vampire)
if(darkheart)
. += "Current blood level: [blood_volume]/[BLOOD_VOLUME_MAXIMUM]."
if(locate(/obj/item/assembly/health) in src)
. += "Health: [health]"
-/mob/living/carbon/get_proc_holders()
- . = ..()
- . += add_abilities_to_panel()
-
/mob/living/carbon/attack_ui(slot, params)
if(!has_hand_for_held_index(active_hand_index))
return 0
@@ -572,7 +568,7 @@
if(!isnull(E.lighting_alpha))
lighting_alpha = E.lighting_alpha
- if(client.eye != src)
+ if(client.eye && client.eye != src)
var/atom/A = client.eye
if(A.update_remote_sight(src)) //returns 1 if we override all other sight updates.
return
diff --git a/code/modules/mob/living/carbon/carbon_movement.dm b/code/modules/mob/living/carbon/carbon_movement.dm
index 419c203b60cbb..dde0e0afe34fe 100644
--- a/code/modules/mob/living/carbon/carbon_movement.dm
+++ b/code/modules/mob/living/carbon/carbon_movement.dm
@@ -1,5 +1,5 @@
/mob/living/carbon/slip(knockdown_amount, obj/O, lube, paralyze, force_drop)
- if(movement_type & FLYING)
+ if(movement_type & (FLYING | FLOATING))
return FALSE
if(!(lube&SLIDE_ICE))
log_combat(src, (O ? O : get_turf(src)), "slipped on the", null, ((lube & SLIDE) ? "(LUBE)" : null))
diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm
index 44597895437c9..7875f8560dd91 100644
--- a/code/modules/mob/living/carbon/examine.dm
+++ b/code/modules/mob/living/carbon/examine.dm
@@ -6,7 +6,7 @@
var/t_has = p_have()
var/t_is = p_are()
- . = list("*---------*\nThis is [icon2html(src, user)] \a [src]!")
+ . = list("This is [icon2html(src, user)] \a [src]!>")
var/obscured = check_obscured_slots()
if (handcuffed)
@@ -150,7 +150,7 @@
. += "[t_He] look[p_s()] very happy."
if(MOOD_LEVEL_HAPPY4 to INFINITY)
. += "[t_He] look[p_s()] ecstatic."
- . += "*---------*"
+ . += ""
SEND_SIGNAL(src, COMSIG_PARENT_EXAMINE, user, .)
diff --git a/code/modules/mob/living/carbon/human/dummy.dm b/code/modules/mob/living/carbon/human/dummy.dm
index c8cc678f10d20..ae1dd2b4c8428 100644
--- a/code/modules/mob/living/carbon/human/dummy.dm
+++ b/code/modules/mob/living/carbon/human/dummy.dm
@@ -102,7 +102,8 @@ INITIALIZE_IMMEDIATE(/mob/living/carbon/human/dummy)
dna.features["snout"] = "Round"
dna.features["spines"] = "None"
dna.features["tail_cat"] = "None"
- dna.features["tail_lizard"] = "Light"
+ dna.features["tail_lizard"] = "Smooth"
+ dna.features["pod_hair"] = "Ivy"
//Inefficient pooling/caching way.
GLOBAL_LIST_EMPTY(human_dummy_list)
diff --git a/code/modules/mob/living/carbon/human/examine.dm b/code/modules/mob/living/carbon/human/examine.dm
index 6645cc24649de..ca9c81a284342 100644
--- a/code/modules/mob/living/carbon/human/examine.dm
+++ b/code/modules/mob/living/carbon/human/examine.dm
@@ -14,7 +14,7 @@
if(HAS_TRAIT(L, TRAIT_PROSOPAGNOSIA) || HAS_TRAIT(L, TRAIT_INVISIBLE_MAN))
obscure_name = TRUE
- . = list("*---------*\nThis is [!obscure_name ? name : "Unknown"]!")
+ . = list("This is [!obscure_name ? name : "Unknown"]!")
var/obscured = check_obscured_slots()
@@ -361,9 +361,9 @@
var/perpname = get_face_name(get_id_name(""))
if(perpname && (HAS_TRAIT(user, TRAIT_SECURITY_HUD) || HAS_TRAIT(user, TRAIT_MEDICAL_HUD)))
- var/datum/data/record/R = find_record("name", perpname, GLOB.data_core.general)
- if(R)
- . += "Rank: [R.fields["rank"]]\n\[Front photo\]\[Side photo\]"
+ var/datum/data/record/target_record = find_record("name", perpname, GLOB.data_core.general)
+ if(target_record)
+ . += "Rank: [target_record.fields["rank"]]\n\[Front photo\]\[Side photo\]"
if(HAS_TRAIT(user, TRAIT_MEDICAL_HUD))
var/cyberimp_detect
for(var/obj/item/organ/internal/cyberimp/CI in internal_organs)
@@ -372,34 +372,34 @@
if(cyberimp_detect)
. += "Detected cybernetic modifications:"
. += "[cyberimp_detect]"
- if(R)
- var/health_r = R.fields["p_stat"]
- . += "\[[health_r]\]"
- health_r = R.fields["m_stat"]
- . += "\[[health_r]\]"
- R = find_record("name", perpname, GLOB.data_core.medical)
- if(R)
- . += "\[Medical evaluation\] "
- . += "\[See quirks\]"
+ if(target_record)
+ var/health_r = target_record.fields["p_stat"]
+ . += "\[[health_r]\]"
+ health_r = target_record.fields["m_stat"]
+ . += "\[[health_r]\]"
+ target_record = find_record("name", perpname, GLOB.data_core.medical)
+ if(target_record)
+ . += "\[Medical evaluation\] "
+ . += "\[See quirks\]"
if(HAS_TRAIT(user, TRAIT_SECURITY_HUD))
if(!user.stat && user != src)
//|| !user.canmove || user.restrained()) Fluff: Sechuds have eye-tracking technology and sets 'arrest' to people that the wearer looks and blinks at.
var/criminal = "None"
- R = find_record("name", perpname, GLOB.data_core.security)
- if(R)
- criminal = R.fields["criminal"]
+ target_record = find_record("name", perpname, GLOB.data_core.security)
+ if(target_record)
+ criminal = target_record.fields["criminal"]
- . += "Criminal status:\[[criminal]\]"
- . += jointext(list("Security record:\[View\]",
- "\[Add citation\]",
- "\[Add crime\]",
- "\[View comment log\]",
- "\[Add comment\]"), "")
+ . += "Criminal status:\[[criminal]\]"
+ . += jointext(list("Security record:\[View\]",
+ "\[Add citation\]",
+ "\[Add crime\]",
+ "\[View comment log\]",
+ "\[Add comment\]"), "")
else if(isobserver(user))
. += span_info("Traits: [get_quirk_string(FALSE, CAT_QUIRK_ALL)]")
- . += "*---------*"
+ . += ""
SEND_SIGNAL(src, COMSIG_PARENT_EXAMINE, user, .)
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index 1dcc00a9eeeb6..72d2e338ad050 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -25,11 +25,11 @@
AddComponent(/datum/component/bloodysoles/feet)
AddElement(/datum/element/ridable, /datum/component/riding/creature/human)
AddElement(/datum/element/strippable, GLOB.strippable_human_items, /mob/living/carbon/human/.proc/should_strip)
- GLOB.human_list += src
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = .proc/on_entered,
)
AddElement(/datum/element/connect_loc, loc_connections)
+ GLOB.human_list += src
/mob/living/carbon/human/proc/setup_human_dna()
//initialize dna. for spawned humans; overwritten by other code
@@ -95,11 +95,6 @@
. += "Chemical Storage: [changeling.chem_charges]/[changeling.total_chem_storage]"
. += "Absorbed DNA: [changeling.absorbed_count]"
-// called when something steps onto a human
-/mob/living/carbon/human/proc/on_entered(datum/source, atom/movable/AM)
- SIGNAL_HANDLER
- spreadFire(AM)
-
/mob/living/carbon/human/reset_perspective(atom/new_eye, force_reset = FALSE)
if(dna?.species?.prevent_perspective_change && !force_reset) // This is in case a species needs to prevent perspective changes in certain cases, like Dullahans preventing perspective changes when they're looking through their head.
update_fullscreen()
@@ -118,38 +113,41 @@
if(href_list["hud"])
if(!ishuman(usr))
return
- var/mob/living/carbon/human/H = usr
+ var/mob/living/carbon/human/human_user = usr
var/perpname = get_face_name(get_id_name(""))
- if(!HAS_TRAIT(H, TRAIT_SECURITY_HUD) && !HAS_TRAIT(H, TRAIT_MEDICAL_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_SECURITY_HUD) && !HAS_TRAIT(human_user, TRAIT_MEDICAL_HUD))
return
- var/datum/data/record/R = find_record("name", perpname, GLOB.data_core.general)
+ if((text2num(href_list["examine_time"]) + 1 MINUTES) < world.time)
+ to_chat(human_user, "[span_notice("It's too late to use this now!")]")
+ return
+ var/datum/data/record/target_record = find_record("name", perpname, GLOB.data_core.general)
if(href_list["photo_front"] || href_list["photo_side"])
- if(!R)
+ if(!target_record)
return
- if(!H.canUseHUD())
+ if(!human_user.canUseHUD())
return
- if(!HAS_TRAIT(H, TRAIT_SECURITY_HUD) && !HAS_TRAIT(H, TRAIT_MEDICAL_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_SECURITY_HUD) && !HAS_TRAIT(human_user, TRAIT_MEDICAL_HUD))
return
- var/obj/item/photo/P = null
+ var/obj/item/photo/photo_from_record = null
if(href_list["photo_front"])
- P = R.fields["photo_front"]
+ photo_from_record = target_record.fields["photo_front"]
else if(href_list["photo_side"])
- P = R.fields["photo_side"]
- if(P)
- P.show(H)
+ photo_from_record = target_record.fields["photo_side"]
+ if(photo_from_record)
+ photo_from_record.show(human_user)
return
if(href_list["hud"] == "m")
- if(!HAS_TRAIT(H, TRAIT_MEDICAL_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_MEDICAL_HUD))
return
if(href_list["evaluation"])
if(!getBruteLoss() && !getFireLoss() && !getOxyLoss() && getToxLoss() < 20)
- to_chat(usr, "[span_notice("No external injuries detected.")] ")
+ to_chat(human_user, "[span_notice("No external injuries detected.")] ")
return
var/span = "notice"
var/status = ""
if(getBruteLoss())
- to_chat(usr, "Physical trauma analysis:")
+ to_chat(human_user, "Physical trauma analysis:")
for(var/X in bodyparts)
var/obj/item/bodypart/BP = X
var/brutedamage = BP.brute_dam
@@ -163,9 +161,9 @@
status = "sustained major trauma!"
span = "userdanger"
if(brutedamage)
- to_chat(usr, "[BP] appears to have [status]")
+ to_chat(human_user, "[BP] appears to have [status]")
if(getFireLoss())
- to_chat(usr, "Analysis of skin burns:")
+ to_chat(human_user, "Analysis of skin burns:")
for(var/X in bodyparts)
var/obj/item/bodypart/BP = X
var/burndamage = BP.burn_dam
@@ -179,122 +177,122 @@
status = "major burns!"
span = "userdanger"
if(burndamage)
- to_chat(usr, "[BP] appears to have [status]")
+ to_chat(human_user, "[BP] appears to have [status]")
if(getOxyLoss())
- to_chat(usr, span_danger("Patient has signs of suffocation, emergency treatment may be required!"))
+ to_chat(human_user, span_danger("Patient has signs of suffocation, emergency treatment may be required!"))
if(getToxLoss() > 20)
- to_chat(usr, span_danger("Gathered data is inconsistent with the analysis, possible cause: poisoning."))
- if(!H.wear_id) //You require access from here on out.
- to_chat(H, span_warning("ERROR: Invalid access"))
+ to_chat(human_user, span_danger("Gathered data is inconsistent with the analysis, possible cause: poisoning."))
+ if(!human_user.wear_id) //You require access from here on out.
+ to_chat(human_user, span_warning("ERROR: Invalid access"))
return
- var/list/access = H.wear_id.GetAccess()
+ var/list/access = human_user.wear_id.GetAccess()
if(!(ACCESS_MEDICAL in access))
- to_chat(H, span_warning("ERROR: Invalid access"))
+ to_chat(human_user, span_warning("ERROR: Invalid access"))
return
if(href_list["p_stat"])
- var/health_status = input(usr, "Specify a new physical status for this person.", "Medical HUD", R.fields["p_stat"]) in list("Active", "Physically Unfit", "*Unconscious*", "*Deceased*", "Cancel")
- if(!R)
+ var/health_status = input(human_user, "Specify a new physical status for this person.", "Medical HUD", target_record.fields["p_stat"]) in list("Active", "Physically Unfit", "*Unconscious*", "*Deceased*", "Cancel")
+ if(!target_record)
return
- if(!H.canUseHUD())
+ if(!human_user.canUseHUD())
return
- if(!HAS_TRAIT(H, TRAIT_MEDICAL_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_MEDICAL_HUD))
return
if(health_status && health_status != "Cancel")
- R.fields["p_stat"] = health_status
+ target_record.fields["p_stat"] = health_status
return
if(href_list["m_stat"])
- var/health_status = input(usr, "Specify a new mental status for this person.", "Medical HUD", R.fields["m_stat"]) in list("Stable", "*Watch*", "*Unstable*", "*Insane*", "Cancel")
- if(!R)
+ var/health_status = input(human_user, "Specify a new mental status for this person.", "Medical HUD", target_record.fields["m_stat"]) in list("Stable", "*Watch*", "*Unstable*", "*Insane*", "Cancel")
+ if(!target_record)
return
- if(!H.canUseHUD())
+ if(!human_user.canUseHUD())
return
- if(!HAS_TRAIT(H, TRAIT_MEDICAL_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_MEDICAL_HUD))
return
if(health_status && health_status != "Cancel")
- R.fields["m_stat"] = health_status
+ target_record.fields["m_stat"] = health_status
return
if(href_list["quirk"])
var/quirkstring = get_quirk_string(TRUE, CAT_QUIRK_ALL)
if(quirkstring)
- to_chat(usr, "Detected physiological traits:\n[quirkstring]")
+ to_chat(human_user, "Detected physiological traits:\n[quirkstring]")
else
- to_chat(usr, "No physiological traits found.")
+ to_chat(human_user, "No physiological traits found.")
return //Medical HUD ends here.
if(href_list["hud"] == "s")
- if(!HAS_TRAIT(H, TRAIT_SECURITY_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_SECURITY_HUD))
return
- if(usr.stat || usr == src) //|| !usr.canmove || usr.restrained()) Fluff: Sechuds have eye-tracking technology and sets 'arrest' to people that the wearer looks and blinks at.
+ if(human_user.stat || human_user == src) //|| !human_user.canmove || human_user.restrained()) Fluff: Sechuds have eye-tracking technology and sets 'arrest' to people that the wearer looks and blinks at.
return //Non-fluff: This allows sec to set people to arrest as they get disarmed or beaten
// Checks the user has security clearence before allowing them to change arrest status via hud, comment out to enable all access
var/allowed_access = null
- var/obj/item/clothing/glasses/hud/security/G = H.glasses
- if(istype(G) && (G.obj_flags & EMAGGED))
+ var/obj/item/clothing/glasses/hud/security/user_glasses = human_user.glasses
+ if(istype(user_glasses) && (user_glasses.obj_flags & EMAGGED))
allowed_access = "@%&ERROR_%$*"
else //Implant and standard glasses check access
- if(H.wear_id)
- var/list/access = H.wear_id.GetAccess()
+ if(human_user.wear_id)
+ var/list/access = human_user.wear_id.GetAccess()
if(ACCESS_SECURITY in access)
- allowed_access = H.get_authentification_name()
+ allowed_access = human_user.get_authentification_name()
if(!allowed_access)
- to_chat(H, span_warning("ERROR: Invalid access."))
+ to_chat(human_user, span_warning("ERROR: Invalid access."))
return
if(!perpname)
- to_chat(H, span_warning("ERROR: Can not identify target."))
+ to_chat(human_user, span_warning("ERROR: Can not identify target."))
return
- R = find_record("name", perpname, GLOB.data_core.security)
- if(!R)
- to_chat(usr, span_warning("ERROR: Unable to locate data core entry for target."))
+ target_record = find_record("name", perpname, GLOB.data_core.security)
+ if(!target_record)
+ to_chat(human_user, span_warning("ERROR: Unable to locate data core entry for target."))
return
if(href_list["status"])
- var/setcriminal = input(usr, "Specify a new criminal status for this person.", "Security HUD", R.fields["criminal"]) in list("None", "*Arrest*", "Incarcerated", "Suspected", "Paroled", "Discharged", "Cancel")
+ var/setcriminal = input(human_user, "Specify a new criminal status for this person.", "Security HUD", target_record.fields["criminal"]) in list("None", "*Arrest*", "Incarcerated", "Suspected", "Paroled", "Discharged", "Cancel")
if(setcriminal != "Cancel")
- if(!R)
+ if(!target_record)
return
- if(!H.canUseHUD())
+ if(!human_user.canUseHUD())
return
- if(!HAS_TRAIT(H, TRAIT_SECURITY_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_SECURITY_HUD))
return
- investigate_log("[key_name(src)] has been set from [R.fields["criminal"]] to [setcriminal] by [key_name(usr)].", INVESTIGATE_RECORDS)
- R.fields["criminal"] = setcriminal
+ investigate_log("[key_name(src)] has been set from [target_record.fields["criminal"]] to [setcriminal] by [key_name(human_user)].", INVESTIGATE_RECORDS)
+ target_record.fields["criminal"] = setcriminal
sec_hud_set_security_status()
return
if(href_list["view"])
- if(!H.canUseHUD())
+ if(!human_user.canUseHUD())
return
- if(!HAS_TRAIT(H, TRAIT_SECURITY_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_SECURITY_HUD))
return
- to_chat(usr, "Name: [R.fields["name"]] Criminal Status: [R.fields["criminal"]]")
- for(var/datum/data/crime/c in R.fields["crim"])
- to_chat(usr, "Crime: [c.crimeName]")
+ to_chat(human_user, "Name: [target_record.fields["name"]] Criminal Status: [target_record.fields["criminal"]]")
+ for(var/datum/data/crime/c in target_record.fields["crim"])
+ to_chat(human_user, "Crime: [c.crimeName]")
if (c.crimeDetails)
- to_chat(usr, "Details: [c.crimeDetails]")
+ to_chat(human_user, "Details: [c.crimeDetails]")
else
- to_chat(usr, "Details:\[Add details]")
- to_chat(usr, "Added by [c.author] at [c.time]")
- to_chat(usr, "----------")
- to_chat(usr, "Notes: [R.fields["notes"]]")
+ to_chat(human_user, "Details:\[Add details]")
+ to_chat(human_user, "Added by [c.author] at [c.time]")
+ to_chat(human_user, "----------")
+ to_chat(human_user, "Notes: [target_record.fields["notes"]]")
return
if(href_list["add_citation"])
var/maxFine = CONFIG_GET(number/maxfine)
- var/t1 = tgui_input_text(usr, "Citation crime", "Security HUD")
- var/fine = tgui_input_number(usr, "Citation fine", "Security HUD", 50, maxFine, 5)
+ var/t1 = tgui_input_text(human_user, "Citation crime", "Security HUD")
+ var/fine = tgui_input_number(human_user, "Citation fine", "Security HUD", 50, maxFine, 5)
if(!fine)
return
- if(!R || !t1 || !allowed_access)
+ if(!target_record || !t1 || !allowed_access)
return
- if(!H.canUseHUD())
+ if(!human_user.canUseHUD())
return
- if(!HAS_TRAIT(H, TRAIT_SECURITY_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_SECURITY_HUD))
return
var/datum/data/crime/crime = GLOB.data_core.createCrimeEntry(t1, "", allowed_access, station_time_timestamp(), fine)
for (var/obj/item/modular_computer/tablet in GLOB.TabletMessengers)
- if(tablet.saved_identification == R.fields["name"])
+ if(tablet.saved_identification == target_record.fields["name"])
var/message = "You have been fined [fine] credits for '[t1]'. Fines may be paid at security."
var/datum/signal/subspace/messaging/tablet_msg/signal = new(src, list(
"name" = "Security Citation",
@@ -304,70 +302,74 @@
"automated" = TRUE
))
signal.send_to_receivers()
- usr.log_message("(PDA: Citation Server) sent \"[message]\" to [signal.format_target()]", LOG_PDA)
- GLOB.data_core.addCitation(R.fields["id"], crime)
- investigate_log("New Citation: [t1] Fine: [fine] | Added to [R.fields["name"]] by [key_name(usr)]", INVESTIGATE_RECORDS)
- SSblackbox.ReportCitation(crime.dataId, usr.ckey, usr.real_name, R.fields["name"], t1, fine)
+ human_user.log_message("(PDA: Citation Server) sent \"[message]\" to [signal.format_target()]", LOG_PDA)
+ GLOB.data_core.addCitation(target_record.fields["id"], crime)
+ investigate_log("New Citation: [t1] Fine: [fine] | Added to [target_record.fields["name"]] by [key_name(human_user)]", INVESTIGATE_RECORDS)
+ SSblackbox.ReportCitation(crime.dataId, human_user.ckey, human_user.real_name, target_record.fields["name"], t1, fine)
return
if(href_list["add_crime"])
- var/t1 = tgui_input_text(usr, "Crime name", "Security HUD")
- if(!R || !t1 || !allowed_access)
+ var/t1 = tgui_input_text(human_user, "Crime name", "Security HUD")
+ if(!target_record || !t1 || !allowed_access)
return
- if(!H.canUseHUD())
+ if(!human_user.canUseHUD())
return
- if(!HAS_TRAIT(H, TRAIT_SECURITY_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_SECURITY_HUD))
return
var/crime = GLOB.data_core.createCrimeEntry(t1, null, allowed_access, station_time_timestamp())
- GLOB.data_core.addCrime(R.fields["id"], crime)
- investigate_log("New Crime: [t1] | Added to [R.fields["name"]] by [key_name(usr)]", INVESTIGATE_RECORDS)
- to_chat(usr, span_notice("Successfully added a crime."))
+ GLOB.data_core.addCrime(target_record.fields["id"], crime)
+ investigate_log("New Crime: [t1] | Added to [target_record.fields["name"]] by [key_name(human_user)]", INVESTIGATE_RECORDS)
+ to_chat(human_user, span_notice("Successfully added a crime."))
return
if(href_list["add_details"])
- var/t1 = tgui_input_text(usr, "Crime details", "Security Records", multiline = TRUE)
- if(!R || !t1 || !allowed_access)
+ var/t1 = tgui_input_text(human_user, "Crime details", "Security Records", multiline = TRUE)
+ if(!target_record || !t1 || !allowed_access)
return
- if(!H.canUseHUD())
+ if(!human_user.canUseHUD())
return
- if(!HAS_TRAIT(H, TRAIT_SECURITY_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_SECURITY_HUD))
return
if(href_list["cdataid"])
- GLOB.data_core.addCrimeDetails(R.fields["id"], href_list["cdataid"], t1)
- investigate_log("New Crime details: [t1] | Added to [R.fields["name"]] by [key_name(usr)]", INVESTIGATE_RECORDS)
- to_chat(usr, span_notice("Successfully added details."))
+ GLOB.data_core.addCrimeDetails(target_record.fields["id"], href_list["cdataid"], t1)
+ investigate_log("New Crime details: [t1] | Added to [target_record.fields["name"]] by [key_name(human_user)]", INVESTIGATE_RECORDS)
+ to_chat(human_user, span_notice("Successfully added details."))
return
if(href_list["view_comment"])
- if(!H.canUseHUD())
+ if(!human_user.canUseHUD())
return
- if(!HAS_TRAIT(H, TRAIT_SECURITY_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_SECURITY_HUD))
return
- to_chat(usr, "Comments/Log:")
+ to_chat(human_user, "Comments/Log:")
var/counter = 1
- while(R.fields[text("com_[]", counter)])
- to_chat(usr, R.fields[text("com_[]", counter)])
- to_chat(usr, "----------")
+ while(target_record.fields[text("com_[]", counter)])
+ to_chat(human_user, target_record.fields[text("com_[]", counter)])
+ to_chat(human_user, "----------")
counter++
return
if(href_list["add_comment"])
- var/t1 = tgui_input_text(usr, "Add a comment", "Security Records", multiline = TRUE)
- if (!R || !t1 || !allowed_access)
+ var/t1 = tgui_input_text(human_user, "Add a comment", "Security Records", multiline = TRUE)
+ if (!target_record || !t1 || !allowed_access)
return
- if(!H.canUseHUD())
+ if(!human_user.canUseHUD())
return
- if(!HAS_TRAIT(H, TRAIT_SECURITY_HUD))
+ if(!HAS_TRAIT(human_user, TRAIT_SECURITY_HUD))
return
var/counter = 1
- while(R.fields[text("com_[]", counter)])
+ while(target_record.fields[text("com_[]", counter)])
counter++
- R.fields[text("com_[]", counter)] = text("Made by [] on [] [], [] []", allowed_access, station_time_timestamp(), time2text(world.realtime, "MMM DD"), GLOB.year_integer+540, t1)
- to_chat(usr, span_notice("Successfully added comment."))
+ target_record.fields[text("com_[]", counter)] = text("Made by [] on [] [], [] []", allowed_access, station_time_timestamp(), time2text(world.realtime, "MMM DD"), GLOB.year_integer+540, t1)
+ to_chat(human_user, span_notice("Successfully added comment."))
return
..() //end of this massive fucking chain. TODO: make the hud chain not spooky. - Yeah, great job doing that.
+//called when something steps onto a human
+/mob/living/carbon/human/proc/on_entered(datum/source, atom/movable/AM)
+ SIGNAL_HANDLER
+ spreadFire(AM)
/mob/living/carbon/human/proc/canUseHUD()
return (mobility_flags & MOBILITY_USE)
@@ -772,9 +774,6 @@
return FALSE
return ..()
-/mob/living/carbon/human/is_literate()
- return !HAS_TRAIT(src, TRAIT_ILLITERATE)
-
/mob/living/carbon/human/vomit(lost_nutrition = 10, blood = FALSE, stun = TRUE, distance = 1, message = TRUE, vomit_type = VOMIT_TOXIC, harm = TRUE, force = FALSE, purge_ratio = 0.1)
if(blood && (NOBLOOD in dna.species.species_traits) && !HAS_TRAIT(src, TRAIT_TOXINLOVER))
if(message)
diff --git a/code/modules/mob/living/carbon/human/human_defense.dm b/code/modules/mob/living/carbon/human/human_defense.dm
index a6e6621b30442..d6246f20e6bab 100644
--- a/code/modules/mob/living/carbon/human/human_defense.dm
+++ b/code/modules/mob/living/carbon/human/human_defense.dm
@@ -709,8 +709,9 @@
return
var/list/combined_msg = list()
- visible_message(span_notice("[src] examines [p_them()]self."), \
- span_notice("You check yourself for injuries."))
+ visible_message(span_notice("[src] examines [p_them()]self."))
+
+ combined_msg += span_notice("You check yourself for injuries.")
var/list/missing = list(BODY_ZONE_HEAD, BODY_ZONE_CHEST, BODY_ZONE_L_ARM, BODY_ZONE_R_ARM, BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
@@ -894,7 +895,7 @@
if(quirks.len)
combined_msg += span_notice("You have these quirks: [get_quirk_string(FALSE, CAT_QUIRK_ALL)].")
- to_chat(src, combined_msg.Join("\n"))
+ to_chat(src, examine_block(combined_msg.Join("\n")))
/mob/living/carbon/human/damage_clothes(damage_amount, damage_type = BRUTE, damage_flag = 0, def_zone)
if(damage_type != BRUTE && damage_type != BURN)
diff --git a/code/modules/mob/living/carbon/human/human_say.dm b/code/modules/mob/living/carbon/human/human_say.dm
index 5d27c1bcfa0e0..f128edc322dd4 100644
--- a/code/modules/mob/living/carbon/human/human_say.dm
+++ b/code/modules/mob/living/carbon/human/human_say.dm
@@ -84,4 +84,4 @@
/mob/living/carbon/human/get_alt_name()
if(name != GetVoice())
- return " (as [get_id_name("Unknown")])"\
+ return " (as [get_id_name("Unknown")])"
diff --git a/code/modules/mob/living/carbon/human/human_update_icons.dm b/code/modules/mob/living/carbon/human/human_update_icons.dm
index cad78f2554a94..23c254ffa9506 100644
--- a/code/modules/mob/living/carbon/human/human_update_icons.dm
+++ b/code/modules/mob/living/carbon/human/human_update_icons.dm
@@ -154,6 +154,13 @@ There are several things that need to be remembered:
var/mutable_appearance/uniform_overlay
+ //This is how non-humanoid clothing works. You check if the mob has the right bodyflag, and the clothing has the corresponding clothing flag.
+ //handled_by_bodytype is used to track whether or not we successfully used an alternate sprite. It's set to TRUE to ease up on copy-paste.
+ //icon_file MUST be set to null by default, or it causes issues.
+ //handled_by_bodytype MUST be set to FALSE under the if(!icon_exists()) statement, or everything breaks.
+ //"override_file = handled_by_bodytype ? icon_file : null" MUST be added to the arguments of build_worn_icon()
+ //Friendly reminder that icon_exists(file, state, scream = TRUE) is your friend when debugging this code.
+ var/handled_by_bodytype = TRUE
var/icon_file
var/woman
if(!uniform_overlay)
@@ -167,6 +174,8 @@ There are several things that need to be remembered:
if(!icon_exists(icon_file, RESOLVE_ICON_STATE(uniform)))
icon_file = DEFAULT_UNIFORM_FILE
+ handled_by_bodytype = FALSE
+
//END SPECIES HANDLING
uniform_overlay = uniform.build_worn_icon(
default_layer = UNIFORM_LAYER,
@@ -174,6 +183,7 @@ There are several things that need to be remembered:
isinhands = FALSE,
female_uniform = woman ? uniform.female_sprite_flags : null,
override_state = target_overlay,
+ override_file = handled_by_bodytype ? icon_file : null,
)
if(OFFSET_UNIFORM in dna.species.offset_features)
@@ -677,7 +687,7 @@ There are several things that need to be remembered:
/mob/living/carbon/human/proc/update_hud_s_store(obj/item/worn_item)
worn_item.screen_loc = ui_sstore1
- if((client && hud_used) && (hud_used.inventory_shown && hud_used.hud_shown))
+ if(client && hud_used?.hud_shown)
client.screen += worn_item
update_observer_view(worn_item,TRUE)
diff --git a/code/modules/mob/living/carbon/human/inventory.dm b/code/modules/mob/living/carbon/human/inventory.dm
index 36765de0ba2c6..a88896969b79d 100644
--- a/code/modules/mob/living/carbon/human/inventory.dm
+++ b/code/modules/mob/living/carbon/human/inventory.dm
@@ -1,19 +1,8 @@
/mob/living/carbon/human/can_equip(obj/item/I, slot, disable_warning = FALSE, bypass_equip_delay_self = FALSE)
return dna.species.can_equip(I, slot, disable_warning, src, bypass_equip_delay_self)
-// Return the item currently in the slot ID
/mob/living/carbon/human/get_item_by_slot(slot_id)
switch(slot_id)
- if(ITEM_SLOT_BACK)
- return back
- if(ITEM_SLOT_MASK)
- return wear_mask
- if(ITEM_SLOT_NECK)
- return wear_neck
- if(ITEM_SLOT_HANDCUFFED)
- return handcuffed
- if(ITEM_SLOT_LEGCUFFED)
- return legcuffed
if(ITEM_SLOT_BELT)
return belt
if(ITEM_SLOT_ID)
@@ -24,8 +13,6 @@
return glasses
if(ITEM_SLOT_GLOVES)
return gloves
- if(ITEM_SLOT_HEAD)
- return head
if(ITEM_SLOT_FEET)
return shoes
if(ITEM_SLOT_OCLOTHING)
@@ -38,7 +25,47 @@
return r_store
if(ITEM_SLOT_SUITSTORE)
return s_store
- return null
+
+ return ..()
+
+/mob/living/carbon/human/get_slot_by_item(obj/item/looking_for)
+ if(looking_for == belt)
+ return ITEM_SLOT_BELT
+
+ if(looking_for == wear_id)
+ return ITEM_SLOT_ID
+
+ if(looking_for == ears)
+ return ITEM_SLOT_EARS
+
+ if(looking_for == glasses)
+ return ITEM_SLOT_EYES
+
+ if(looking_for == gloves)
+ return ITEM_SLOT_GLOVES
+
+ if(looking_for == head)
+ return ITEM_SLOT_HEAD
+
+ if(looking_for == shoes)
+ return ITEM_SLOT_FEET
+
+ if(looking_for == wear_suit)
+ return ITEM_SLOT_OCLOTHING
+
+ if(looking_for == w_uniform)
+ return ITEM_SLOT_ICLOTHING
+
+ if(looking_for == r_store)
+ return ITEM_SLOT_RPOCKET
+
+ if(looking_for == l_store)
+ return ITEM_SLOT_LPOCKET
+
+ if(looking_for == s_store)
+ return ITEM_SLOT_SUITSTORE
+
+ return ..()
/mob/living/carbon/human/get_all_worn_items()
. = get_head_slots() | get_body_slots()
diff --git a/code/modules/mob/living/carbon/human/login.dm b/code/modules/mob/living/carbon/human/login.dm
index b66d842fd10e8..dc2e5e70546e4 100644
--- a/code/modules/mob/living/carbon/human/login.dm
+++ b/code/modules/mob/living/carbon/human/login.dm
@@ -7,7 +7,6 @@
return
var/list/print_msg = list()
- print_msg += span_info("*---------*")
print_msg += span_userdanger("As you snap back to consciousness, you recall people messing with your stuff...")
afk_thefts = reverse_range(afk_thefts)
@@ -28,7 +27,6 @@
if(LAZYLEN(afk_thefts) >= AFK_THEFT_MAX_MESSAGES)
print_msg += span_warning("There may have been more, but that's all you can remember...")
- print_msg += span_info("*---------*")
- to_chat(src, print_msg.Join("\n"))
+ to_chat(src, examine_block(print_msg.Join("\n")))
LAZYNULL(afk_thefts)
diff --git a/code/modules/mob/living/carbon/human/monkey/monkey.dm b/code/modules/mob/living/carbon/human/monkey/monkey.dm
index a4ce98269e53f..b0cccf6618d8a 100644
--- a/code/modules/mob/living/carbon/human/monkey/monkey.dm
+++ b/code/modules/mob/living/carbon/human/monkey/monkey.dm
@@ -24,10 +24,13 @@
/mob/living/carbon/human/species/monkey/angry/Initialize(mapload)
. = ..()
if(prob(10))
- var/obj/item/clothing/head/helmet/justice/escape/helmet = new(src)
- equip_to_slot_or_del(helmet,ITEM_SLOT_HEAD)
- helmet.attack_self(src) // todo encapsulate toggle
+ INVOKE_ASYNC(src, .proc/give_ape_escape_helmet)
+/// Gives our funny monkey an Ape Escape hat reference
+/mob/living/carbon/human/species/monkey/angry/proc/give_ape_escape_helmet()
+ var/obj/item/clothing/head/helmet/justice/escape/helmet = new(src)
+ equip_to_slot_or_del(helmet, ITEM_SLOT_HEAD)
+ helmet.attack_self(src) // todo encapsulate toggle
GLOBAL_DATUM(the_one_and_only_punpun, /mob/living/carbon/human/species/monkey/punpun)
diff --git a/code/modules/mob/living/carbon/human/species.dm b/code/modules/mob/living/carbon/human/species.dm
index a5996aec183d0..f952fb621e905 100644
--- a/code/modules/mob/living/carbon/human/species.dm
+++ b/code/modules/mob/living/carbon/human/species.dm
@@ -173,7 +173,7 @@ GLOBAL_LIST_EMPTY(features_by_species)
///Species-only traits. Can be found in [code/__DEFINES/DNA.dm]
var/list/species_traits = list()
///Generic traits tied to having the species.
- var/list/inherent_traits = list(TRAIT_ADVANCEDTOOLUSER, TRAIT_CAN_STRIP)
+ var/list/inherent_traits = list(TRAIT_ADVANCEDTOOLUSER, TRAIT_CAN_STRIP, TRAIT_LITERATE)
/// List of biotypes the mob belongs to. Used by diseases.
var/inherent_biotypes = MOB_ORGANIC|MOB_HUMANOID
///List of factions the mob gain upon gaining this species.
@@ -1120,10 +1120,14 @@ GLOBAL_LIST_EMPTY(features_by_species)
var/attack_direction = get_dir(user, target)
if(atk_effect == ATTACK_EFFECT_KICK)//kicks deal 1.5x raw damage
target.apply_damage(damage*1.5, user.dna.species.attack_type, affecting, armor_block, attack_direction = attack_direction)
+ if((damage * 1.5) >= 9)
+ target.force_say()
log_combat(user, target, "kicked")
else//other attacks deal full raw damage + 1.5x in stamina damage
target.apply_damage(damage, user.dna.species.attack_type, affecting, armor_block, attack_direction = attack_direction)
target.apply_damage(damage*1.5, STAMINA, affecting, armor_block)
+ if(damage >= 9)
+ target.force_say()
log_combat(user, target, "punched")
if((target.stat != DEAD) && damage >= user.dna.species.punchstunthreshold)
@@ -1157,126 +1161,133 @@ GLOBAL_LIST_EMPTY(features_by_species)
/datum/species/proc/spec_hitby(atom/movable/AM, mob/living/carbon/human/H)
return
-/datum/species/proc/spec_attack_hand(mob/living/carbon/human/M, mob/living/carbon/human/H, datum/martial_art/attacker_style, modifiers)
- if(!istype(M))
+/datum/species/proc/spec_attack_hand(mob/living/carbon/human/owner, mob/living/carbon/human/target, datum/martial_art/attacker_style, modifiers)
+ if(!istype(owner))
return
- CHECK_DNA_AND_SPECIES(M)
- CHECK_DNA_AND_SPECIES(H)
+ CHECK_DNA_AND_SPECIES(owner)
+ CHECK_DNA_AND_SPECIES(target)
- if(!istype(M)) //sanity check for drones.
+ if(!istype(owner)) //sanity check for drones.
return
- if(M.mind)
- attacker_style = M.mind.martial_art
- if((M != H) && M.combat_mode && H.check_shields(M, 0, M.name, attack_type = UNARMED_ATTACK))
- log_combat(M, H, "attempted to touch")
- H.visible_message(span_warning("[M] attempts to touch [H]!"), \
- span_danger("[M] attempts to touch you!"), span_hear("You hear a swoosh!"), COMBAT_MESSAGE_RANGE, M)
- to_chat(M, span_warning("You attempt to touch [H]!"))
+ if(owner.mind)
+ attacker_style = owner.mind.martial_art
+ if((owner != target) && owner.combat_mode && target.check_shields(owner, 0, owner.name, attack_type = UNARMED_ATTACK))
+ log_combat(owner, target, "attempted to touch")
+ target.visible_message(span_warning("[owner] attempts to touch [target]!"), \
+ span_danger("[owner] attempts to touch you!"), span_hear("You hear a swoosh!"), COMBAT_MESSAGE_RANGE, owner)
+ to_chat(owner, span_warning("You attempt to touch [target]!"))
return
- SEND_SIGNAL(M, COMSIG_MOB_ATTACK_HAND, M, H, attacker_style)
+ SEND_SIGNAL(owner, COMSIG_MOB_ATTACK_HAND, owner, target, attacker_style)
if(LAZYACCESS(modifiers, RIGHT_CLICK))
- disarm(M, H, attacker_style)
+ disarm(owner, target, attacker_style)
return // dont attack after
- if(M.combat_mode)
- harm(M, H, attacker_style)
+ if(owner.combat_mode)
+ harm(owner, target, attacker_style)
else
- help(M, H, attacker_style)
+ help(owner, target, attacker_style)
-/datum/species/proc/spec_attacked_by(obj/item/I, mob/living/user, obj/item/bodypart/affecting, mob/living/carbon/human/H)
+/datum/species/proc/spec_attacked_by(obj/item/weapon, mob/living/user, obj/item/bodypart/affecting, mob/living/carbon/human/human)
// Allows you to put in item-specific reactions based on species
- if(user != H)
- if(H.check_shields(I, I.force, "the [I.name]", MELEE_ATTACK, I.armour_penetration))
+ if(user != human)
+ if(human.check_shields(weapon, weapon.force, "the [weapon.name]", MELEE_ATTACK, weapon.armour_penetration))
return FALSE
- if(H.check_block())
- H.visible_message(span_warning("[H] blocks [I]!"), \
- span_userdanger("You block [I]!"))
+ if(human.check_block())
+ human.visible_message(span_warning("[human] blocks [weapon]!"), \
+ span_userdanger("You block [weapon]!"))
return FALSE
var/hit_area
if(!affecting) //Something went wrong. Maybe the limb is missing?
- affecting = H.bodyparts[1]
+ affecting = human.bodyparts[1]
hit_area = affecting.plaintext_zone
var/def_zone = affecting.body_zone
- var/armor_block = H.run_armor_check(affecting, MELEE, span_notice("Your armor has protected your [hit_area]!"), span_warning("Your armor has softened a hit to your [hit_area]!"),I.armour_penetration, weak_against_armour = I.weak_against_armour)
+ var/armor_block = human.run_armor_check(affecting, MELEE, span_notice("Your armor has protected your [hit_area]!"), span_warning("Your armor has softened a hit to your [hit_area]!"),weapon.armour_penetration, weak_against_armour = weapon.weak_against_armour)
armor_block = min(ARMOR_MAX_BLOCK, armor_block) //cap damage reduction at 90%
- var/Iwound_bonus = I.wound_bonus
+ var/Iwound_bonus = weapon.wound_bonus
// this way, you can't wound with a surgical tool on help intent if they have a surgery active and are lying down, so a misclick with a circular saw on the wrong limb doesn't bleed them dry (they still get hit tho)
- if((I.item_flags & SURGICAL_TOOL) && !user.combat_mode && H.body_position == LYING_DOWN && (LAZYLEN(H.surgeries) > 0))
+ if((weapon.item_flags & SURGICAL_TOOL) && !user.combat_mode && human.body_position == LYING_DOWN && (LAZYLEN(human.surgeries) > 0))
Iwound_bonus = CANT_WOUND
- var/weakness = check_species_weakness(I, user)
+ var/weakness = check_species_weakness(weapon, user)
- H.send_item_attack_message(I, user, hit_area, affecting)
+ human.send_item_attack_message(weapon, user, hit_area, affecting)
- var/attack_direction = get_dir(user, H)
- apply_damage(I.force * weakness, I.damtype, def_zone, armor_block, H, wound_bonus = Iwound_bonus, bare_wound_bonus = I.bare_wound_bonus, sharpness = I.get_sharpness(), attack_direction = attack_direction)
+ var/attack_direction = get_dir(user, human)
+ apply_damage(weapon.force * weakness, weapon.damtype, def_zone, armor_block, human, wound_bonus = Iwound_bonus, bare_wound_bonus = weapon.bare_wound_bonus, sharpness = weapon.get_sharpness(), attack_direction = attack_direction)
- if(!I.force)
+ if(!weapon.force)
return FALSE //item force is zero
-
var/bloody = FALSE
- if(((I.damtype == BRUTE) && I.force && prob(25 + (I.force * 2))))
- if(IS_ORGANIC_LIMB(affecting))
- I.add_mob_blood(H) //Make the weapon bloody, not the person.
- if(prob(I.force * 2)) //blood spatter!
- bloody = TRUE
- var/turf/location = H.loc
- if(istype(location))
- H.add_splatter_floor(location)
- if(get_dist(user, H) <= 1) //people with TK won't get smeared with blood
- user.add_mob_blood(H)
-
- switch(hit_area)
- if(BODY_ZONE_HEAD)
- if(!I.get_sharpness() && armor_block < 50)
- if(prob(I.force))
- H.adjustOrganLoss(ORGAN_SLOT_BRAIN, 20)
- if(H.stat == CONSCIOUS)
- H.visible_message(span_danger("[H] is knocked senseless!"), \
- span_userdanger("You're knocked senseless!"))
- H.set_timed_status_effect(20 SECONDS, /datum/status_effect/confusion, only_if_higher = TRUE)
- H.adjust_blurriness(10)
- if(prob(10))
- H.gain_trauma(/datum/brain_trauma/mild/concussion)
- else
- H.adjustOrganLoss(ORGAN_SLOT_BRAIN, I.force * 0.2)
-
- if(H.mind && H.stat == CONSCIOUS && H != user && prob(I.force + ((100 - H.health) * 0.5))) // rev deconversion through blunt trauma.
- var/datum/antagonist/rev/rev = H.mind.has_antag_datum(/datum/antagonist/rev)
- if(rev)
- rev.remove_revolutionary(FALSE, user)
-
- if(bloody) //Apply blood
- if(H.wear_mask)
- H.wear_mask.add_mob_blood(H)
- H.update_inv_wear_mask()
- if(H.head)
- H.head.add_mob_blood(H)
- H.update_inv_head()
- if(H.glasses && prob(33))
- H.glasses.add_mob_blood(H)
- H.update_inv_glasses()
-
- if(BODY_ZONE_CHEST)
- if(H.stat == CONSCIOUS && !I.get_sharpness() && armor_block < 50)
- if(prob(I.force))
- H.visible_message(span_danger("[H] is knocked down!"), \
- span_userdanger("You're knocked down!"))
- H.apply_effect(60, EFFECT_KNOCKDOWN, armor_block)
-
- if(bloody)
- if(H.wear_suit)
- H.wear_suit.add_mob_blood(H)
- H.update_inv_wear_suit()
- if(H.w_uniform)
- H.w_uniform.add_mob_blood(H)
- H.update_inv_w_uniform()
+ if(weapon.damtype != BRUTE)
+ return TRUE
+ if(!(prob(25 + (weapon.force * 2))))
+ return TRUE
+
+ if(IS_ORGANIC_LIMB(affecting))
+ weapon.add_mob_blood(human) //Make the weapon bloody, not the person.
+ if(prob(weapon.force * 2)) //blood spatter!
+ bloody = TRUE
+ var/turf/location = human.loc
+ if(istype(location))
+ human.add_splatter_floor(location)
+ if(get_dist(user, human) <= 1) //people with TK won't get smeared with blood
+ user.add_mob_blood(human)
+
+ switch(hit_area)
+ if(BODY_ZONE_HEAD)
+ if(!weapon.get_sharpness() && armor_block < 50)
+ if(prob(weapon.force))
+ human.adjustOrganLoss(ORGAN_SLOT_BRAIN, 20)
+ if(human.stat == CONSCIOUS)
+ human.visible_message(span_danger("[human] is knocked senseless!"), \
+ span_userdanger("You're knocked senseless!"))
+ human.set_timed_status_effect(20 SECONDS, /datum/status_effect/confusion, only_if_higher = TRUE)
+ human.adjust_blurriness(10)
+ if(prob(10))
+ human.gain_trauma(/datum/brain_trauma/mild/concussion)
+ else
+ human.adjustOrganLoss(ORGAN_SLOT_BRAIN, weapon.force * 0.2)
+
+ if(human.mind && human.stat == CONSCIOUS && human != user && prob(weapon.force + ((100 - human.health) * 0.5))) // rev deconversion through blunt trauma.
+ var/datum/antagonist/rev/rev = human.mind.has_antag_datum(/datum/antagonist/rev)
+ if(rev)
+ rev.remove_revolutionary(FALSE, user)
+
+ if(bloody) //Apply blood
+ if(human.wear_mask)
+ human.wear_mask.add_mob_blood(human)
+ human.update_inv_wear_mask()
+ if(human.head)
+ human.head.add_mob_blood(human)
+ human.update_inv_head()
+ if(human.glasses && prob(33))
+ human.glasses.add_mob_blood(human)
+ human.update_inv_glasses()
+
+ if(BODY_ZONE_CHEST)
+ if(human.stat == CONSCIOUS && !weapon.get_sharpness() && armor_block < 50)
+ if(prob(weapon.force))
+ human.visible_message(span_danger("[human] is knocked down!"), \
+ span_userdanger("You're knocked down!"))
+ human.apply_effect(60, EFFECT_KNOCKDOWN, armor_block)
+
+ if(bloody)
+ if(human.wear_suit)
+ human.wear_suit.add_mob_blood(human)
+ human.update_inv_wear_suit()
+ if(human.w_uniform)
+ human.w_uniform.add_mob_blood(human)
+ human.update_inv_w_uniform()
+
+ /// Triggers force say events
+ if(weapon.force > 10 || weapon.force >= 5 && prob(33))
+ human.force_say(user)
return TRUE
diff --git a/code/modules/mob/living/carbon/human/species_types/abductors.dm b/code/modules/mob/living/carbon/human/species_types/abductors.dm
index 056f13623c892..f3a8f04c5a2ba 100644
--- a/code/modules/mob/living/carbon/human/species_types/abductors.dm
+++ b/code/modules/mob/living/carbon/human/species_types/abductors.dm
@@ -7,10 +7,11 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_VIRUSIMMUNE,
TRAIT_CHUNKYFINGERS,
- TRAIT_NOHUNGER,
TRAIT_NOBREATH,
+ TRAIT_NOHUNGER,
+ TRAIT_LITERATE,
+ TRAIT_VIRUSIMMUNE,
)
mutanttongue = /obj/item/organ/internal/tongue/abductor
changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC | RACE_SWAP | ERT_SPAWN | SLIME_EXTRACT
diff --git a/code/modules/mob/living/carbon/human/species_types/android.dm b/code/modules/mob/living/carbon/human/species_types/android.dm
index 7c1f24f696894..14846670a23ee 100644
--- a/code/modules/mob/living/carbon/human/species_types/android.dm
+++ b/code/modules/mob/living/carbon/human/species_types/android.dm
@@ -6,21 +6,22 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_NOMETABOLISM,
- TRAIT_TOXIMMUNE,
- TRAIT_RESISTHEAT,
- TRAIT_NOBREATH,
- TRAIT_RESISTCOLD,
- TRAIT_RESISTHIGHPRESSURE,
- TRAIT_RESISTLOWPRESSURE,
- TRAIT_RADIMMUNE,
+ TRAIT_CAN_USE_FLIGHT_POTION,
TRAIT_GENELESS,
- TRAIT_NOFIRE,
- TRAIT_PIERCEIMMUNE,
- TRAIT_NOHUNGER,
TRAIT_LIMBATTACHMENT,
+ TRAIT_LITERATE,
+ TRAIT_NOBREATH,
TRAIT_NOCLONELOSS,
- TRAIT_CAN_USE_FLIGHT_POTION,
+ TRAIT_NOFIRE,
+ TRAIT_NOHUNGER,
+ TRAIT_NOMETABOLISM,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTHEAT,
+ TRAIT_RESISTLOWPRESSURE,
+ TRAIT_RESISTHIGHPRESSURE,
+ TRAIT_TOXIMMUNE,
)
inherent_biotypes = MOB_ROBOTIC|MOB_HUMANOID
meat = null
diff --git a/code/modules/mob/living/carbon/human/species_types/dullahan.dm b/code/modules/mob/living/carbon/human/species_types/dullahan.dm
index 1cdca7279be93..6282ebe156aee 100644
--- a/code/modules/mob/living/carbon/human/species_types/dullahan.dm
+++ b/code/modules/mob/living/carbon/human/species_types/dullahan.dm
@@ -5,8 +5,9 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_NOHUNGER,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
+ TRAIT_NOHUNGER,
)
inherent_biotypes = MOB_UNDEAD|MOB_HUMANOID
mutant_bodyparts = list("wings" = "None")
diff --git a/code/modules/mob/living/carbon/human/species_types/flypeople.dm b/code/modules/mob/living/carbon/human/species_types/flypeople.dm
index f3d58a512ff12..9cb6eef50fe67 100644
--- a/code/modules/mob/living/carbon/human/species_types/flypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/flypeople.dm
@@ -8,6 +8,7 @@
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
TRAIT_CAN_USE_FLIGHT_POTION,
+ TRAIT_LITERATE,
)
inherent_biotypes = MOB_ORGANIC|MOB_HUMANOID|MOB_BUG
meat = /obj/item/food/meat/slab/human/mutant/fly
diff --git a/code/modules/mob/living/carbon/human/species_types/golems.dm b/code/modules/mob/living/carbon/human/species_types/golems.dm
index 930923cd5e2ec..386baf3d921b5 100644
--- a/code/modules/mob/living/carbon/human/species_types/golems.dm
+++ b/code/modules/mob/living/carbon/human/species_types/golems.dm
@@ -6,17 +6,18 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_RESISTHEAT,
+ TRAIT_CHUNKYFINGERS,
+ TRAIT_GENELESS,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
+ TRAIT_NODISMEMBER,
+ TRAIT_NOFIRE,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
TRAIT_RESISTCOLD,
+ TRAIT_RESISTHEAT,
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
- TRAIT_NOFIRE,
- TRAIT_CHUNKYFINGERS,
- TRAIT_RADIMMUNE,
- TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
- TRAIT_NODISMEMBER,
)
inherent_biotypes = MOB_HUMANOID|MOB_MINERAL
mutant_organs = list(/obj/item/organ/internal/adamantine_resonator)
@@ -115,15 +116,16 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
+ TRAIT_CHUNKYFINGERS,
+ TRAIT_GENELESS,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
+ TRAIT_NODISMEMBER,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
TRAIT_RESISTCOLD,
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
- TRAIT_CHUNKYFINGERS,
- TRAIT_RADIMMUNE,
- TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
- TRAIT_NODISMEMBER,
)
info_text = "As a Plasma Golem, you burn easily. Be careful, if you get hot enough while burning, you'll blow up!"
heatmod = 0 //fine until they blow up
@@ -209,7 +211,7 @@
fixed_mut_color = "#dddddd"
punchstunthreshold = 9 //60% chance, from 40%
meat = /obj/item/stack/ore/silver
- info_text = "As a Silver Golem, your attacks have a higher chance of stunning. Being made of silver, your body is immune to most types of magic."
+ info_text = "As a Silver Golem, your attacks have a higher chance of stunning. Being made of silver, your body is immune to spirits of the damned and runic golems."
prefix = "Silver"
special_names = list("Surfer", "Chariot", "Lining")
examine_limb_id = SPECIES_GOLEM
@@ -325,15 +327,16 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_NOBREATH,
- TRAIT_RESISTCOLD,
- TRAIT_RESISTHIGHPRESSURE,
- TRAIT_RESISTLOWPRESSURE,
TRAIT_CHUNKYFINGERS,
- TRAIT_RADIMMUNE,
TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
+ TRAIT_LITERATE,
+ TRAIT_NOBREATH,
TRAIT_NODISMEMBER,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTLOWPRESSURE,
+ TRAIT_RESISTHIGHPRESSURE,
)
inherent_biotypes = MOB_ORGANIC | MOB_HUMANOID | MOB_PLANT
armor = 30
@@ -593,18 +596,19 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_RESISTHEAT,
- TRAIT_NOBREATH,
- TRAIT_RESISTCOLD,
- TRAIT_RESISTHIGHPRESSURE,
- TRAIT_RESISTLOWPRESSURE,
- TRAIT_NOFIRE,
TRAIT_CHUNKYFINGERS,
TRAIT_CLUMSY,
- TRAIT_RADIMMUNE,
TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
+ TRAIT_LITERATE,
+ TRAIT_NOBREATH,
TRAIT_NODISMEMBER,
+ TRAIT_NOFIRE,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTHEAT,
+ TRAIT_RESISTHIGHPRESSURE,
+ TRAIT_RESISTLOWPRESSURE,
)
punchdamagelow = 0
punchdamagehigh = 1
@@ -701,18 +705,19 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
+ TRAIT_CHUNKYFINGERS,
+ TRAIT_GENELESS,
+ TRAIT_LITERATE,
+ TRAIT_NOBREATH,
+ TRAIT_NODISMEMBER,
+ TRAIT_NOFIRE,
TRAIT_NOFLASH,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
TRAIT_RESISTHEAT,
- TRAIT_NOBREATH,
TRAIT_RESISTCOLD,
- TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
- TRAIT_NOFIRE,
- TRAIT_CHUNKYFINGERS,
- TRAIT_RADIMMUNE,
- TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
- TRAIT_NODISMEMBER,
+ TRAIT_RESISTHIGHPRESSURE,
)
inherent_biotypes = MOB_HUMANOID|MOB_MINERAL
prefix = "Runic"
@@ -728,9 +733,12 @@
BODY_ZONE_CHEST = /obj/item/bodypart/chest/golem/cult,
)
- var/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/golem/phase_shift
- var/obj/effect/proc_holder/spell/pointed/abyssal_gaze/abyssal_gaze
- var/obj/effect/proc_holder/spell/pointed/dominate/dominate
+ /// A ref to our jaunt spell that we get on species gain.
+ var/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/golem/jaunt
+ /// A ref to our gaze spell that we get on species gain.
+ var/datum/action/cooldown/spell/pointed/abyssal_gaze/abyssal_gaze
+ /// A ref to our dominate spell that we get on species gain.
+ var/datum/action/cooldown/spell/pointed/dominate/dominate
/datum/species/golem/runic/random_name(gender,unique,lastname)
var/edgy_first_name = pick("Razor","Blood","Dark","Evil","Cold","Pale","Black","Silent","Chaos","Deadly","Coldsteel")
@@ -738,26 +746,30 @@
var/golem_name = "[edgy_first_name] [edgy_last_name]"
return golem_name
-/datum/species/golem/runic/on_species_gain(mob/living/carbon/C, datum/species/old_species)
+/datum/species/golem/runic/on_species_gain(mob/living/carbon/grant_to, datum/species/old_species)
. = ..()
- phase_shift = new
- phase_shift.charge_counter = 0
- C.AddSpell(phase_shift)
- abyssal_gaze = new
- abyssal_gaze.charge_counter = 0
- C.AddSpell(abyssal_gaze)
- dominate = new
- dominate.charge_counter = 0
- C.AddSpell(dominate)
+ // Create our species specific spells here.
+ // Note we link them to the mob, not the mind,
+ // so they're not moved around on mindswaps
+ jaunt = new(grant_to)
+ jaunt.StartCooldown()
+ jaunt.Grant(grant_to)
+
+ abyssal_gaze = new(grant_to)
+ abyssal_gaze.StartCooldown()
+ abyssal_gaze.Grant(grant_to)
+
+ dominate = new(grant_to)
+ dominate.StartCooldown()
+ dominate.Grant(grant_to)
/datum/species/golem/runic/on_species_loss(mob/living/carbon/C)
- . = ..()
- if(phase_shift)
- C.RemoveSpell(phase_shift)
- if(abyssal_gaze)
- C.RemoveSpell(abyssal_gaze)
- if(dominate)
- C.RemoveSpell(dominate)
+ // Aaand cleanup our species specific spells.
+ // No free rides.
+ QDEL_NULL(jaunt)
+ QDEL_NULL(abyssal_gaze)
+ QDEL_NULL(dominate)
+ return ..()
/datum/species/golem/runic/handle_chemicals(datum/reagent/chem, mob/living/carbon/human/H, delta_time, times_fired)
if(istype(chem, /datum/reagent/water/holywater))
@@ -779,15 +791,16 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_RESISTCOLD,
+ TRAIT_CHUNKYFINGERS,
+ TRAIT_GENELESS,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
+ TRAIT_NODISMEMBER,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
+ TRAIT_RESISTCOLD,
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
- TRAIT_RADIMMUNE,
- TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
- TRAIT_NODISMEMBER,
- TRAIT_CHUNKYFINGERS,
)
inherent_biotypes = MOB_UNDEAD|MOB_HUMANOID
armor = 15 //feels no pain, but not too resistant
@@ -951,17 +964,18 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_RESISTHEAT,
+ TRAIT_CHUNKYFINGERS,
+ TRAIT_GENELESS,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
+ TRAIT_NODISMEMBER,
+ TRAIT_NOFIRE,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
TRAIT_RESISTCOLD,
+ TRAIT_RESISTHEAT,
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
- TRAIT_NOFIRE,
- TRAIT_CHUNKYFINGERS,
- TRAIT_RADIMMUNE,
- TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
- TRAIT_NODISMEMBER,
TRAIT_VENTCRAWLER_NUDE,
)
prefix = "Plastic"
@@ -1050,16 +1064,17 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_NOBREATH,
- TRAIT_RESISTCOLD,
- TRAIT_RESISTHIGHPRESSURE,
- TRAIT_RESISTLOWPRESSURE,
TRAIT_CHUNKYFINGERS,
- TRAIT_RADIMMUNE,
TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
+ TRAIT_LITERATE,
+ TRAIT_NOBREATH,
TRAIT_NODISMEMBER,
TRAIT_NOFLASH,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTHIGHPRESSURE,
+ TRAIT_RESISTLOWPRESSURE,
)
attack_verb = "whips"
attack_sound = 'sound/weapons/whip.ogg'
@@ -1115,15 +1130,16 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
+ TRAIT_CHUNKYFINGERS,
+ TRAIT_GENELESS,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
+ TRAIT_NODISMEMBER,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
TRAIT_RESISTCOLD,
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
- TRAIT_CHUNKYFINGERS,
- TRAIT_RADIMMUNE,
- TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
- TRAIT_NODISMEMBER,
TRAIT_STRONG_GRABBER,
)
prefix = "Leather"
@@ -1142,16 +1158,17 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_NOBREATH,
- TRAIT_RESISTCOLD,
- TRAIT_RESISTHIGHPRESSURE,
- TRAIT_RESISTLOWPRESSURE,
TRAIT_CHUNKYFINGERS,
- TRAIT_RADIMMUNE,
TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
+ TRAIT_LITERATE,
+ TRAIT_NOBREATH,
TRAIT_NODISMEMBER,
TRAIT_NOFLASH,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTHIGHPRESSURE,
+ TRAIT_RESISTLOWPRESSURE,
)
info_text = "As a Durathread Golem, your strikes will cause those your targets to start choking, but your woven body won't withstand fire as well."
bodypart_overrides = list(
@@ -1184,19 +1201,20 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_NOFLASH,
- TRAIT_RESISTHEAT,
+ TRAIT_CHUNKYFINGERS,
+ TRAIT_FAKEDEATH,
+ TRAIT_GENELESS,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
+ TRAIT_NOFIRE,
+ TRAIT_NODISMEMBER,
+ TRAIT_NOFLASH,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
TRAIT_RESISTCOLD,
+ TRAIT_RESISTHEAT,
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
- TRAIT_NOFIRE,
- TRAIT_CHUNKYFINGERS,
- TRAIT_RADIMMUNE,
- TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
- TRAIT_NODISMEMBER,
- TRAIT_FAKEDEATH,
)
species_language_holder = /datum/language_holder/golem/bone
info_text = "As a Bone Golem, You have a powerful spell that lets you chill your enemies with fear, and milk heals you! Just make sure to watch our for bone-hurting juice."
@@ -1297,15 +1315,16 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
+ TRAIT_CHUNKYFINGERS,
+ TRAIT_GENELESS,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
+ TRAIT_NODISMEMBER,
+ TRAIT_PIERCEIMMUNE,
+ TRAIT_RADIMMUNE,
TRAIT_RESISTCOLD,
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
- TRAIT_CHUNKYFINGERS,
- TRAIT_RADIMMUNE,
- TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
- TRAIT_NODISMEMBER,
)
bodypart_overrides = list(
BODY_ZONE_L_ARM = /obj/item/bodypart/l_arm/golem/snow,
@@ -1315,8 +1334,11 @@
BODY_ZONE_R_LEG = /obj/item/bodypart/r_leg/golem/snow,
BODY_ZONE_CHEST = /obj/item/bodypart/chest/golem/snow,
)
- var/obj/effect/proc_holder/spell/targeted/conjure_item/snowball/ball
- var/obj/effect/proc_holder/spell/aimed/cryo/cryo
+
+ /// A ref to our "throw snowball" spell we get on species gain.
+ var/datum/action/cooldown/spell/conjure_item/snowball/snowball
+ /// A ref to our cryobeam spell we get on species gain.
+ var/datum/action/cooldown/spell/pointed/projectile/cryo/cryo
/datum/species/golem/snow/spec_death(gibbed, mob/living/carbon/human/H)
H.visible_message(span_danger("[H] turns into a pile of snow!"))
@@ -1327,31 +1349,23 @@
new /obj/item/food/grown/carrot(get_turf(H))
qdel(H)
-/datum/species/golem/snow/on_species_gain(mob/living/carbon/C, datum/species/old_species)
- . = ..()
- ADD_TRAIT(C, TRAIT_SNOWSTORM_IMMUNE, SPECIES_TRAIT)
- ball = new
- ball.charge_counter = 0
- C.AddSpell(ball)
- cryo = new
- cryo.charge_counter = 0
- C.AddSpell(cryo)
-
-/datum/species/golem/snow/on_species_loss(mob/living/carbon/C)
+/datum/species/golem/snow/on_species_gain(mob/living/carbon/grant_to, datum/species/old_species)
. = ..()
- REMOVE_TRAIT(C, TRAIT_SNOWSTORM_IMMUNE, SPECIES_TRAIT)
- if(ball)
- C.RemoveSpell(ball)
- if(cryo)
- C.RemoveSpell(cryo)
-
-/obj/effect/proc_holder/spell/targeted/conjure_item/snowball
- name = "Snowball"
- desc = "Concentrates cryokinetic forces to create snowballs, useful for throwing at people."
- item_type = /obj/item/toy/snowball
- charge_max = 15
- action_icon = 'icons/obj/toy.dmi'
- action_icon_state = "snowball"
+ ADD_TRAIT(grant_to, TRAIT_SNOWSTORM_IMMUNE, SPECIES_TRAIT)
+
+ snowball = new(grant_to)
+ snowball.StartCooldown()
+ snowball.Grant(grant_to)
+
+ cryo = new(grant_to)
+ cryo.StartCooldown()
+ cryo.Grant(grant_to)
+
+/datum/species/golem/snow/on_species_loss(mob/living/carbon/remove_from)
+ REMOVE_TRAIT(remove_from, TRAIT_SNOWSTORM_IMMUNE, SPECIES_TRAIT)
+ QDEL_NULL(snowball)
+ QDEL_NULL(cryo)
+ return ..()
/datum/species/golem/mhydrogen
name = "Metallic Hydrogen Golem"
@@ -1363,15 +1377,16 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_NOFLASH,
- TRAIT_RESISTHEAT,
+ TRAIT_CHUNKYFINGERS,
+ TRAIT_GENELESS,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
- TRAIT_RESISTHIGHPRESSURE,
+ TRAIT_NODISMEMBER,
TRAIT_NOFIRE,
+ TRAIT_NOFLASH,
TRAIT_RADIMMUNE,
- TRAIT_GENELESS,
- TRAIT_NODISMEMBER,
- TRAIT_CHUNKYFINGERS,
+ TRAIT_RESISTHEAT,
+ TRAIT_RESISTHIGHPRESSURE,
)
examine_limb_id = SPECIES_GOLEM
diff --git a/code/modules/mob/living/carbon/human/species_types/humans.dm b/code/modules/mob/living/carbon/human/species_types/humans.dm
index 490ca6e01f686..f08ca038bd17a 100644
--- a/code/modules/mob/living/carbon/human/species_types/humans.dm
+++ b/code/modules/mob/living/carbon/human/species_types/humans.dm
@@ -6,6 +6,7 @@
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
TRAIT_CAN_USE_FLIGHT_POTION,
+ TRAIT_LITERATE,
)
mutant_bodyparts = list("wings" = "None")
use_skintones = 1
diff --git a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
index e7a6037310509..5f4e124e33400 100644
--- a/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/jellypeople.dm
@@ -8,6 +8,7 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
+ TRAIT_LITERATE,
TRAIT_TOXINLOVER,
)
mutantlungs = /obj/item/organ/internal/lungs/slime
diff --git a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
index d175613706e62..ed6e32420b6f0 100644
--- a/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/lizardpeople.dm
@@ -9,6 +9,7 @@
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
TRAIT_CAN_USE_FLIGHT_POTION,
+ TRAIT_LITERATE,
)
inherent_biotypes = MOB_ORGANIC|MOB_HUMANOID|MOB_REPTILE
mutant_bodyparts = list("body_markings" = "None", "legs" = "Normal Legs")
@@ -135,8 +136,8 @@ Lizard subspecies: ASHWALKERS
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
TRAIT_CHUNKYFINGERS,
+ //TRAIT_LITERATE,
TRAIT_VIRUSIMMUNE,
- TRAIT_ILLITERATE,
)
species_language_holder = /datum/language_holder/lizard/ash
digitigrade_customization = DIGITIGRADE_FORCED
@@ -151,11 +152,12 @@ Lizard subspecies: SILVER SCALED
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
+ TRAIT_LITERATE,
TRAIT_HOLY,
TRAIT_NOBREATH,
+ TRAIT_PIERCEIMMUNE,
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
- TRAIT_PIERCEIMMUNE,
TRAIT_VIRUSIMMUNE,
TRAIT_WINE_TASTER,
)
diff --git a/code/modules/mob/living/carbon/human/species_types/monkeys.dm b/code/modules/mob/living/carbon/human/species_types/monkeys.dm
index 0bb54535c167e..24d11a8e510e7 100644
--- a/code/modules/mob/living/carbon/human/species_types/monkeys.dm
+++ b/code/modules/mob/living/carbon/human/species_types/monkeys.dm
@@ -25,10 +25,11 @@
)
inherent_traits = list(
TRAIT_CAN_STRIP,
- TRAIT_VENTCRAWLER_NUDE,
+ TRAIT_GUN_NATURAL,
+ //TRAIT_LITERATE,
TRAIT_PRIMITIVE,
+ TRAIT_VENTCRAWLER_NUDE,
TRAIT_WEAK_SOUL,
- TRAIT_GUN_NATURAL,
)
no_equip = list(
ITEM_SLOT_OCLOTHING,
diff --git a/code/modules/mob/living/carbon/human/species_types/mothmen.dm b/code/modules/mob/living/carbon/human/species_types/mothmen.dm
index 1800cdd52cf48..4823b8008b624 100644
--- a/code/modules/mob/living/carbon/human/species_types/mothmen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/mothmen.dm
@@ -8,6 +8,7 @@
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
TRAIT_CAN_USE_FLIGHT_POTION,
+ TRAIT_LITERATE,
)
inherent_biotypes = MOB_ORGANIC|MOB_HUMANOID|MOB_BUG
mutant_bodyparts = list("moth_markings" = "None")
@@ -110,7 +111,7 @@
SPECIES_PERK_TYPE = SPECIES_POSITIVE_PERK,
SPECIES_PERK_ICON = "tshirt",
SPECIES_PERK_NAME = "Meal Plan",
- SPECIES_PERK_DESC = "Moths can eat clothes for nourishment.",
+ SPECIES_PERK_DESC = "Moths can eat clothes for temporary nourishment.",
),
list(
SPECIES_PERK_TYPE = SPECIES_NEGATIVE_PERK,
diff --git a/code/modules/mob/living/carbon/human/species_types/mushpeople.dm b/code/modules/mob/living/carbon/human/species_types/mushpeople.dm
index 876634c5db102..28e3e6259b544 100644
--- a/code/modules/mob/living/carbon/human/species_types/mushpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/mushpeople.dm
@@ -14,6 +14,7 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
TRAIT_NOFLASH,
)
diff --git a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
index 39dfd44a0a247..55a644b44a302 100644
--- a/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
+++ b/code/modules/mob/living/carbon/human/species_types/plasmamen.dm
@@ -10,11 +10,12 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_RESISTCOLD,
- TRAIT_RADIMMUNE,
TRAIT_GENELESS,
- TRAIT_NOHUNGER,
TRAIT_HARDLY_WOUNDED,
+ TRAIT_LITERATE,
+ TRAIT_RADIMMUNE,
+ TRAIT_RESISTCOLD,
+ TRAIT_NOHUNGER,
)
inherent_biotypes = MOB_HUMANOID|MOB_MINERAL
diff --git a/code/modules/mob/living/carbon/human/species_types/podpeople.dm b/code/modules/mob/living/carbon/human/species_types/podpeople.dm
index eec8917fb9844..603958f9367f3 100644
--- a/code/modules/mob/living/carbon/human/species_types/podpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/podpeople.dm
@@ -7,6 +7,7 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
+ TRAIT_LITERATE,
TRAIT_PLANT_SAFE,
)
external_organs = list(
diff --git a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm
index bdcc3f150f9f9..cf033942be167 100644
--- a/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm
+++ b/code/modules/mob/living/carbon/human/species_types/shadowpeople.dm
@@ -9,9 +9,10 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
+ TRAIT_LITERATE,
+ TRAIT_NOBREATH,
TRAIT_RADIMMUNE,
TRAIT_VIRUSIMMUNE,
- TRAIT_NOBREATH,
)
inherent_factions = list("faithless")
changesource_flags = MIRROR_BADMIN | WABBAJACK | MIRROR_PRIDE | MIRROR_MAGIC
diff --git a/code/modules/mob/living/carbon/human/species_types/skeletons.dm b/code/modules/mob/living/carbon/human/species_types/skeletons.dm
index e1da28b300ed7..ac9714fc3d36c 100644
--- a/code/modules/mob/living/carbon/human/species_types/skeletons.dm
+++ b/code/modules/mob/living/carbon/human/species_types/skeletons.dm
@@ -9,23 +9,24 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_NOMETABOLISM,
- TRAIT_TOXIMMUNE,
- TRAIT_RESISTHEAT,
+ TRAIT_CAN_USE_FLIGHT_POTION,
+ TRAIT_EASYDISMEMBER,
+ TRAIT_FAKEDEATH,
+ TRAIT_GENELESS,
+ TRAIT_LIMBATTACHMENT,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
+ TRAIT_NOCLONELOSS,
+ TRAIT_NOHUNGER,
+ TRAIT_NOMETABOLISM,
+ TRAIT_RADIMMUNE,
+ TRAIT_PIERCEIMMUNE,
TRAIT_RESISTCOLD,
+ TRAIT_RESISTHEAT,
TRAIT_RESISTHIGHPRESSURE,
TRAIT_RESISTLOWPRESSURE,
- TRAIT_RADIMMUNE,
- TRAIT_GENELESS,
- TRAIT_PIERCEIMMUNE,
- TRAIT_NOHUNGER,
- TRAIT_EASYDISMEMBER,
- TRAIT_LIMBATTACHMENT,
- TRAIT_FAKEDEATH,
+ TRAIT_TOXIMMUNE,
TRAIT_XENO_IMMUNE,
- TRAIT_NOCLONELOSS,
- TRAIT_CAN_USE_FLIGHT_POTION,
)
inherent_biotypes = MOB_UNDEAD|MOB_HUMANOID
mutanttongue = /obj/item/organ/internal/tongue/bone
diff --git a/code/modules/mob/living/carbon/human/species_types/snail.dm b/code/modules/mob/living/carbon/human/species_types/snail.dm
index 6c0a1355724a2..6062e46178801 100644
--- a/code/modules/mob/living/carbon/human/species_types/snail.dm
+++ b/code/modules/mob/living/carbon/human/species_types/snail.dm
@@ -6,6 +6,7 @@
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
TRAIT_NOSLIPALL,
+ TRAIT_LITERATE,
)
attack_verb = "slap"
attack_effect = ATTACK_EFFECT_DISARM
diff --git a/code/modules/mob/living/carbon/human/species_types/vampire.dm b/code/modules/mob/living/carbon/human/species_types/vampire.dm
index 9201a2f870d31..18e1483839d97 100644
--- a/code/modules/mob/living/carbon/human/species_types/vampire.dm
+++ b/code/modules/mob/living/carbon/human/species_types/vampire.dm
@@ -20,8 +20,9 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_NOHUNGER,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
+ TRAIT_NOHUNGER,
)
inherent_biotypes = MOB_UNDEAD|MOB_HUMANOID
mutant_bodyparts = list("wings" = "None")
diff --git a/code/modules/mob/living/carbon/human/species_types/zombies.dm b/code/modules/mob/living/carbon/human/species_types/zombies.dm
index c051c8e3bd8a4..6cf04410528e9 100644
--- a/code/modules/mob/living/carbon/human/species_types/zombies.dm
+++ b/code/modules/mob/living/carbon/human/species_types/zombies.dm
@@ -11,20 +11,21 @@
inherent_traits = list(
TRAIT_ADVANCEDTOOLUSER,
TRAIT_CAN_STRIP,
- TRAIT_NOMETABOLISM,
- TRAIT_NOHUNGER,
- TRAIT_TOXIMMUNE,
- TRAIT_RESISTCOLD,
- TRAIT_RESISTHIGHPRESSURE,
- TRAIT_RESISTLOWPRESSURE,
- TRAIT_RADIMMUNE,
- TRAIT_EASYDISMEMBER,
TRAIT_EASILY_WOUNDED,
+ TRAIT_EASYDISMEMBER,
+ TRAIT_FAKEDEATH,
TRAIT_LIMBATTACHMENT,
+ TRAIT_LITERATE,
TRAIT_NOBREATH,
- TRAIT_NODEATH,
- TRAIT_FAKEDEATH,
TRAIT_NOCLONELOSS,
+ TRAIT_NODEATH,
+ TRAIT_NOHUNGER,
+ TRAIT_NOMETABOLISM,
+ TRAIT_RADIMMUNE,
+ TRAIT_RESISTCOLD,
+ TRAIT_RESISTHIGHPRESSURE,
+ TRAIT_RESISTLOWPRESSURE,
+ TRAIT_TOXIMMUNE,
)
inherent_biotypes = MOB_UNDEAD|MOB_HUMANOID
mutanttongue = /obj/item/organ/internal/tongue/zombie
diff --git a/code/modules/mob/living/carbon/inventory.dm b/code/modules/mob/living/carbon/inventory.dm
index 9fe7e1ddcc333..7cac0335a5a70 100644
--- a/code/modules/mob/living/carbon/inventory.dm
+++ b/code/modules/mob/living/carbon/inventory.dm
@@ -12,7 +12,32 @@
return handcuffed
if(ITEM_SLOT_LEGCUFFED)
return legcuffed
- return null
+
+ return ..()
+
+/mob/living/carbon/get_slot_by_item(obj/item/looking_for)
+ if(looking_for == back)
+ return ITEM_SLOT_BACK
+
+ if(back && (looking_for in back))
+ return ITEM_SLOT_BACKPACK
+
+ if(looking_for == wear_mask)
+ return ITEM_SLOT_MASK
+
+ if(looking_for == wear_neck)
+ return ITEM_SLOT_NECK
+
+ if(looking_for == head)
+ return ITEM_SLOT_HEAD
+
+ if(looking_for == handcuffed)
+ return ITEM_SLOT_HANDCUFFED
+
+ if(looking_for == legcuffed)
+ return ITEM_SLOT_LEGCUFFED
+
+ return ..()
/mob/living/carbon/proc/get_all_worn_items()
return list(
diff --git a/code/modules/mob/living/carbon/life.dm b/code/modules/mob/living/carbon/life.dm
index 8b3968535c631..23bf69ebe76bf 100644
--- a/code/modules/mob/living/carbon/life.dm
+++ b/code/modules/mob/living/carbon/life.dm
@@ -401,26 +401,6 @@
if(HM?.timeout)
dna.remove_mutation(HM.type)
-/*
-Alcohol Poisoning Chart
-Note that all higher effects of alcohol poisoning will inherit effects for smaller amounts (i.e. light poisoning inherts from slight poisoning)
-In addition, severe effects won't always trigger unless the drink is poisonously strong
-All effects don't start immediately, but rather get worse over time; the rate is affected by the imbiber's alcohol tolerance
-
-0: Non-alcoholic
-1-10: Barely classifiable as alcohol - occassional slurring
-11-20: Slight alcohol content - slurring
-21-30: Below average - imbiber begins to look slightly drunk
-31-40: Just below average - no unique effects
-41-50: Average - mild disorientation, imbiber begins to look drunk
-51-60: Just above average - disorientation, vomiting, imbiber begins to look heavily drunk
-61-70: Above average - small chance of blurry vision, imbiber begins to look smashed
-71-80: High alcohol content - blurry vision, imbiber completely shitfaced
-81-90: Extremely high alcohol content - light brain damage, passing out
-91-100: Dangerously toxic - swift death
-*/
-#define BALLMER_POINTS 5
-
// This updates all special effects that really should be status effect datums: Druggy, Hallucinations, Drunkenness, Mute, etc..
/mob/living/carbon/handle_status_effects(delta_time, times_fired)
..()
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index e665e40f84d3b..fba6f0df21b57 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -33,8 +33,6 @@
else
effect.be_replaced()
- if(ranged_ability)
- ranged_ability.remove_ranged_ability(src)
if(buckled)
buckled.unbuckle_mob(src,force=1)
@@ -748,10 +746,6 @@
clear_alert(ALERT_NOT_ENOUGH_OXYGEN)
reload_fullscreen()
. = TRUE
- if(mind)
- for(var/S in mind.spell_list)
- var/obj/effect/proc_holder/spell/spell = S
- spell.updateButtons()
if(excess_healing)
INVOKE_ASYNC(src, .proc/emote, "gasp")
log_combat(src, src, "revived")
@@ -879,7 +873,7 @@
var/mob/living/L = pulledby
L.set_pull_offsets(src, pulledby.grab_state)
- if(active_storage && !(CanReach(active_storage.parent,view_only = TRUE)))
+ if(active_storage && !((active_storage.parent in important_recursive_contents?[RECURSIVE_CONTENTS_ACTIVE_STORAGE]) || CanReach(active_storage.parent,view_only = TRUE)))
active_storage.close(src)
if(body_position == LYING_DOWN && !buckled && prob(getBruteLoss()*200/maxHealth))
@@ -1280,7 +1274,7 @@
/mob/living/simple_animal/hostile/carp,
/mob/living/simple_animal/hostile/bear,
/mob/living/simple_animal/hostile/mushroom,
- /mob/living/simple_animal/hostile/statue,
+ /mob/living/simple_animal/hostile/netherworld/statue,
/mob/living/simple_animal/hostile/retaliate/bat,
/mob/living/simple_animal/hostile/retaliate/goat,
/mob/living/simple_animal/hostile/killertomato,
@@ -1404,6 +1398,7 @@ GLOBAL_LIST_EMPTY(fire_appearances)
var/datum/status_effect/fire_handler/fire_stacks/fire_status = has_status_effect(/datum/status_effect/fire_handler/fire_stacks)
if(!fire_status || !fire_status.on_fire)
return
+
remove_status_effect(/datum/status_effect/fire_handler/fire_stacks)
/**
@@ -1546,24 +1541,6 @@ GLOBAL_LIST_EMPTY(fire_appearances)
/mob/living/proc/on_fall()
return
-/mob/living/proc/AddAbility(obj/effect/proc_holder/A)
- abilities.Add(A)
- A.on_gain(src)
- if(A.has_action)
- A.action.Grant(src)
-
-/mob/living/proc/RemoveAbility(obj/effect/proc_holder/A)
- abilities.Remove(A)
- A.on_lose(src)
- if(A.action)
- A.action.Remove(src)
-
-/mob/living/proc/add_abilities_to_panel()
- var/list/L = list()
- for(var/obj/effect/proc_holder/A in abilities)
- L[++L.len] = list("[A.panel]",A.get_panel_text(),A.name,"[REF(A)]")
- return L
-
/mob/living/forceMove(atom/destination)
if(!currently_z_moving)
stop_pulling()
@@ -1669,12 +1646,6 @@ GLOBAL_LIST_EMPTY(fire_appearances)
else
clear_fullscreen("remote_view", 0)
-/mob/living/update_mouse_pointer()
- ..()
- if (client && ranged_ability?.ranged_mousepointer)
- client.mouse_pointer_icon = ranged_ability.ranged_mousepointer
-
-
/mob/living/vv_edit_var(var_name, var_value)
switch(var_name)
if (NAMEOF(src, maxHealth))
diff --git a/code/modules/mob/living/living_defines.dm b/code/modules/mob/living/living_defines.dm
index c75c60a019893..d59c77c8ca054 100644
--- a/code/modules/mob/living/living_defines.dm
+++ b/code/modules/mob/living/living_defines.dm
@@ -7,7 +7,8 @@
hud_type = /datum/hud/living
- var/resize = 1 ///Badminnery resize
+ ///Badminnery resize
+ var/resize = 1
var/lastattacker = null
var/lastattackerckey = null
@@ -18,20 +19,27 @@
var/health = MAX_LIVING_HEALTH
//Damage related vars, NOTE: THESE SHOULD ONLY BE MODIFIED BY PROCS
- var/bruteloss = 0 ///Brutal damage caused by brute force (punching, being clubbed by a toolbox ect... this also accounts for pressure damage)
- var/oxyloss = 0 ///Oxygen depravation damage (no air in lungs)
- var/toxloss = 0 ///Toxic damage caused by being poisoned or radiated
- var/fireloss = 0 ///Burn damage caused by being way too hot, too cold or burnt.
- var/cloneloss = 0 ///Damage caused by being cloned or ejected from the cloner early. slimes also deal cloneloss damage to victims
- var/staminaloss = 0 ///Stamina damage, or exhaustion. You recover it slowly naturally, and are knocked down if it gets too high. Holodeck and hallucinations deal this.
- var/crit_threshold = HEALTH_THRESHOLD_CRIT /// when the mob goes from "normal" to crit
+ ///Brutal damage caused by brute force (punching, being clubbed by a toolbox ect... this also accounts for pressure damage)
+ var/bruteloss = 0
+ ///Oxygen depravation damage (no air in lungs)
+ var/oxyloss = 0
+ ///Toxic damage caused by being poisoned or radiated
+ var/toxloss = 0
+ ///Burn damage caused by being way too hot, too cold or burnt.
+ var/fireloss = 0
+ ///Damage caused by being cloned or ejected from the cloner early. slimes also deal cloneloss damage to victims
+ var/cloneloss = 0
+ ///Stamina damage, or exhaustion. You recover it slowly naturally, and are knocked down if it gets too high. Holodeck and hallucinations deal this.
+ var/staminaloss = 0
+ /// when the mob goes from "normal" to crit
+ var/crit_threshold = HEALTH_THRESHOLD_CRIT
///When the mob enters hard critical state and is fully incapacitated.
var/hardcrit_threshold = HEALTH_THRESHOLD_FULLCRIT
//Damage dealing vars! These are meaningless outside of specific instances where it's checked and defined.
- // Lower bound of damage done by unarmed melee attacks. Mob code is a mess, only works where this is checked for.
+ /// Lower bound of damage done by unarmed melee attacks. Mob code is a mess, only works where this is checked for.
var/melee_damage_lower = 0
- // Upper bound of damage done by unarmed melee attacks. Please ensure you check the xyz_defenses.dm for the mobs in question to see if it uses this or hardcoded values.
+ /// Upper bound of damage done by unarmed melee attacks. Please ensure you check the xyz_defenses.dm for the mobs in question to see if it uses this or hardcoded values.
var/melee_damage_upper = 0
/// Generic bitflags for boolean conditions at the [/mob/living] level. Keep this for inherent traits of living types, instead of runtime-changeable ones.
@@ -48,10 +56,10 @@
VAR_PROTECTED/lying_angle = 0
/// Value of lying lying_angle before last change. TODO: Remove the need for this.
var/lying_prev = 0
-
- var/hallucination = 0 ///Directly affects how long a mob will hallucinate for
-
- var/last_special = 0 ///Used by the resist verb, likely used to prevent players from bypassing next_move by logging in/out.
+ ///Directly affects how long a mob will hallucinate for
+ var/hallucination = 0
+ ///Used by the resist verb, likely used to prevent players from bypassing next_move by logging in/out.
+ var/last_special = 0
var/timeofdeath = 0
/// Helper vars for quick access to firestacks, these should be updated every time firestacks are adjusted
@@ -67,24 +75,28 @@
var/incorporeal_move = FALSE
var/list/quirks = list()
-
- var/list/surgeries = list() ///a list of surgery datums. generally empty, they're added when the player wants them.
+ ///a list of surgery datums. generally empty, they're added when the player wants them.
+ var/list/surgeries = list()
///Mob specific surgery speed modifier
var/mob_surgery_speed_mod = 1
- var/now_pushing = null //! Used by [living/Bump()][/mob/living/proc/Bump] and [living/PushAM()][/mob/living/proc/PushAM] to prevent potential infinite loop.
+ /// Used by [living/Bump()][/mob/living/proc/Bump] and [living/PushAM()][/mob/living/proc/PushAM] to prevent potential infinite loop.
+ var/now_pushing = null
var/cameraFollow = null
/// Time of death
var/tod = null
- var/limb_destroyer = 0 //1 Sets AI behavior that allows mobs to target and dismember limbs with their basic attack.
+ /// Sets AI behavior that allows mobs to target and dismember limbs with their basic attack.
+ var/limb_destroyer = 0
var/mob_size = MOB_SIZE_HUMAN
var/mob_biotypes = MOB_ORGANIC
- var/metabolism_efficiency = 1 ///more or less efficiency to metabolize helpful/harmful reagents and regulate body temperature..
- var/has_limbs = FALSE ///does the mob have distinct limbs?(arms,legs, chest,head)
+ ///more or less efficiency to metabolize helpful/harmful reagents and regulate body temperature..
+ var/metabolism_efficiency = 1
+ ///does the mob have distinct limbs?(arms,legs, chest,head)
+ var/has_limbs = FALSE
///How many legs does this mob have by default. This shouldn't change at runtime.
var/default_num_legs = 2
@@ -110,32 +122,39 @@
var/smoke_delay = 0 ///used to prevent spam with smoke reagent reaction on mob.
- var/bubble_icon = "default" ///what icon the mob uses for speechbubbles
- var/health_doll_icon ///if this exists AND the normal sprite is bigger than 32x32, this is the replacement icon state (because health doll size limitations). the icon will always be screen_gen.dmi
+ ///what icon the mob uses for speechbubbles
+ var/bubble_icon = "default"
+ ///if this exists AND the normal sprite is bigger than 32x32, this is the replacement icon state (because health doll size limitations). the icon will always be screen_gen.dmi
+ var/health_doll_icon
var/last_bumped = 0
- var/unique_name = FALSE ///if a mob's name should be appended with an id when created e.g. Mob (666)
- var/numba = 0 ///the id a mob gets when it's created
+ ///if a mob's name should be appended with an id when created e.g. Mob (666)
+ var/unique_name = FALSE
+ ///the id a mob gets when it's created
+ var/numba = 0
- var/list/butcher_results = null ///these will be yielded from butchering with a probability chance equal to the butcher item's effectiveness
- var/list/guaranteed_butcher_results = null ///these will always be yielded from butchering
- var/butcher_difficulty = 0 ///effectiveness prob. is modified negatively by this amount; positive numbers make it more difficult, negative ones make it easier
+ ///these will be yielded from butchering with a probability chance equal to the butcher item's effectiveness
+ var/list/butcher_results = null
+ ///these will always be yielded from butchering
+ var/list/guaranteed_butcher_results = null
+ ///effectiveness prob. is modified negatively by this amount; positive numbers make it more difficult, negative ones make it easier
+ var/butcher_difficulty = 0
- var/stun_absorption = null ///converted to a list of stun absorption sources this mob has when one is added
+ ///converted to a list of stun absorption sources this mob has when one is added
+ var/stun_absorption = null
- var/blood_volume = 0 ///how much blood the mob has
- var/obj/effect/proc_holder/ranged_ability ///Any ranged ability the mob has, as a click override
+ ///how much blood the mob has
+ var/blood_volume = 0
- var/see_override = 0 ///0 for no override, sets see_invisible = see_override in silicon & carbon life process via update_sight()
-
- var/list/status_effects ///a list of all status effects the mob has
- var/druggy = 0
+ ///0 for no override, sets see_invisible = see_override in silicon & carbon life process via update_sight()
+ var/see_override = 0
+ ///a list of all status effects the mob has
+ var/list/status_effects
var/list/implants = null
- var/last_words ///used for database logging
-
- var/list/obj/effect/proc_holder/abilities = list()
+ ///used for database logging
+ var/last_words
///whether this can be picked up and held.
var/can_be_held = FALSE
@@ -148,19 +167,25 @@
var/losebreath = 0
//List of active diseases
- var/list/diseases /// list of all diseases in a mob
+ /// list of all diseases in a mob
+ var/list/diseases
var/list/disease_resistances
- var/slowed_by_drag = TRUE ///Whether the mob is slowed down when dragging another prone mob
+ ///Whether the mob is slowed down when dragging another prone mob
+ var/slowed_by_drag = TRUE
/// List of changes to body temperature, used by desease symtoms like fever
var/list/body_temp_changes = list()
//this stuff is here to make it simple for admins to mess with custom held sprites
- var/icon/held_lh = 'icons/mob/pets_held_lh.dmi'//icons for holding mobs
+ ///left hand icon for holding mobs
+ var/icon/held_lh = 'icons/mob/pets_held_lh.dmi'
+ ///right hand icon for holding mobs
var/icon/held_rh = 'icons/mob/pets_held_rh.dmi'
- var/icon/head_icon = 'icons/mob/pets_held.dmi'//what it looks like on your head
- var/held_state = ""//icon state for the above
+ ///what it looks like when the mob is held on your head
+ var/icon/head_icon = 'icons/mob/pets_held.dmi'
+ /// icon_state for holding mobs.
+ var/held_state = ""
///If combat mode is on or not
var/combat_mode = FALSE
@@ -179,3 +204,5 @@
var/native_fov = FOV_90_DEGREES
/// Lazy list of FOV traits that will apply a FOV view when handled.
var/list/fov_traits
+ ///what multiplicative slowdown we get from turfs currently.
+ var/current_turf_slowdown = 0
diff --git a/code/modules/mob/living/living_fov.dm b/code/modules/mob/living/living_fov.dm
index 669dff63c8908..2955f44251e1e 100644
--- a/code/modules/mob/living/living_fov.dm
+++ b/code/modules/mob/living/living_fov.dm
@@ -88,11 +88,20 @@
UNSETEMPTY(fov_traits)
update_fov()
+//did you know you can subtype /image and /mutable_appearance?
+/image/fov_image
+ icon = 'icons/effects/fov/fov_effects.dmi'
+ layer = FOV_EFFECTS_LAYER
+ appearance_flags = RESET_COLOR | RESET_TRANSFORM
+ plane = FULLSCREEN_PLANE
+
/// Plays a visual effect representing a sound cue for people with vision obstructed by FOV or blindness
-/proc/play_fov_effect(atom/center, range, icon_state, dir = SOUTH, ignore_self = FALSE, angle = 0)
+/proc/play_fov_effect(atom/center, range, icon_state, dir = SOUTH, ignore_self = FALSE, angle = 0, list/override_list)
var/turf/anchor_point = get_turf(center)
- var/image/fov_image
- for(var/mob/living/living_mob in get_hearers_in_view(range, center))
+ var/image/fov_image/fov_image
+ var/list/clients_shown
+
+ for(var/mob/living/living_mob in override_list || get_hearers_in_view(range, center))
var/client/mob_client = living_mob.client
if(!mob_client)
continue
@@ -101,18 +110,22 @@
if(living_mob.in_fov(center, ignore_self))
continue
if(!fov_image) //Make the image once we found one recipient to receive it
- fov_image = image(icon = 'icons/effects/fov/fov_effects.dmi', icon_state = icon_state, loc = anchor_point)
- fov_image.plane = FULLSCREEN_PLANE
- fov_image.layer = FOV_EFFECTS_LAYER
+ fov_image = new()
+ fov_image.loc = anchor_point
+ fov_image.icon_state = icon_state
fov_image.dir = dir
- fov_image.appearance_flags = RESET_COLOR | RESET_TRANSFORM
if(angle)
var/matrix/matrix = new
matrix.Turn(angle)
fov_image.transform = matrix
fov_image.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ LAZYADD(clients_shown, mob_client)
+
mob_client.images += fov_image
- addtimer(CALLBACK(GLOBAL_PROC, .proc/remove_image_from_client, fov_image, mob_client), 30)
+ //when added as an image mutable_appearances act identically. we just make it an MA becuase theyre faster to change appearance
+
+ if(clients_shown)
+ addtimer(CALLBACK(GLOBAL_PROC, .proc/remove_images_from_clients, fov_image, clients_shown), 30)
/atom/movable/screen/fov_blocker
icon = 'icons/effects/fov/field_of_view.dmi'
diff --git a/code/modules/mob/living/living_movement.dm b/code/modules/mob/living/living_movement.dm
index f23512353fff3..e94cc955db390 100644
--- a/code/modules/mob/living/living_movement.dm
+++ b/code/modules/mob/living/living_movement.dm
@@ -28,9 +28,12 @@
/mob/living/proc/update_turf_movespeed(turf/open/T)
if(isopenturf(T))
- add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/turf_slowdown, multiplicative_slowdown = T.slowdown)
- else
+ if(T.slowdown != current_turf_slowdown)
+ add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/turf_slowdown, multiplicative_slowdown = T.slowdown)
+ current_turf_slowdown = T.slowdown
+ else if(current_turf_slowdown)
remove_movespeed_modifier(/datum/movespeed_modifier/turf_slowdown)
+ current_turf_slowdown = 0
/mob/living/proc/update_pull_movespeed()
diff --git a/code/modules/mob/living/living_say.dm b/code/modules/mob/living/living_say.dm
index ce1a7dbbc9523..d76ce29b13100 100644
--- a/code/modules/mob/living/living_say.dm
+++ b/code/modules/mob/living/living_say.dm
@@ -493,7 +493,21 @@ GLOBAL_LIST_INIT(message_modes_stat_limits, list(
else
. = ..()
-/mob/living/whisper(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null, filterproof)
+/**
+ * Living level whisper.
+ *
+ * Living mobs which whisper have their message only appear to people very close.
+ *
+ * message - the message to display
+ * bubble_type - the type of speech bubble that shows up when they speak (currently does nothing)
+ * spans - a list of spans to apply around the message
+ * sanitize - whether we sanitize the message
+ * language - typepath language to force them to speak / whisper in
+ * ignore_spam - whether we ignore the spam filter
+ * forced - string source of what forced this speech to happen, also bypasses spam filter / mutes if supplied
+ * filterproof - whether we ignore the word filter
+ */
+/mob/living/whisper(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language, ignore_spam = FALSE, forced, filterproof)
if(!message)
return
say("#[message]", bubble_type, spans, sanitize, language, ignore_spam, forced, filterproof)
diff --git a/code/modules/mob/living/login.dm b/code/modules/mob/living/login.dm
index 201142b341431..c6d6b6a5e9d70 100644
--- a/code/modules/mob/living/login.dm
+++ b/code/modules/mob/living/login.dm
@@ -18,9 +18,6 @@
if(ventcrawler)
to_chat(src, span_notice("You can ventcrawl! Use alt+click on vents to quickly travel about the station."))
- if(ranged_ability)
- ranged_ability.add_ranged_ability(src, span_notice("You currently have [ranged_ability] active!"))
-
med_hud_set_status()
update_fov_client()
diff --git a/code/modules/mob/living/silicon/ai/examine.dm b/code/modules/mob/living/silicon/ai/examine.dm
index 3202372a89745..265ad8144b961 100644
--- a/code/modules/mob/living/silicon/ai/examine.dm
+++ b/code/modules/mob/living/silicon/ai/examine.dm
@@ -1,5 +1,5 @@
/mob/living/silicon/ai/examine(mob/user)
- . = list("*---------*\nThis is [icon2html(src, user)] [src]!")
+ . = list("This is [icon2html(src, user)] [src]!")
if (stat == DEAD)
. += span_deadsay("It appears to be powered-down.")
else
@@ -14,9 +14,12 @@
else
. += span_warning("Its casing is melted and heat-warped!")
if(deployed_shell)
- . += "The wireless networking light is blinking.\n"
+ . += "The wireless networking light is blinking."
else if (!shunted && !client)
- . += "[src]Core.exe has stopped responding! NTOS is searching for a solution to the problem...\n"
- . += "*---------*"
+ . += "[src]Core.exe has stopped responding! NTOS is searching for a solution to the problem..."
+ . += ""
. += ..()
+
+/mob/living/silicon/ai/get_examine_string(mob/user, thats = FALSE)
+ return null
diff --git a/code/modules/mob/living/silicon/laws.dm b/code/modules/mob/living/silicon/laws.dm
index d85ec0c56acd9..62c12bdd6e901 100644
--- a/code/modules/mob/living/silicon/laws.dm
+++ b/code/modules/mob/living/silicon/laws.dm
@@ -69,9 +69,9 @@
hackedcheck += law
post_lawchange(announce)
-/mob/living/silicon/proc/replace_random_law(law, groups, announce = TRUE)
+/mob/living/silicon/proc/replace_random_law(law, remove_law_groups, insert_law_group, announce = TRUE)
laws_sanity_check()
- . = laws.replace_random_law(law,groups)
+ . = laws.replace_random_law(law, remove_law_groups, insert_law_group)
post_lawchange(announce)
/mob/living/silicon/proc/shuffle_laws(list/groups, announce = TRUE)
diff --git a/code/modules/mob/living/silicon/pai/pai.dm b/code/modules/mob/living/silicon/pai/pai.dm
index 7e1d636874e2c..fac7d5a45f759 100644
--- a/code/modules/mob/living/silicon/pai/pai.dm
+++ b/code/modules/mob/living/silicon/pai/pai.dm
@@ -90,6 +90,7 @@
"hawk" = FALSE,
"lizard" = FALSE,
"duffel" = TRUE,
+ "crow" = TRUE,
)
var/emitterhealth = 20
diff --git a/code/modules/mob/living/silicon/robot/examine.dm b/code/modules/mob/living/silicon/robot/examine.dm
index e7ba74698ff35..eeb46c28b611e 100644
--- a/code/modules/mob/living/silicon/robot/examine.dm
+++ b/code/modules/mob/living/silicon/robot/examine.dm
@@ -1,5 +1,5 @@
/mob/living/silicon/robot/examine(mob/user)
- . = list("*---------*\nThis is [icon2html(src, user)] \a [src]!")
+ . = list("This is [icon2html(src, user)] [src]!")
if(desc)
. += "[desc]"
@@ -43,6 +43,9 @@
. += span_warning("It doesn't seem to be responding.")
if(DEAD)
. += span_deadsay("It looks like its system is corrupted and requires a reset.")
- . += "*---------*"
+ . += ""
. += ..()
+
+/mob/living/silicon/robot/get_examine_string(mob/user, thats = FALSE)
+ return null
diff --git a/code/modules/mob/living/silicon/robot/robot_defense.dm b/code/modules/mob/living/silicon/robot/robot_defense.dm
index 3b3c4d1b997dd..721ab6422f745 100644
--- a/code/modules/mob/living/silicon/robot/robot_defense.dm
+++ b/code/modules/mob/living/silicon/robot/robot_defense.dm
@@ -363,7 +363,7 @@ GLOBAL_LIST_INIT(blacklisted_borg_hats, typecacheof(list( //Hats that don't real
lawupdate = FALSE
set_connected_ai(null)
message_admins("[ADMIN_LOOKUPFLW(user)] emagged cyborg [ADMIN_LOOKUPFLW(src)]. Laws overridden.")
- log_silicon("EMAG: [key_name(user)] emagged cyborg [key_name(src)]. Laws overridden.")
+ log_silicon("EMAG: [key_name(user)] emagged cyborg [key_name(src)]. Laws overridden.")
var/time = time2text(world.realtime,"hh:mm:ss")
if(user)
GLOB.lawchanges.Add("[time] : [user.name]([user.key]) emagged [name]([key])")
diff --git a/code/modules/mob/living/silicon/robot/robot_model.dm b/code/modules/mob/living/silicon/robot/robot_model.dm
index db9636a6d0bb5..c556040514c9c 100644
--- a/code/modules/mob/living/silicon/robot/robot_model.dm
+++ b/code/modules/mob/living/silicon/robot/robot_model.dm
@@ -596,7 +596,7 @@
basic_modules = list(
/obj/item/assembly/flash/cyborg,
/obj/item/healthanalyzer,
- /obj/item/reagent_containers/borghypo,
+ /obj/item/reagent_containers/borghypo/medical,
/obj/item/borg/apparatus/beaker,
/obj/item/reagent_containers/dropper,
/obj/item/reagent_containers/syringe,
@@ -616,7 +616,7 @@
/obj/item/borg/apparatus/organ_storage,
/obj/item/borg/lollipop)
radio_channels = list(RADIO_CHANNEL_MEDICAL)
- emag_modules = list(/obj/item/reagent_containers/borghypo/hacked)
+ emag_modules = list(/obj/item/reagent_containers/borghypo/medical/hacked)
cyborg_base_icon = "medical"
model_select_icon = "medical"
model_traits = list(TRAIT_PUSHIMMUNE)
diff --git a/code/modules/mob/living/silicon/silicon.dm b/code/modules/mob/living/silicon/silicon.dm
index 8d1a2d78de61c..c754b3836c4e0 100644
--- a/code/modules/mob/living/silicon/silicon.dm
+++ b/code/modules/mob/living/silicon/silicon.dm
@@ -65,8 +65,7 @@
ADD_TRAIT(src, TRAIT_MARTIAL_ARTS_IMMUNE, ROUNDSTART_TRAIT)
ADD_TRAIT(src, TRAIT_NOFIRE_SPREAD, ROUNDSTART_TRAIT)
ADD_TRAIT(src, TRAIT_ASHSTORM_IMMUNE, ROUNDSTART_TRAIT)
-
-
+ ADD_TRAIT(src, TRAIT_LITERATE, ROUNDSTART_TRAIT)
/mob/living/silicon/Destroy()
QDEL_NULL(radio)
@@ -404,9 +403,6 @@
if (aicamera)
return aicamera.selectpicture(user)
-/mob/living/silicon/is_literate()
- return TRUE
-
/mob/living/silicon/get_inactive_held_item()
return FALSE
diff --git a/code/modules/mob/living/simple_animal/bot/mulebot.dm b/code/modules/mob/living/simple_animal/bot/mulebot.dm
index 3447985e3bef9..3c412c4ebe51d 100644
--- a/code/modules/mob/living/simple_animal/bot/mulebot.dm
+++ b/code/modules/mob/living/simple_animal/bot/mulebot.dm
@@ -52,7 +52,6 @@
var/obj/item/stock_parts/cell/cell /// Internal Powercell
var/cell_move_power_usage = 1///How much power we use when we move.
- var/bloodiness = 0 ///If we've run over a mob, how many tiles will we leave tracks on while moving
var/num_steps = 0 ///The amount of steps we should take until we rest for a time.
@@ -281,7 +280,7 @@
if(usr.has_unlimited_silicon_privilege)
bot_cover_flags ^= BOT_COVER_LOCKED
. = TRUE
- if("power")
+ if("on")
if(bot_mode_flags & BOT_MODE_ON)
turn_off()
else if(bot_cover_flags & BOT_COVER_OPEN)
@@ -479,18 +478,6 @@
pathset = TRUE //Indicates the AI's custom path is initialized.
start()
-/mob/living/simple_animal/bot/mulebot/Move(atom/newloc, direct) //handle leaving bloody tracks. can't be done via Moved() since that can end up putting the tracks somewhere BEFORE we get bloody.
- if(!bloodiness) //important to check this first since Bump() is called in the Move() -> Entered() chain
- return ..()
- var/atom/oldLoc = loc
- . = ..()
- if(!last_move || isspaceturf(oldLoc)) //if we didn't sucessfully move, or if our old location was a spaceturf.
- return
- var/obj/effect/decal/cleanable/blood/tracks/B = new(oldLoc)
- B.add_blood_DNA(GET_ATOM_BLOOD_DNA(src))
- B.setDir(direct)
- bloodiness--
-
/mob/living/simple_animal/bot/mulebot/Moved()
. = ..()
if(has_gravity())
@@ -686,26 +673,33 @@
return ..()
// when mulebot is in the same loc
-/mob/living/simple_animal/bot/mulebot/proc/run_over(mob/living/carbon/human/H)
- log_combat(src, H, "run over", null, "(DAMTYPE: [uppertext(BRUTE)])")
- H.visible_message(span_danger("[src] drives over [H]!"), \
- span_userdanger("[src] drives over you!"))
+/mob/living/simple_animal/bot/mulebot/proc/run_over(mob/living/carbon/human/crushed)
+ log_combat(src, crushed, "run over", addition = "(DAMTYPE: [uppertext(BRUTE)])")
+ crushed.visible_message(
+ span_danger("[src] drives over [crushed]!"),
+ span_userdanger("[src] drives over you!"),
+ )
+
playsound(src, 'sound/effects/splat.ogg', 50, TRUE)
- var/damage = rand(5,15)
- H.apply_damage(2*damage, BRUTE, BODY_ZONE_HEAD, run_armor_check(BODY_ZONE_HEAD, MELEE))
- H.apply_damage(2*damage, BRUTE, BODY_ZONE_CHEST, run_armor_check(BODY_ZONE_CHEST, MELEE))
- H.apply_damage(0.5*damage, BRUTE, BODY_ZONE_L_LEG, run_armor_check(BODY_ZONE_L_LEG, MELEE))
- H.apply_damage(0.5*damage, BRUTE, BODY_ZONE_R_LEG, run_armor_check(BODY_ZONE_R_LEG, MELEE))
- H.apply_damage(0.5*damage, BRUTE, BODY_ZONE_L_ARM, run_armor_check(BODY_ZONE_L_ARM, MELEE))
- H.apply_damage(0.5*damage, BRUTE, BODY_ZONE_R_ARM, run_armor_check(BODY_ZONE_R_ARM, MELEE))
+ var/damage = rand(5, 15)
+ crushed.apply_damage(2 * damage, BRUTE, BODY_ZONE_HEAD, run_armor_check(BODY_ZONE_HEAD, MELEE))
+ crushed.apply_damage(2 * damage, BRUTE, BODY_ZONE_CHEST, run_armor_check(BODY_ZONE_CHEST, MELEE))
+ crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_LEG, run_armor_check(BODY_ZONE_L_LEG, MELEE))
+ crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_LEG, run_armor_check(BODY_ZONE_R_LEG, MELEE))
+ crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_L_ARM, run_armor_check(BODY_ZONE_L_ARM, MELEE))
+ crushed.apply_damage(0.5 * damage, BRUTE, BODY_ZONE_R_ARM, run_armor_check(BODY_ZONE_R_ARM, MELEE))
+
+ add_mob_blood(crushed)
- var/turf/T = get_turf(src)
- T.add_mob_blood(H)
+ var/turf/below_us = get_turf(src)
+ below_us.add_mob_blood(crushed)
- var/list/blood_dna = H.get_blood_dna_list()
- add_blood_DNA(blood_dna)
- bloodiness += 4
+ AddComponent(/datum/component/blood_walk, \
+ blood_type = /obj/effect/decal/cleanable/blood/tracks, \
+ target_dir_change = TRUE, \
+ transfer_blood_dna = TRUE, \
+ max_blood = 4)
// player on mulebot attempted to move
/mob/living/simple_animal/bot/mulebot/relaymove(mob/living/user, direction)
diff --git a/code/modules/mob/living/simple_animal/bot/secbot.dm b/code/modules/mob/living/simple_animal/bot/secbot.dm
index bdc183d3de956..e494b1777e59d 100644
--- a/code/modules/mob/living/simple_animal/bot/secbot.dm
+++ b/code/modules/mob/living/simple_animal/bot/secbot.dm
@@ -63,6 +63,7 @@
/mob/living/simple_animal/bot/secbot/beepsky/armsky
name = "Sergeant-At-Armsky"
+ desc = "It's Sergeant-At-Armsky! He's a disgruntled assistant to the warden that would probably shoot you if he had hands."
health = 45
bot_mode_flags = ~(BOT_MODE_PAI_CONTROLLABLE|BOT_MODE_AUTOPATROL)
security_mode_flags = SECBOT_DECLARE_ARRESTS | SECBOT_CHECK_IDS | SECBOT_CHECK_RECORDS
diff --git a/code/modules/mob/living/simple_animal/constructs.dm b/code/modules/mob/living/simple_animal/constructs.dm
index 41901ae5c8cd1..fcf409ac0902b 100644
--- a/code/modules/mob/living/simple_animal/constructs.dm
+++ b/code/modules/mob/living/simple_animal/constructs.dm
@@ -42,8 +42,6 @@
var/can_repair = FALSE
/// Whether this construct can repair itself. Works independently of can_repair.
var/can_repair_self = FALSE
- var/runetype
- var/datum/action/innate/cult/create_rune/our_rune
/// Theme controls color. THEME_CULT is red THEME_WIZARD is purple and THEME_HOLY is blue
var/theme = THEME_CULT
@@ -52,29 +50,25 @@
AddElement(/datum/element/simple_flying)
ADD_TRAIT(src, TRAIT_HEALS_FROM_CULT_PYLONS, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_SPACEWALK, INNATE_TRAIT)
- var/spellnum = 1
for(var/spell in construct_spells)
- var/pos = 2+spellnum*31
+ var/datum/action/new_spell = new spell(src)
+ new_spell.Grant(src)
+
+ var/spellnum = 1
+ for(var/datum/action/spell as anything in actions)
+ if(!(type in construct_spells))
+ continue
+
+ var/pos = 2 + spellnum * 31
if(construct_spells.len >= 4)
- pos -= 31*(construct_spells.len - 4)
- var/obj/effect/proc_holder/spell/the_spell = new spell(null)
- the_spell?.action.default_button_position ="6:[pos],4:-2"
- AddSpell(the_spell)
+ pos -= 31 * (construct_spells.len - 4)
+ spell.default_button_position = "6:[pos],4:-2" // Set the default position to this random position
spellnum++
- if(runetype)
- var/pos = 2+spellnum*31
- if(construct_spells.len >= 4)
- pos -= 31*(construct_spells.len - 4)
- our_rune = new runetype(src)
- our_rune.default_button_position = "6:[pos],4:-2" // Set the default position to this random position
- our_rune.Grant(src)
+ update_action_buttons()
+
if(icon_state)
add_overlay("glow_[icon_state]_[theme]")
-/mob/living/simple_animal/hostile/construct/Destroy()
- QDEL_NULL(our_rune)
- return ..()
-
/mob/living/simple_animal/hostile/construct/Login()
. = ..()
if(!. || !client)
@@ -92,13 +86,13 @@
text_span = "purple"
if(THEME_HOLY)
text_span = "blue"
- . = list("*---------*\nThis is [icon2html(src, user)] \a [src]!\n[desc]")
+ . = list("This is [icon2html(src, user)] \a [src]!\n[desc]")
if(health < maxHealth)
if(health >= maxHealth/2)
. += span_warning("[t_He] look[t_s] slightly dented.")
else
. += span_warning("[t_He] look[t_s] severely dented!")
- . += "*---------*"
+ . += ""
/mob/living/simple_animal/hostile/construct/attack_animal(mob/living/simple_animal/user, list/modifiers)
if(isconstruct(user)) //is it a construct?
@@ -154,10 +148,10 @@
mob_size = MOB_SIZE_LARGE
force_threshold = 10
construct_spells = list(
- /obj/effect/proc_holder/spell/targeted/forcewall/cult,
- /obj/effect/proc_holder/spell/targeted/projectile/dumbfire/juggernaut
- )
- runetype = /datum/action/innate/cult/create_rune/wall
+ /datum/action/cooldown/spell/forcewall/cult,
+ /datum/action/cooldown/spell/basic_projectile/juggernaut,
+ /datum/action/innate/cult/create_rune/wall,
+ )
playstyle_string = "You are a Juggernaut. Though slow, your shell can withstand heavy punishment, \
create shield walls, rip apart enemies and walls alike, and even deflect energy weapons."
@@ -221,13 +215,18 @@
attack_verb_simple = "slash"
attack_sound = 'sound/weapons/bladeslice.ogg'
attack_vis_effect = ATTACK_EFFECT_SLASH
- construct_spells = list(/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift)
- runetype = /datum/action/innate/cult/create_rune/tele
- playstyle_string = "You are a Wraith. Though relatively fragile, you are fast, deadly, can phase through walls, and your attacks will lower the cooldown on phasing."
-
- var/attack_refund = 10 //1 second per attack
- var/crit_refund = 50 //5 seconds when putting a target into critical
- var/kill_refund = 250 //full refund on kills
+ construct_spells = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift,
+ /datum/action/innate/cult/create_rune/tele,
+ )
+ playstyle_string = "You are a Wraith. Though relatively fragile, you are fast, deadly, \
+ can phase through walls, and your attacks will lower the cooldown on phasing."
+
+ // Accomplishing various things gives you a refund on jaunt, to jump in and out.
+ /// The seconds refunded per attack
+ var/attack_refund = 1 SECONDS
+ /// The seconds refunded when putting a target into critical
+ var/crit_refund = 5 SECONDS
/mob/living/simple_animal/hostile/construct/wraith/AttackingTarget() //refund jaunt cooldown when attacking living targets
var/prev_stat
@@ -239,16 +238,23 @@
. = ..()
if(. && isnum(prev_stat))
- var/mob/living/L = target
- var/refund = 0
- if(QDELETED(L) || (L.stat == DEAD && prev_stat != DEAD)) //they're dead, you killed them
- refund += kill_refund
- else if(HAS_TRAIT(L, TRAIT_CRITICAL_CONDITION) && prev_stat == CONSCIOUS) //you knocked them into critical
- refund += crit_refund
- if(L.stat != DEAD && prev_stat != DEAD)
- refund += attack_refund
- for(var/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/S in mob_spell_list)
- S.charge_counter = min(S.charge_counter + refund, S.charge_max)
+ var/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/jaunt = locate() in actions
+ if(!jaunt)
+ return
+
+ var/total_refund = 0 SECONDS
+ // they're dead, and you killed them - full refund
+ if(QDELETED(living_target) || (living_target.stat == DEAD && prev_stat != DEAD))
+ total_refund += jaunt.cooldown_time
+ // you knocked them into critical
+ else if(HAS_TRAIT(living_target, TRAIT_CRITICAL_CONDITION) && prev_stat == CONSCIOUS)
+ total_refund += crit_refund
+
+ if(living_target.stat != DEAD && prev_stat != DEAD)
+ total_refund += attack_refund
+
+ jaunt.next_use_time -= total_refund
+ jaunt.UpdateButtons()
/mob/living/simple_animal/hostile/construct/wraith/hostile //actually hostile, will move around, hit things
AIStatus = AI_ON
@@ -256,12 +262,18 @@
//////////////////////////Wraith-alts////////////////////////////
/mob/living/simple_animal/hostile/construct/wraith/angelic
theme = THEME_HOLY
- construct_spells = list(/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/angelic)
+ construct_spells = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/angelic,
+ /datum/action/innate/cult/create_rune/tele,
+ )
loot = list(/obj/item/ectoplasm/angelic)
/mob/living/simple_animal/hostile/construct/wraith/mystic
theme = THEME_WIZARD
- construct_spells = list(/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/mystic)
+ construct_spells = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/mystic,
+ /datum/action/innate/cult/create_rune/tele,
+ )
loot = list(/obj/item/ectoplasm/mystic)
/mob/living/simple_animal/hostile/construct/wraith/noncult
@@ -288,18 +300,18 @@
environment_smash = ENVIRONMENT_SMASH_WALLS
attack_sound = 'sound/weapons/punch2.ogg'
construct_spells = list(
- /obj/effect/proc_holder/spell/aoe_turf/conjure/wall,
- /obj/effect/proc_holder/spell/aoe_turf/conjure/floor,
- /obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone,
- /obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser,
- /obj/effect/proc_holder/spell/targeted/projectile/magic_missile/lesser
- )
- runetype = /datum/action/innate/cult/create_rune/revive
- playstyle_string = "You are an Artificer. You are incredibly weak and fragile, but you are able to construct fortifications, \
-
- use magic missile, repair allied constructs, shades, and yourself (by clicking on them), \
- and, most important of all, create new constructs by producing soulstones to capture souls, \
- and shells to place those soulstones into."
+ /datum/action/cooldown/spell/conjure/cult_floor,
+ /datum/action/cooldown/spell/conjure/cult_wall,
+ /datum/action/cooldown/spell/conjure/soulstone,
+ /datum/action/cooldown/spell/conjure/construct/lesser,
+ /datum/action/cooldown/spell/aoe/magic_missile/lesser,
+ /datum/action/innate/cult/create_rune/revive,
+ )
+ playstyle_string = "You are an Artificer. You are incredibly weak and fragile, \
+ but you are able to construct fortifications, use magic missile, and repair allied constructs, shades, \
+ and yourself (by clicking on them). Additionally, and most important of all, you can create new constructs \
+ by producing soulstones to capture souls, and shells to place those soulstones into."
+
can_repair = TRUE
can_repair_self = TRUE
///The health HUD applied to this mob.
@@ -356,30 +368,32 @@
theme = THEME_HOLY
loot = list(/obj/item/ectoplasm/angelic)
construct_spells = list(
- /obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/purified,
- /obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser,
- /obj/effect/proc_holder/spell/targeted/projectile/magic_missile/lesser
- )
-
+ /datum/action/cooldown/spell/conjure/soulstone/purified,
+ /datum/action/cooldown/spell/conjure/construct/lesser,
+ /datum/action/cooldown/spell/aoe/magic_missile/lesser,
+ /datum/action/innate/cult/create_rune/revive,
+ )
/mob/living/simple_animal/hostile/construct/artificer/mystic
theme = THEME_WIZARD
loot = list(/obj/item/ectoplasm/mystic)
construct_spells = list(
- /obj/effect/proc_holder/spell/aoe_turf/conjure/wall,
- /obj/effect/proc_holder/spell/aoe_turf/conjure/floor,
- /obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/mystic,
- /obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser,
- /obj/effect/proc_holder/spell/targeted/projectile/magic_missile/lesser
- )
+ /datum/action/cooldown/spell/conjure/cult_floor,
+ /datum/action/cooldown/spell/conjure/cult_wall,
+ /datum/action/cooldown/spell/conjure/soulstone/mystic,
+ /datum/action/cooldown/spell/conjure/construct/lesser,
+ /datum/action/cooldown/spell/aoe/magic_missile/lesser,
+ /datum/action/innate/cult/create_rune/revive,
+ )
/mob/living/simple_animal/hostile/construct/artificer/noncult
construct_spells = list(
- /obj/effect/proc_holder/spell/aoe_turf/conjure/wall,
- /obj/effect/proc_holder/spell/aoe_turf/conjure/floor,
- /obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/noncult,
- /obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser,
- /obj/effect/proc_holder/spell/targeted/projectile/magic_missile/lesser
- )
+ /datum/action/cooldown/spell/conjure/cult_floor,
+ /datum/action/cooldown/spell/conjure/cult_wall,
+ /datum/action/cooldown/spell/conjure/soulstone/noncult,
+ /datum/action/cooldown/spell/conjure/construct/lesser,
+ /datum/action/cooldown/spell/aoe/magic_missile/lesser,
+ /datum/action/innate/cult/create_rune/revive,
+ )
/////////////////////////////Harvester/////////////////////////
/mob/living/simple_animal/hostile/construct/harvester
@@ -397,8 +411,10 @@
attack_verb_simple = "butcher"
attack_sound = 'sound/weapons/bladeslice.ogg'
attack_vis_effect = ATTACK_EFFECT_SLASH
- construct_spells = list(/obj/effect/proc_holder/spell/aoe_turf/area_conversion,
- /obj/effect/proc_holder/spell/targeted/forcewall/cult)
+ construct_spells = list(
+ /datum/action/cooldown/spell/aoe/area_conversion,
+ /datum/action/cooldown/spell/forcewall/cult,
+ )
playstyle_string = "You are a Harvester. You are incapable of directly killing humans, but your attacks will remove their limbs: \
Bring those who still cling to this world of illusion back to the Geometer so they may know Truth. Your form and any you are pulling can pass through runed walls effortlessly."
can_repair = TRUE
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
index 58249e36f3995..48547d76359b0 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
+++ b/code/modules/mob/living/simple_animal/friendly/drone/_drone.dm
@@ -186,6 +186,7 @@
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_NEGATES_GRAVITY, INNATE_TRAIT)
+ ADD_TRAIT(src, TRAIT_LITERATE, INNATE_TRAIT)
listener = new(list(ALARM_ATMOS, ALARM_FIRE, ALARM_POWER), list(z))
RegisterSignal(listener, COMSIG_ALARM_TRIGGERED, .proc/alarm_triggered)
@@ -249,7 +250,7 @@
dust()
/mob/living/simple_animal/drone/examine(mob/user)
- . = list("*---------*\nThis is [icon2html(src, user)] \a [src]!")
+ . = list("This is [icon2html(src, user)] \a [src]!")
//Hands
for(var/obj/item/I in held_items)
@@ -285,7 +286,7 @@
. += span_deadsay("A message repeatedly flashes on its display: \"REBOOT -- REQUIRED\".")
else
. += span_deadsay("A message repeatedly flashes on its display: \"ERROR -- OFFLINE\".")
- . += "*---------*"
+ . += ""
/mob/living/simple_animal/drone/assess_threat(judgement_criteria, lasercolor = "", datum/callback/weaponcheck=null) //Secbots won't hunt maintenance drones.
diff --git a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm b/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm
index 6ec3f9f3d062c..ed9e032902d46 100644
--- a/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm
+++ b/code/modules/mob/living/simple_animal/friendly/drone/inventory.dm
@@ -40,8 +40,15 @@
return head
if(ITEM_SLOT_DEX_STORAGE)
return internal_storage
+
return ..()
+/mob/living/simple_animal/drone/get_slot_by_item(obj/item/looking_for)
+ if(internal_storage == looking_for)
+ return ITEM_SLOT_DEX_STORAGE
+ if(head == looking_for)
+ return ITEM_SLOT_HEAD
+ return ..()
/mob/living/simple_animal/drone/equip_to_slot(obj/item/I, slot)
if(!slot)
diff --git a/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm b/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
index 70db007d955ab..9da8cd8dd0384 100644
--- a/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
+++ b/code/modules/mob/living/simple_animal/guardian/types/dextrous.dm
@@ -19,13 +19,13 @@
/mob/living/simple_animal/hostile/guardian/dextrous/examine(mob/user)
if(dextrous)
- . = list("*---------*\nThis is [icon2html(src)] \a [src]!\n[desc]")
+ . = list("This is [icon2html(src)] \a [src]!\n[desc]")
for(var/obj/item/I in held_items)
if(!(I.item_flags & ABSTRACT))
. += "It has [I.get_examine_string(user)] in its [get_held_index_name(get_held_index_of_item(I))]."
if(internal_storage && !(internal_storage.item_flags & ABSTRACT))
. += "It is holding [internal_storage.get_examine_string(user)] in its internal storage."
- . += "*---------*"
+ . += ""
else
return ..()
@@ -58,6 +58,16 @@
return TRUE
..()
+/mob/living/simple_animal/hostile/guardian/dextrous/get_item_by_slot(slot_id)
+ if(slot_id == ITEM_SLOT_DEX_STORAGE)
+ return internal_storage
+ return ..()
+
+/mob/living/simple_animal/hostile/guardian/dextrous/get_slot_by_item(obj/item/looking_for)
+ if(internal_storage == looking_for)
+ return ITEM_SLOT_DEX_STORAGE
+ return ..()
+
/mob/living/simple_animal/hostile/guardian/dextrous/equip_to_slot(obj/item/I, slot)
if(!..())
return
diff --git a/code/modules/mob/living/simple_animal/heretic_monsters.dm b/code/modules/mob/living/simple_animal/heretic_monsters.dm
index 91469ab0bb9ba..f2f01b091f452 100644
--- a/code/modules/mob/living/simple_animal/heretic_monsters.dm
+++ b/code/modules/mob/living/simple_animal/heretic_monsters.dm
@@ -32,21 +32,15 @@
loot = list(/obj/effect/gibspawner/human)
faction = list(FACTION_HERETIC)
simple_mob_flags = SILENCE_RANGED_MESSAGE
+
/// Innate spells that are added when a beast is created.
- var/list/spells_to_add
+ var/list/actions_to_add
/mob/living/simple_animal/hostile/heretic_summon/Initialize(mapload)
. = ..()
- add_spells()
-
-/**
- * Add_spells
- *
- * Goes through spells_to_add and adds each spell to the mind.
- */
-/mob/living/simple_animal/hostile/heretic_summon/proc/add_spells()
- for(var/spell in spells_to_add)
- AddSpell(new spell())
+ for(var/spell in actions_to_add)
+ var/datum/action/cooldown/spell/new_spell = new spell(src)
+ new_spell.Grant(src)
/mob/living/simple_animal/hostile/heretic_summon/raw_prophet
name = "Raw Prophet"
@@ -61,10 +55,11 @@
health = 65
sight = SEE_MOBS|SEE_OBJS|SEE_TURFS
loot = list(/obj/effect/gibspawner/human, /obj/item/bodypart/l_arm, /obj/item/organ/internal/eyes)
- spells_to_add = list(
- /obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash/long,
- /obj/effect/proc_holder/spell/targeted/telepathy/eldritch,
- /obj/effect/proc_holder/spell/pointed/trigger/blind/eldritch,
+ actions_to_add = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash/long,
+ /datum/action/cooldown/spell/list_target/telepathy/eldritch,
+ /datum/action/cooldown/spell/pointed/blind/eldritch,
+ /datum/action/innate/expand_sight,
)
/// A weakref to the last target we smacked. Hitting targets consecutively does more damage.
var/datum/weakref/last_target
@@ -79,16 +74,13 @@
AddComponent(/datum/component/mind_linker, \
network_name = "Mansus Link", \
chat_color = "#568b00", \
- linker_action_path = /datum/action/cooldown/manse_link, \
+ linker_action_path = /datum/action/cooldown/spell/pointed/manse_link, \
link_message = on_link_message, \
unlink_message = on_unlink_message, \
post_unlink_callback = CALLBACK(src, .proc/after_unlink), \
speech_action_background_icon_state = "bg_ecult", \
)
- var/datum/action/innate/expand_sight/sight_seer = new(src)
- sight_seer.Grant(src)
-
/mob/living/simple_animal/hostile/heretic_summon/raw_prophet/attack_animal(mob/living/simple_animal/user, list/modifiers)
if(user == src) // Easy to hit yourself + very fragile = accidental suicide, prevent that
return
@@ -155,7 +147,7 @@
ranged_cooldown_time = 5
ranged = TRUE
rapid = 1
- spells_to_add = list(/obj/effect/proc_holder/spell/targeted/worm_contract)
+ actions_to_add = list(/datum/action/cooldown/spell/worm_contract)
///Previous segment in the chain
var/mob/living/simple_animal/hostile/heretic_summon/armsy/back
///Next segment in the chain
@@ -187,7 +179,9 @@
if(!spawn_bodyparts)
return
- AddElement(/datum/element/blood_walk, /obj/effect/decal/cleanable/blood/tracks, target_dir_change = TRUE)
+ AddComponent(/datum/component/blood_walk, \
+ blood_type = /obj/effect/decal/cleanable/blood/tracks, \
+ target_dir_change = TRUE)
allow_pulling = TRUE
// Sets the hp of the head to be exactly the (length * hp), so the head is de facto the hardest to destroy.
@@ -363,9 +357,9 @@
melee_damage_lower = 15
melee_damage_upper = 20
sight = SEE_TURFS
- spells_to_add = list(
- /obj/effect/proc_holder/spell/aoe_turf/rust_conversion/small,
- /obj/effect/proc_holder/spell/targeted/projectile/dumbfire/rust_wave/short,
+ actions_to_add = list(
+ /datum/action/cooldown/spell/aoe/rust_conversion/small,
+ /datum/action/cooldown/spell/basic_projectile/rust_wave/short,
)
/mob/living/simple_animal/hostile/heretic_summon/rust_spirit/setDir(newdir)
@@ -403,10 +397,10 @@
melee_damage_lower = 15
melee_damage_upper = 20
sight = SEE_TURFS
- spells_to_add = list(
- /obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash,
- /obj/effect/proc_holder/spell/pointed/cleave,
- /obj/effect/proc_holder/spell/targeted/fire_sworn,
+ actions_to_add = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash,
+ /datum/action/cooldown/spell/pointed/cleave,
+ /datum/action/cooldown/spell/fire_sworn,
)
/mob/living/simple_animal/hostile/heretic_summon/stalker
@@ -421,8 +415,8 @@
melee_damage_lower = 15
melee_damage_upper = 20
sight = SEE_MOBS
- spells_to_add = list(
- /obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/ash,
- /obj/effect/proc_holder/spell/targeted/shapeshift/eldritch,
- /obj/effect/proc_holder/spell/targeted/emplosion/eldritch,
+ actions_to_add = list(
+ /datum/action/cooldown/spell/jaunt/ethereal_jaunt/ash,
+ /datum/action/cooldown/spell/shapeshift/eldritch,
+ /datum/action/cooldown/spell/emp/eldritch,
)
diff --git a/code/modules/mob/living/simple_animal/hostile/carp.dm b/code/modules/mob/living/simple_animal/hostile/carp.dm
index c41b19b371136..401ad2aaa99c1 100644
--- a/code/modules/mob/living/simple_animal/hostile/carp.dm
+++ b/code/modules/mob/living/simple_animal/hostile/carp.dm
@@ -103,7 +103,6 @@
/mob/living/simple_animal/hostile/carp/proc/tamed(mob/living/tamer)
tamed = TRUE
- can_buckle = TRUE
buckle_lying = 0
AddElement(/datum/element/ridable, /datum/component/riding/creature/carp)
if(ai_controller)
diff --git a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
index 0f02e0272ccbe..379042ed4167f 100644
--- a/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
+++ b/code/modules/mob/living/simple_animal/hostile/giant_spider.dm
@@ -1,3 +1,4 @@
+#define INTERACTION_SPIDER_KEY "spider_key"
/**
* # Giant Spider
@@ -50,14 +51,10 @@
var/poison_per_bite = 0
///What reagent the mob injects targets with
var/poison_type = /datum/reagent/toxin
- ///Whether or not the spider is in the middle of an action.
- var/is_busy = FALSE
///How quickly the spider can place down webbing. One is base speed, larger numbers are slower.
var/web_speed = 1
///Whether or not the spider can create sealed webs.
var/web_sealer = FALSE
- ///The web laying ability
- var/datum/action/innate/spider/lay_web/lay_web
///The message that the mother spider left for this spider when the egg was layed.
var/directive = ""
/// Short description of what this mob is capable of, for radial menu uses
@@ -65,8 +62,9 @@
/mob/living/simple_animal/hostile/giant_spider/Initialize(mapload)
. = ..()
- lay_web = new
- lay_web.Grant(src)
+ var/datum/action/innate/spider/lay_web/webbing = new(src)
+ webbing.Grant(src)
+
if(poison_per_bite)
AddElement(/datum/element/venomous, poison_type, poison_per_bite)
AddElement(/datum/element/nerfed_pulling, GLOB.typecache_general_bad_things_to_easily_move)
@@ -145,7 +143,7 @@
datahud.show_to(src)
/mob/living/simple_animal/hostile/giant_spider/nurse/AttackingTarget()
- if(is_busy)
+ if(DOING_INTERACTION(src, INTERACTION_SPIDER_KEY))
return
if(!istype(target, /mob/living/simple_animal/hostile/giant_spider))
return ..()
@@ -159,13 +157,20 @@
if(hurt_spider.stat == DEAD)
to_chat(src, span_warning("You're a nurse, not a miracle worker."))
return
- visible_message(span_notice("[src] begins wrapping the wounds of [hurt_spider]."),span_notice("You begin wrapping the wounds of [hurt_spider]."))
- is_busy = TRUE
- if(do_after(src, 20, target = hurt_spider))
- hurt_spider.heal_overall_damage(20, 20)
- new /obj/effect/temp_visual/heal(get_turf(hurt_spider), "#80F5FF")
- visible_message(span_notice("[src] wraps the wounds of [hurt_spider]."),span_notice("You wrap the wounds of [hurt_spider]."))
- is_busy = FALSE
+ visible_message(
+ span_notice("[src] begins wrapping the wounds of [hurt_spider]."),
+ span_notice("You begin wrapping the wounds of [hurt_spider]."),
+ )
+
+ if(!do_after(src, 2 SECONDS, target = hurt_spider, interaction_key = INTERACTION_SPIDER_KEY))
+ return
+
+ hurt_spider.heal_overall_damage(20, 20)
+ new /obj/effect/temp_visual/heal(get_turf(hurt_spider), "#80F5FF")
+ visible_message(
+ span_notice("[src] wraps the wounds of [hurt_spider]."),
+ span_notice("You wrap the wounds of [hurt_spider]."),
+ )
/**
* # Tarantula
@@ -273,228 +278,238 @@
gold_core_spawnable = NO_SPAWN
web_sealer = TRUE
menu_description = "Royal spider variant specializing in reproduction and leadership, but has very low amount of health and deals low damage."
- ///If the spider is trying to cocoon something, what that something is.
- var/atom/movable/cocoon_target
- ///How many humans this spider has drained but not layed enriched eggs for.
- var/fed = 0
- ///How long it takes for a broodmother to lay eggs.
- var/egg_lay_time = 15 SECONDS
- ///The ability for the spider to wrap targets.
- var/obj/effect/proc_holder/wrap/wrap
- ///The ability for the spider to lay basic eggs.
- var/datum/action/innate/spider/lay_eggs/lay_eggs
- ///The ability for the spider to lay enriched eggs.
- var/datum/action/innate/spider/lay_eggs/enriched/lay_eggs_enriched
- ///The ability for the spider to set a directive, a message shown to the child spider player when the player takes control.
- var/datum/action/innate/spider/set_directive/set_directive
- ///A shared list of all the mobs consumed by any spider so that the same target can't be drained several times.
- var/static/list/consumed_mobs = list() //the tags of mobs that have been consumed by nurse spiders to lay eggs
- ///The ability for the spider to send a message to all currently living spiders.
- var/datum/action/innate/spider/comm/letmetalkpls
/mob/living/simple_animal/hostile/giant_spider/midwife/Initialize(mapload)
. = ..()
- wrap = new
- AddAbility(wrap)
- lay_eggs = new
- lay_eggs.Grant(src)
- lay_eggs_enriched = new
- lay_eggs_enriched.Grant(src)
- set_directive = new
- set_directive.Grant(src)
- letmetalkpls = new
- letmetalkpls.Grant(src)
+ var/datum/action/cooldown/wrap/wrapping = new(src)
+ wrapping.Grant(src)
-/**
- * Attempts to cocoon the spider's current cocoon_target.
- *
- * Attempts to coccon the spider's cocoon_target after a do_after.
- * If the target is a human who hasn't been drained before, ups the spider's fed counter so it can lay enriched eggs.
- */
-/mob/living/simple_animal/hostile/giant_spider/midwife/proc/cocoon()
- if(stat == DEAD || !cocoon_target || cocoon_target.anchored)
- return
- if(cocoon_target == src)
- to_chat(src, span_warning("You can't wrap yourself!"))
- return
- if(istype(cocoon_target, /mob/living/simple_animal/hostile/giant_spider))
- to_chat(src, span_warning("You can't wrap other spiders!"))
- return
- if(!Adjacent(cocoon_target))
- to_chat(src, span_warning("You can't reach [cocoon_target]!"))
- return
- if(is_busy)
- to_chat(src, span_warning("You're already doing something else!"))
- return
- is_busy = TRUE
- visible_message(span_notice("[src] begins to secrete a sticky substance around [cocoon_target]."),span_notice("You begin wrapping [cocoon_target] into a cocoon."))
- stop_automated_movement = TRUE
- if(do_after(src, 50, target = cocoon_target))
- if(is_busy)
- var/obj/structure/spider/cocoon/casing = new(cocoon_target.loc)
- if(isliving(cocoon_target))
- var/mob/living/living_target = cocoon_target
- if(ishuman(living_target) && (living_target.stat != DEAD || !consumed_mobs[living_target.tag])) //if they're not dead, you can consume them anyway
- consumed_mobs[living_target.tag] = TRUE
- fed++
- lay_eggs_enriched.UpdateButtons(TRUE)
- visible_message(span_danger("[src] sticks a proboscis into [living_target] and sucks a viscous substance out."),span_notice("You suck the nutriment out of [living_target], feeding you enough to lay a cluster of eggs."))
- living_target.death() //you just ate them, they're dead.
- else
- to_chat(src, span_warning("[living_target] cannot sate your hunger!"))
- cocoon_target.forceMove(casing)
- if(cocoon_target.density || ismob(cocoon_target))
- casing.icon_state = pick("cocoon_large1","cocoon_large2","cocoon_large3")
- cocoon_target = null
- is_busy = FALSE
- stop_automated_movement = FALSE
+ var/datum/action/innate/spider/lay_eggs/make_eggs = new(src)
+ make_eggs.Grant(src)
+
+ var/datum/action/innate/spider/lay_eggs/enriched/make_better_eggs = new(src)
+ make_better_eggs.Grant(src)
+
+ var/datum/action/innate/spider/set_directive/give_orders = new(src)
+ give_orders.Grant(src)
+
+ var/datum/action/innate/spider/comm/not_hivemind_talk = new(src)
+ not_hivemind_talk.Grant(src)
/datum/action/innate/spider
icon_icon = 'icons/mob/actions/actions_animal.dmi'
background_icon_state = "bg_alien"
-/datum/action/innate/spider/lay_web
+/datum/action/innate/spider/lay_web // Todo: Unify this with the genetics power
name = "Spin Web"
desc = "Spin a web to slow down potential prey."
check_flags = AB_CHECK_CONSCIOUS
button_icon_state = "lay_web"
-/datum/action/innate/spider/lay_web/Activate()
- if(!istype(owner, /mob/living/simple_animal/hostile/giant_spider))
- return
+/datum/action/innate/spider/lay_web/IsAvailable()
+ . = ..()
+ if(!.)
+ return FALSE
+
+ if(DOING_INTERACTION(owner, INTERACTION_SPIDER_KEY))
+ return FALSE
+ if(!isspider(owner))
+ return FALSE
+
var/mob/living/simple_animal/hostile/giant_spider/spider = owner
+ var/obj/structure/spider/stickyweb/web = locate() in get_turf(spider)
+ if(web && (!spider.web_sealer || istype(web, /obj/structure/spider/stickyweb/sealed)))
+ to_chat(owner, span_warning("There's already a web here!"))
+ return FALSE
if(!isturf(spider.loc))
- return
- var/turf/spider_turf = get_turf(spider)
+ return FALSE
+
+ return TRUE
+/datum/action/innate/spider/lay_web/Activate()
+ var/turf/spider_turf = get_turf(owner)
+ var/mob/living/simple_animal/hostile/giant_spider/spider = owner
var/obj/structure/spider/stickyweb/web = locate() in spider_turf
if(web)
- if(!spider.web_sealer || istype(web, /obj/structure/spider/stickyweb/sealed))
- to_chat(spider, span_warning("There's already a web here!"))
- return
-
- if(!spider.is_busy)
- spider.is_busy = TRUE
- if(web)
- spider.visible_message(span_notice("[spider] begins to pack more webbing onto the web."),span_notice("You begin to seal the web."))
- else
- spider.visible_message(span_notice("[spider] begins to secrete a sticky substance."),span_notice("You begin to lay a web."))
- spider.stop_automated_movement = TRUE
- if(do_after(spider, 40 * spider.web_speed, target = spider_turf))
- if(spider.is_busy && spider.loc == spider_turf)
- if(web)
- qdel(web)
- new /obj/structure/spider/stickyweb/sealed(spider_turf)
- new /obj/structure/spider/stickyweb(spider_turf)
- spider.is_busy = FALSE
- spider.stop_automated_movement = FALSE
+ spider.visible_message(
+ span_notice("[spider] begins to pack more webbing onto the web."),
+ span_notice("You begin to seal the web."),
+ )
else
- to_chat(spider, span_warning("You're already doing something else!"))
+ spider.visible_message(
+ span_notice("[spider] begins to secrete a sticky substance."),
+ span_notice("You begin to lay a web."),
+ )
+
+ spider.stop_automated_movement = TRUE
+
+ if(do_after(spider, 4 SECONDS * spider.web_speed, target = spider_turf))
+ if(spider.loc == spider_turf)
+ if(web)
+ qdel(web)
+ new /obj/structure/spider/stickyweb/sealed(spider_turf)
+ new /obj/structure/spider/stickyweb(spider_turf)
-/obj/effect/proc_holder/wrap
+ spider.stop_automated_movement = FALSE
+
+/datum/action/cooldown/wrap
name = "Wrap"
- panel = "Spider"
- desc = "Wrap something or someone in a cocoon. If it's a human or similar species, you'll also consume them, allowing you to lay enriched eggs."
+ desc = "Wrap something or someone in a cocoon. If it's a human or similar species, \
+ you'll also consume them, allowing you to lay enriched eggs."
+ background_icon_state = "bg_alien"
+ icon_icon = 'icons/mob/actions/actions_animal.dmi'
+ button_icon_state = "wrap_0"
+ check_flags = AB_CHECK_CONSCIOUS
+ click_to_activate = TRUE
ranged_mousepointer = 'icons/effects/mouse_pointers/wrap_target.dmi'
- action_icon = 'icons/mob/actions/actions_animal.dmi'
- action_icon_state = "wrap_0"
- action_background_icon_state = "bg_alien"
-
-/obj/effect/proc_holder/wrap/update_icon()
- action.button_icon_state = "wrap_[active]"
- action.UpdateButtons()
- return ..()
+ /// The time it takes to wrap something.
+ var/wrap_time = 5 SECONDS
-/obj/effect/proc_holder/wrap/Click()
- if(!istype(usr, /mob/living/simple_animal/hostile/giant_spider/midwife))
- return TRUE
- var/mob/living/simple_animal/hostile/giant_spider/midwife/user = usr
- activate(user)
+/datum/action/cooldown/wrap/IsAvailable()
+ . = ..()
+ if(!.)
+ return FALSE
+ if(owner.incapacitated())
+ return FALSE
+ if(DOING_INTERACTION(owner, INTERACTION_SPIDER_KEY))
+ return FALSE
return TRUE
-/obj/effect/proc_holder/wrap/proc/activate(mob/living/user)
- var/message
- if(active)
- message = span_notice("You no longer prepare to wrap something in a cocoon.")
- remove_ranged_ability(message)
- else
- message = span_notice("You prepare to wrap something in a cocoon. Left-click your target to start wrapping!")
- add_ranged_ability(user, message, TRUE)
- return TRUE
-
-/obj/effect/proc_holder/wrap/InterceptClickOn(mob/living/caller, params, atom/target)
- if(..())
+/datum/action/cooldown/wrap/set_click_ability(mob/on_who)
+ . = ..()
+ if(!.)
return
- if(ranged_ability_user.incapacitated() || !istype(ranged_ability_user, /mob/living/simple_animal/hostile/giant_spider/midwife))
- remove_ranged_ability()
+
+ to_chat(on_who, span_notice("You prepare to wrap something in a cocoon. Left-click your target to start wrapping!"))
+ button_icon_state = "wrap_0"
+ UpdateButtons()
+
+/datum/action/cooldown/wrap/unset_click_ability(mob/on_who, refund_cooldown = TRUE)
+ . = ..()
+ if(!.)
return
- var/mob/living/simple_animal/hostile/giant_spider/midwife/user = ranged_ability_user
+ if(refund_cooldown)
+ to_chat(on_who, span_notice("You no longer prepare to wrap something in a cocoon."))
+ button_icon_state = "wrap_1"
+ UpdateButtons()
- if(user.Adjacent(target) && (ismob(target) || isobj(target)))
- var/atom/movable/target_atom = target
- if(target_atom.anchored)
- return
- user.cocoon_target = target_atom
- INVOKE_ASYNC(user, /mob/living/simple_animal/hostile/giant_spider/midwife/.proc/cocoon)
- remove_ranged_ability()
- return TRUE
+/datum/action/cooldown/wrap/Activate(atom/to_wrap)
+ if(!owner.Adjacent(to_wrap))
+ owner.balloon_alert(owner, "must be closer!")
+ return FALSE
-/obj/effect/proc_holder/wrap/on_lose(mob/living/carbon/user)
- remove_ranged_ability()
+ if(!ismob(to_wrap) && !isobj(to_wrap))
+ return FALSE
+
+ if(to_wrap == owner)
+ return FALSE
+
+ if(isspider(to_wrap))
+ owner.balloon_alert(owner, "can't wrap spiders!")
+ return FALSE
+
+ var/atom/movable/target_movable = to_wrap
+ if(target_movable.anchored)
+ return FALSE
+
+ StartCooldown(wrap_time)
+ INVOKE_ASYNC(src, .proc/cocoon, to_wrap)
+ return TRUE
+
+/datum/action/cooldown/wrap/proc/cocoon(atom/movable/to_wrap)
+ owner.visible_message(
+ span_notice("[owner] begins to secrete a sticky substance around [to_wrap]."),
+ span_notice("You begin wrapping [to_wrap] into a cocoon."),
+ )
+
+ var/mob/living/simple_animal/animal_owner = owner
+ if(istype(animal_owner))
+ animal_owner.stop_automated_movement = TRUE
+
+ if(do_after(owner, wrap_time, target = to_wrap, interaction_key = INTERACTION_SPIDER_KEY))
+ var/obj/structure/spider/cocoon/casing = new(to_wrap.loc)
+ if(isliving(to_wrap))
+ var/mob/living/living_wrapped = to_wrap
+ // if they're not dead, you can consume them anyway
+ if(ishuman(living_wrapped) && (living_wrapped.stat != DEAD || !HAS_TRAIT(living_wrapped, TRAIT_SPIDER_CONSUMED)))
+ var/datum/action/innate/spider/lay_eggs/enriched/egg_power = locate() in owner.actions
+ if(egg_power)
+ egg_power.charges++
+ egg_power.UpdateButtons()
+ owner.visible_message(
+ span_danger("[owner] sticks a proboscis into [living_wrapped] and sucks a viscous substance out."),
+ span_notice("You suck the nutriment out of [living_wrapped], feeding you enough to lay a cluster of enriched eggs."),
+ )
+
+ living_wrapped.death() //you just ate them, they're dead.
+ else
+ to_chat(owner, span_warning("[living_wrapped] cannot sate your hunger!"))
+
+ to_wrap.forceMove(casing)
+ if(to_wrap.density || ismob(to_wrap))
+ casing.icon_state = pick("cocoon_large1", "cocoon_large2", "cocoon_large3")
+
+ if(istype(animal_owner))
+ animal_owner.stop_automated_movement = TRUE
/datum/action/innate/spider/lay_eggs
name = "Lay Eggs"
desc = "Lay a cluster of eggs, which will soon grow into a normal spider."
check_flags = AB_CHECK_CONSCIOUS
button_icon_state = "lay_eggs"
- var/enriched = FALSE
+ ///How long it takes for a broodmother to lay eggs.
+ var/egg_lay_time = 15 SECONDS
+ ///The type of egg we create
+ var/egg_type = /obj/effect/mob_spawn/ghost_role/spider
/datum/action/innate/spider/lay_eggs/IsAvailable()
. = ..()
if(!.)
- return
- if(!istype(owner, /mob/living/simple_animal/hostile/giant_spider/midwife))
return FALSE
- var/mob/living/simple_animal/hostile/giant_spider/midwife/S = owner
- if(enriched && !S.fed)
+
+ if(!isspider(owner))
+ return FALSE
+ var/obj/structure/spider/eggcluster/eggs = locate() in get_turf(owner)
+ if(eggs)
+ to_chat(owner, span_warning("There is already a cluster of eggs here!"))
+ return FALSE
+ if(DOING_INTERACTION(owner, INTERACTION_SPIDER_KEY))
return FALSE
+
return TRUE
/datum/action/innate/spider/lay_eggs/Activate()
- if(!istype(owner, /mob/living/simple_animal/hostile/giant_spider/midwife))
- return
- var/mob/living/simple_animal/hostile/giant_spider/midwife/spider = owner
- var/obj/structure/spider/eggcluster/eggs = locate() in get_turf(spider)
- if(eggs)
- to_chat(spider, span_warning("There is already a cluster of eggs here!"))
- else if(enriched && !spider.fed)
- to_chat(spider, span_warning("You are too hungry to do this!"))
- else if(!spider.is_busy)
- spider.is_busy = TRUE
- spider.visible_message(span_notice("[spider] begins to lay a cluster of eggs."),span_notice("You begin to lay a cluster of eggs."))
- spider.stop_automated_movement = TRUE
- if(do_after(spider, spider.egg_lay_time, target = get_turf(spider)))
- if(spider.is_busy)
- eggs = locate() in get_turf(spider)
- if(!eggs || !isturf(spider.loc))
- var/egg_choice = enriched ? /obj/effect/mob_spawn/ghost_role/spider/enriched : /obj/effect/mob_spawn/ghost_role/spider
- var/obj/effect/mob_spawn/ghost_role/spider/new_eggs = new egg_choice(get_turf(spider))
- new_eggs.directive = spider.directive
- new_eggs.faction = spider.faction
- if(enriched)
- spider.fed--
- UpdateButtons(TRUE)
- spider.is_busy = FALSE
- spider.stop_automated_movement = FALSE
+ owner.visible_message(
+ span_notice("[owner] begins to lay a cluster of eggs."),
+ span_notice("You begin to lay a cluster of eggs."),
+ )
+
+ var/mob/living/simple_animal/hostile/giant_spider/spider = owner
+ spider.stop_automated_movement = TRUE
+
+ if(do_after(owner, egg_lay_time, target = get_turf(owner), interaction_key = INTERACTION_SPIDER_KEY))
+ var/obj/structure/spider/eggcluster/eggs = locate() in get_turf(owner)
+ if(!eggs || !isturf(spider.loc))
+ var/obj/effect/mob_spawn/ghost_role/spider/new_eggs = new egg_type(get_turf(spider))
+ new_eggs.directive = spider.directive
+ new_eggs.faction = spider.faction
+ UpdateButtons(TRUE)
+
+ spider.stop_automated_movement = FALSE
/datum/action/innate/spider/lay_eggs/enriched
name = "Lay Enriched Eggs"
desc = "Lay a cluster of eggs, which will soon grow into a greater spider. Requires you drain a human per cluster of these eggs."
button_icon_state = "lay_enriched_eggs"
- enriched = TRUE
+ egg_type = /obj/effect/mob_spawn/ghost_role/spider/enriched
+ /// How many charges we have to make eggs
+ var/charges = 0
+
+/datum/action/innate/spider/lay_eggs/enriched/IsAvailable()
+ return ..() && (charges > 0)
/datum/action/innate/spider/set_directive
name = "Set Directive"
@@ -503,20 +518,18 @@
button_icon_state = "directive"
/datum/action/innate/spider/set_directive/IsAvailable()
- if(..())
- if(!istype(owner, /mob/living/simple_animal/hostile/giant_spider))
- return FALSE
- return TRUE
+ return ..() && istype(owner, /mob/living/simple_animal/hostile/giant_spider)
/datum/action/innate/spider/set_directive/Activate()
- if(!istype(owner, /mob/living/simple_animal/hostile/giant_spider/midwife))
- return
var/mob/living/simple_animal/hostile/giant_spider/midwife/spider = owner
+
spider.directive = tgui_input_text(spider, "Enter the new directive", "Create directive", "[spider.directive]")
- if(isnull(spider.directive))
- return
+ if(isnull(spider.directive) || QDELETED(src) || QDELETED(owner) || !IsAvailable())
+ return FALSE
+
message_admins("[ADMIN_LOOKUPFLW(owner)] set its directive to: '[spider.directive]'.")
log_game("[key_name(owner)] set its directive to: '[spider.directive]'.")
+ return TRUE
/datum/action/innate/spider/comm
name = "Command"
@@ -525,15 +538,13 @@
button_icon_state = "command"
/datum/action/innate/spider/comm/IsAvailable()
- if(..())
- if(!istype(owner, /mob/living/simple_animal/hostile/giant_spider/midwife))
- return FALSE
- return TRUE
+ return ..() && istype(owner, /mob/living/simple_animal/hostile/giant_spider/midwife)
/datum/action/innate/spider/comm/Trigger(trigger_flags)
var/input = tgui_input_text(owner, "Input a command for your legions to follow.", "Command")
- if(QDELETED(src) || !input || !IsAvailable())
+ if(!input || QDELETED(src) || QDELETED(owner) || !IsAvailable())
return FALSE
+
spider_command(owner, input)
return TRUE
@@ -550,12 +561,12 @@
return
var/my_message
my_message = span_spider("Command from [user]: [message]")
- for(var/mob/living/simple_animal/hostile/giant_spider/spider in GLOB.spidermobs)
+ for(var/mob/living/simple_animal/hostile/giant_spider/spider as anything in GLOB.spidermobs)
to_chat(spider, my_message)
for(var/ghost in GLOB.dead_mob_list)
var/link = FOLLOW_LINK(ghost, user)
to_chat(ghost, "[link] [my_message]")
- usr.log_talk(message, LOG_SAY, tag="spider command")
+ user.log_talk(message, LOG_SAY, tag = "spider command")
/**
* # Giant Ice Spider
@@ -675,22 +686,22 @@
/mob/living/simple_animal/hostile/giant_spider/hunter/flesh/Initialize(mapload)
. = ..()
- AddElement(/datum/element/blood_walk, /obj/effect/decal/cleanable/blood/bubblegum, blood_spawn_chance = 5)
+ AddComponent(/datum/component/blood_walk, \
+ blood_type = /obj/effect/decal/cleanable/blood/bubblegum, \
+ blood_spawn_chance = 5)
/mob/living/simple_animal/hostile/giant_spider/hunter/flesh/AttackingTarget()
- if(is_busy)
+ if(DOING_INTERACTION(src, INTERACTION_SPIDER_KEY))
return
if(src == target)
if(health >= maxHealth)
to_chat(src, span_warning("You're not injured, there's no reason to heal."))
return
visible_message(span_notice("[src] begins mending themselves..."),span_notice("You begin mending your wounds..."))
- is_busy = TRUE
- if(do_after(src, 20, target = src))
+ if(do_after(src, 2 SECONDS, target = src, interaction_key = INTERACTION_SPIDER_KEY))
heal_overall_damage(50, 50)
new /obj/effect/temp_visual/heal(get_turf(src), "#80F5FF")
visible_message(span_notice("[src]'s wounds mend together."),span_notice("You mend your wounds together."))
- is_busy = FALSE
return
return ..()
@@ -709,3 +720,5 @@
/mob/living/simple_animal/hostile/giant_spider/viper/wizard/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS, INNATE_TRAIT)
+
+#undef INTERACTION_SPIDER_KEY
diff --git a/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm b/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm
index 19b695b19ba17..85a3887389528 100644
--- a/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm
+++ b/code/modules/mob/living/simple_animal/hostile/gorilla/gorilla.dm
@@ -21,7 +21,7 @@
response_disarm_simple = "challenge"
response_harm_continuous = "thumps"
response_harm_simple = "thump"
- speed = 1
+ speed = 0.5
melee_damage_lower = 15
melee_damage_upper = 18
damage_coeff = list(BRUTE = 1, BURN = 1.5, TOX = 1.5, CLONE = 0, STAMINA = 0, OXY = 1.5)
@@ -32,7 +32,7 @@
attack_sound = 'sound/weapons/punch1.ogg'
dextrous = TRUE
held_items = list(null, null)
- faction = list("jungle")
+ faction = list("monkey", "jungle")
robust_searching = TRUE
stat_attack = HARD_CRIT
minbodytemp = 270
@@ -110,17 +110,19 @@
return FALSE
/mob/living/simple_animal/hostile/gorilla/proc/oogaooga()
- oogas++
- if(oogas >= rand(2,6))
+ oogas -= 1
+ if(oogas <= 0)
+ oogas = rand(2,6)
playsound(src, 'sound/creatures/gorilla.ogg', 50)
- oogas = 0
+
/mob/living/simple_animal/hostile/gorilla/cargo_domestic
name = "Cargorilla" // Overriden, normally
- desc = "Cargo's pet gorilla."
+ icon = 'icons/mob/cargorillia.dmi'
+ desc = "Cargo's pet gorilla. They seem to have an 'I love Mom' tattoo."
maxHealth = 200
health = 200
- faction = list(FACTION_STATION)
+ faction = list("neutral", "monkey", "jungle")
gold_core_spawnable = NO_SPAWN
unique_name = FALSE
/// Whether we're currently being polled over
diff --git a/code/modules/mob/living/simple_animal/hostile/gorilla/visuals_icons.dm b/code/modules/mob/living/simple_animal/hostile/gorilla/visuals_icons.dm
index 9d451748b059c..8ab5a0aa0b1c4 100644
--- a/code/modules/mob/living/simple_animal/hostile/gorilla/visuals_icons.dm
+++ b/code/modules/mob/living/simple_animal/hostile/gorilla/visuals_icons.dm
@@ -21,11 +21,11 @@
if(!standing)
if(stat != DEAD)
icon_state = "crawling"
- speed = 1
+ set_varspeed(0.5)
return ..()
if(stat != DEAD)
icon_state = "standing"
- speed = 3 // Gorillas are slow when standing up.
+ set_varspeed(1) // Gorillas are slow when standing up.
var/list/hands_overlays = list()
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm
index 06843b089142f..bdb1e8ac93d0b 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/bubblegum.dm
@@ -100,7 +100,10 @@ Difficulty: Hard
RegisterSignal(src, COMSIG_BLOOD_WARP, .proc/blood_enrage)
RegisterSignal(src, COMSIG_FINISHED_CHARGE, .proc/after_charge)
if(spawn_blood)
- AddElement(/datum/element/blood_walk, /obj/effect/decal/cleanable/blood/bubblegum, 'sound/effects/meteorimpact.ogg', 200)
+ AddComponent(/datum/component/blood_walk, \
+ blood_type = /obj/effect/decal/cleanable/blood/bubblegum, \
+ sound_played = 'sound/effects/meteorimpact.ogg', \
+ sound_volume = 200)
/mob/living/simple_animal/hostile/megafauna/bubblegum/Destroy()
QDEL_NULL(triple_charge)
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
index 83d7c5aab5e61..ba0ef910e8e70 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/colossus.dm
@@ -82,8 +82,8 @@
shotgun_blast.Grant(src)
dir_shots.Grant(src)
colossus_final.Grant(src)
- RegisterSignal(src, COMSIG_ABILITY_STARTED, .proc/start_attack)
- RegisterSignal(src, COMSIG_ABILITY_FINISHED, .proc/finished_attack)
+ RegisterSignal(src, COMSIG_MOB_ABILITY_STARTED, .proc/start_attack)
+ RegisterSignal(src, COMSIG_MOB_ABILITY_FINISHED, .proc/finished_attack)
AddElement(/datum/element/projectile_shield)
/mob/living/simple_animal/hostile/megafauna/colossus/Destroy()
@@ -587,8 +587,8 @@
ADD_TRAIT(L, TRAIT_MUTE, STASIS_MUTE)
L.status_flags |= GODMODE
L.mind.transfer_to(holder_animal)
- var/obj/effect/proc_holder/spell/targeted/exit_possession/P = new /obj/effect/proc_holder/spell/targeted/exit_possession
- holder_animal.mind.AddSpell(P)
+ var/datum/action/exit_possession/escape = new(holder_animal)
+ escape.Grant(holder_animal)
remove_verb(holder_animal, /mob/living/verb/pulled)
/obj/structure/closet/stasis/dump_contents(kill = 1)
@@ -599,7 +599,7 @@
L.notransform = 0
if(holder_animal)
holder_animal.mind.transfer_to(L)
- L.mind.RemoveSpell(/obj/effect/proc_holder/spell/targeted/exit_possession)
+ holder_animal.gib()
if(kill || !isanimal(loc))
L.death(0)
..()
@@ -610,33 +610,27 @@
/obj/structure/closet/stasis/ex_act()
return
-/obj/effect/proc_holder/spell/targeted/exit_possession
+/datum/action/exit_possession
name = "Exit Possession"
- desc = "Exits the body you are possessing."
- charge_max = 60
- clothes_req = 0
- invocation_type = INVOCATION_NONE
- max_targets = 1
- range = -1
- include_user = TRUE
- selection_type = "view"
- action_icon = 'icons/mob/actions/actions_spells.dmi'
- action_icon_state = "exit_possession"
- sound = null
-
-/obj/effect/proc_holder/spell/targeted/exit_possession/cast(list/targets, mob/living/user = usr)
- if(!isfloorturf(user.loc))
- return
- var/datum/mind/target_mind = user.mind
- for(var/i in user)
- if(istype(i, /obj/structure/closet/stasis))
- var/obj/structure/closet/stasis/S = i
- S.dump_contents(0)
- qdel(S)
- break
- user.gib()
- target_mind.RemoveSpell(/obj/effect/proc_holder/spell/targeted/exit_possession)
+ desc = "Exits the body you are possessing. They will explode violently when this occurs."
+ icon_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "exit_possession"
+
+/datum/action/exit_possession/IsAvailable()
+ return ..() && isfloorturf(owner.loc)
+
+/datum/action/exit_possession/Trigger(trigger_flags)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ var/obj/structure/closet/stasis/stasis = locate() in owner
+ if(!stasis)
+ CRASH("[type] did not find a stasis closet thing in the owner.")
+ stasis.dump_contents(FALSE)
+ qdel(stasis)
+ qdel(src)
#undef ACTIVATE_TOUCH
#undef ACTIVATE_SPEECH
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm
index 1d4e84f51df48..7140399ec913c 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/demonic_frost_miner.dm
@@ -64,7 +64,7 @@ Difficulty: Extremely Hard
ice_shotgun.Grant(src)
for(var/obj/structure/frost_miner_prism/prism_to_set in GLOB.frost_miner_prisms)
prism_to_set.set_prism_light(LIGHT_COLOR_BLUE, 5)
- RegisterSignal(src, COMSIG_ABILITY_STARTED, .proc/start_attack)
+ RegisterSignal(src, COMSIG_MOB_ABILITY_STARTED, .proc/start_attack)
AddElement(/datum/element/knockback, 7, FALSE, TRUE)
AddElement(/datum/element/lifesteal, 50)
ADD_TRAIT(src, TRAIT_NO_FLOATING_ANIM, INNATE_TRAIT)
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
index 72d9673454160..4cde7ad1baaa6 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/drake.dm
@@ -93,8 +93,8 @@
meteors.Grant(src)
mass_fire.Grant(src)
lava_swoop.Grant(src)
- RegisterSignal(src, COMSIG_ABILITY_STARTED, .proc/start_attack)
- RegisterSignal(src, COMSIG_ABILITY_FINISHED, .proc/finished_attack)
+ RegisterSignal(src, COMSIG_MOB_ABILITY_STARTED, .proc/start_attack)
+ RegisterSignal(src, COMSIG_MOB_ABILITY_FINISHED, .proc/finished_attack)
RegisterSignal(src, COMSIG_SWOOP_INVULNERABILITY_STARTED, .proc/swoop_invulnerability_started)
RegisterSignal(src, COMSIG_LAVA_ARENA_FAILED, .proc/on_arena_fail)
diff --git a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
index f1559082ef9c6..9040f80fb4315 100644
--- a/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
+++ b/code/modules/mob/living/simple_animal/hostile/megafauna/wendigo.dm
@@ -303,17 +303,11 @@ Difficulty: Hard
if(!human_user.mind)
return
to_chat(human_user, span_danger("Power courses through you! You can now shift your form at will."))
- var/obj/effect/proc_holder/spell/targeted/shapeshift/polar_bear/transformation_spell = new
- human_user.mind.AddSpell(transformation_spell)
+ var/datum/action/cooldown/spell/shapeshift/polar_bear/transformation_spell = new(user.mind || user)
+ transformation_spell.Grant(user)
playsound(human_user.loc, 'sound/items/drink.ogg', rand(10,50), TRUE)
qdel(src)
-/obj/effect/proc_holder/spell/targeted/shapeshift/polar_bear
- name = "Polar Bear Form"
- desc = "Take on the shape of a polar bear."
- invocation = "RAAAAAAAAWR!"
- shapeshift_type = /mob/living/simple_animal/hostile/asteroid/polarbear/lesser
-
/obj/item/crusher_trophy/wendigo_horn
name = "wendigo horn"
desc = "A horn from the head of an unstoppable beast."
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
index b29de2f72bcae..23d6be2d6e554 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/elite.dm
@@ -255,8 +255,8 @@ While using this makes the system rely on OnFire, it still gives options for tim
/obj/structure/elite_tumor/attackby(obj/item/attacking_item, mob/user, params)
. = ..()
- if(istype(attacking_item, /obj/item/organ/regenerative_core) && activity == TUMOR_INACTIVE && !boosted)
- var/obj/item/organ/regenerative_core/core = attacking_item
+ if(istype(attacking_item, /obj/item/organ/internal/regenerative_core) && activity == TUMOR_INACTIVE && !boosted)
+ var/obj/item/organ/internal/regenerative_core/core = attacking_item
visible_message(span_boldwarning("As [user] drops the core into [src], [src] appears to swell."))
icon_state = "advanced_tumor"
boosted = TRUE
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
index f4aa963e61fd8..3d06670944fe8 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/elites/legionnaire.dm
@@ -231,7 +231,7 @@
else
visible_message(span_boldwarning("[src] spews smoke from its maw!"))
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(2, location = smoke_location)
+ smoke.set_up(2, holder = src, location = smoke_location)
smoke.start()
//The legionnaire's head. Basically the same as any legion head, but we have to tell our creator when we die so they can generate another head.
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/goldgrub.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/goldgrub.dm
index 39dc4698518f8..e40f4937e409a 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/goldgrub.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/goldgrub.dm
@@ -80,16 +80,15 @@
return
if(G.is_burrowed)
holder = G.loc
- G.forceMove(T)
- QDEL_NULL(holder)
+ holder.eject_jaunter()
+ holder = null
G.is_burrowed = FALSE
G.visible_message(span_danger("[G] emerges from the ground!"))
playsound(get_turf(G), 'sound/effects/break_stone.ogg', 50, TRUE, -1)
else
G.visible_message(span_danger("[G] buries into the ground, vanishing from sight!"))
playsound(get_turf(G), 'sound/effects/break_stone.ogg', 50, TRUE, -1)
- holder = new /obj/effect/dummy/phased_mob(T)
- G.forceMove(holder)
+ holder = new /obj/effect/dummy/phased_mob(T, G)
G.is_burrowed = TRUE
/mob/living/simple_animal/hostile/asteroid/goldgrub/GiveTarget(new_target)
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/goliath.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/goliath.dm
index b94d833e6834f..95a3e57ad80dc 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/goliath.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/goliath.dm
@@ -118,7 +118,6 @@
user.visible_message(span_notice("You manage to put [O] on [src], you can now ride [p_them()]."))
qdel(O)
saddled = TRUE
- can_buckle = TRUE
buckle_lying = 0
add_overlay("goliath_saddled")
AddElement(/datum/element/ridable, /datum/component/riding/creature/goliath)
diff --git a/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm b/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm
index 9c6b6432c9315..a98b5721cd4f4 100644
--- a/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm
+++ b/code/modules/mob/living/simple_animal/hostile/mining_mobs/hivelord.dm
@@ -30,7 +30,7 @@
retreat_distance = 3
minimum_distance = 3
pass_flags = PASSTABLE
- loot = list(/obj/item/organ/regenerative_core)
+ loot = list(/obj/item/organ/internal/regenerative_core)
var/brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood
var/has_clickbox = TRUE
@@ -122,7 +122,7 @@
attack_sound = 'sound/weapons/pierce.ogg'
throw_message = "bounces harmlessly off of"
crusher_loot = /obj/item/crusher_trophy/legion_skull
- loot = list(/obj/item/organ/regenerative_core/legion)
+ loot = list(/obj/item/organ/internal/regenerative_core/legion)
brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion
del_on_death = 1
stat_attack = HARD_CRIT
@@ -267,7 +267,7 @@
layer = MOB_LAYER
del_on_death = TRUE
sentience_type = SENTIENCE_BOSS
- loot = list(/obj/item/organ/regenerative_core/legion = 3, /obj/effect/mob_spawn/corpse/human/legioninfested = 5)
+ loot = list(/obj/item/organ/internal/regenerative_core/legion = 3, /obj/effect/mob_spawn/corpse/human/legioninfested = 5)
move_to_delay = 14
vision_range = 5
aggro_vision_range = 9
@@ -294,7 +294,7 @@
icon_aggro = "snowlegion_alive"
icon_dead = "snowlegion"
crusher_loot = /obj/item/crusher_trophy/legion_skull
- loot = list(/obj/item/organ/regenerative_core/legion)
+ loot = list(/obj/item/organ/internal/regenerative_core/legion)
brood_type = /mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/snow
/mob/living/simple_animal/hostile/asteroid/hivelordbrood/legion/snow/make_legion(mob/living/carbon/human/H)
diff --git a/code/modules/mob/living/simple_animal/hostile/netherworld.dm b/code/modules/mob/living/simple_animal/hostile/netherworld.dm
index 752d47813de1b..2242435324c10 100644
--- a/code/modules/mob/living/simple_animal/hostile/netherworld.dm
+++ b/code/modules/mob/living/simple_animal/hostile/netherworld.dm
@@ -20,13 +20,12 @@
minbodytemp = 0
lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
var/phaser = TRUE
- var/datum/action/innate/creature/teleport/teleport
var/is_phased = FALSE
/mob/living/simple_animal/hostile/netherworld/Initialize(mapload)
. = ..()
if(phaser)
- teleport = new
+ var/datum/action/innate/creature/teleport/teleport = new(src)
teleport.Grant(src)
add_cell_sample()
@@ -54,14 +53,13 @@
return
if(N.is_phased)
holder = N.loc
- N.forceMove(T)
- QDEL_NULL(holder)
+ holder.eject_jaunter()
+ holder = null
N.is_phased = FALSE
playsound(get_turf(N), 'sound/effects/podwoosh.ogg', 50, TRUE, -1)
else
playsound(get_turf(N), 'sound/effects/podwoosh.ogg', 50, TRUE, -1)
- holder = new /obj/effect/dummy/phased_mob(T)
- N.forceMove(holder)
+ holder = new /obj/effect/dummy/phased_mob(T, N)
N.is_phased = TRUE
/mob/living/simple_animal/hostile/netherworld/proc/can_be_seen(turf/location)
diff --git a/code/modules/mob/living/simple_animal/hostile/ooze.dm b/code/modules/mob/living/simple_animal/hostile/ooze.dm
index aed4425acf323..b65018f2cb460 100644
--- a/code/modules/mob/living/simple_animal/hostile/ooze.dm
+++ b/code/modules/mob/living/simple_animal/hostile/ooze.dm
@@ -282,85 +282,85 @@
obj_damage = 15
deathmessage = "deflates and spills its vital juices!"
edible_food_types = MEAT | VEGETABLES
- ///The ability lets you envelop a carbon in a healing cocoon. Useful for saving critical carbons.
- var/datum/action/cooldown/gel_cocoon/gel_cocoon
- ///The ability to shoot a mending globule, a sticky projectile that heals over time.
- var/obj/effect/proc_holder/globules/globules
/mob/living/simple_animal/hostile/ooze/grapes/Initialize(mapload)
. = ..()
- globules = new
- AddAbility(globules)
- gel_cocoon = new
+ var/datum/action/cooldown/globules/glob_shooter = new(src)
+ glob_shooter.Grant(src)
+ var/datum/action/cooldown/gel_cocoon/gel_cocoon = new(src)
gel_cocoon.Grant(src)
-/mob/living/simple_animal/hostile/ooze/grapes/Destroy()
- . = ..()
- QDEL_NULL(gel_cocoon)
- QDEL_NULL(globules)
-
/mob/living/simple_animal/hostile/ooze/grapes/add_cell_sample()
AddElement(/datum/element/swabable, CELL_LINE_TABLE_GRAPE, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
///Ability that allows the owner to fire healing globules at mobs, targetting specific limbs.
-/obj/effect/proc_holder/globules
+/datum/action/cooldown/globules
name = "Fire Mending globule"
desc = "Fires a mending globule at someone, healing a specific limb of theirs."
- active = FALSE
- action_icon = 'icons/mob/actions/actions_slime.dmi'
- action_icon_state = "globules"
- action_background_icon_state = "bg_hive"
- var/cooldown = 5 SECONDS
- var/current_cooldown = 0
+ background_icon_state = "bg_hive"
+ icon_icon = 'icons/mob/actions/actions_slime.dmi'
+ button_icon_state = "globules"
+ check_flags = AB_CHECK_CONSCIOUS
+ cooldown_time = 5 SECONDS
+ click_to_activate = TRUE
-/obj/effect/proc_holder/globules/Click(location, control, params)
+/datum/action/cooldown/globules/set_click_ability(mob/on_who)
. = ..()
- if(!isliving(usr))
- return TRUE
- var/mob/living/user = usr
- fire(user)
-
-/obj/effect/proc_holder/globules/fire(mob/living/carbon/user)
- var/message
- if(current_cooldown > world.time)
- to_chat(user, span_notice("This ability is still on cooldown."))
+ if(!.)
return
- if(active)
- message = span_notice("You stop preparing your mending globules.")
- remove_ranged_ability(message)
- else
- message = span_notice("You prepare to launch a mending globule. Left-click to fire at a target!")
- add_ranged_ability(user, message, TRUE)
-
-/obj/effect/proc_holder/globules/InterceptClickOn(mob/living/caller, params, atom/target)
+
+ to_chat(on_who, span_notice("You prepare to launch a mending globule. Left-click to fire at a target!"))
+
+/datum/action/cooldown/globules/unset_click_ability(mob/on_who, refund_cooldown = TRUE)
. = ..()
- if(.)
- return
- if(!istype(ranged_ability_user, /mob/living/simple_animal/hostile/ooze) || ranged_ability_user.stat)
- remove_ranged_ability()
+ if(!.)
return
- var/mob/living/simple_animal/hostile/ooze/ooze = ranged_ability_user
+ if(refund_cooldown)
+ to_chat(on_who, span_notice("You stop preparing your mending globules."))
- if(ooze.ooze_nutrition < 5)
- to_chat(ooze, span_warning("You need at least 5 nutrition to launch a mending globule."))
- remove_ranged_ability()
- return
+/datum/action/cooldown/globules/Activate(atom/target)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ var/mob/living/simple_animal/hostile/ooze/oozy_owner = owner
+ if(istype(oozy_owner))
+ if(oozy_owner.ooze_nutrition < 5)
+ to_chat(oozy_owner, span_warning("You need at least 5 nutrition to launch a mending globule."))
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/globules/InterceptClickOn(mob/living/caller, params, atom/target)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ // Why is this in InterceptClickOn() and not Activate()?
+ // Well, we need to use the params of the click intercept
+ // for passing into preparePixelProjectile, so we'll handle it here instead.
+ // We just need to make sure Pre-activate and Activate return TRUE so we make it this far
+ caller.visible_message(
+ span_nicegreen("[caller] launches a mending globule!"),
+ span_notice("You launch a mending globule."),
+ )
+
+ var/mob/living/simple_animal/hostile/ooze/oozy = caller
+ if(istype(oozy))
+ oozy.adjust_ooze_nutrition(-5)
- ooze.visible_message(span_nicegreen("[ooze] launches a mending globule!"), span_notice("You launch a mending globule."))
var/modifiers = params2list(params)
- var/obj/projectile/globule/globule = new (ooze.loc)
- globule.preparePixelProjectile(target, ooze, modifiers)
- globule.def_zone = ooze.zone_selected
+ var/obj/projectile/globule/globule = new(caller.loc)
+ globule.preparePixelProjectile(target, caller, modifiers)
+ globule.def_zone = caller.zone_selected
globule.fire()
- ooze.adjust_ooze_nutrition(-5)
- remove_ranged_ability()
- current_cooldown = world.time + cooldown
return TRUE
-/obj/effect/proc_holder/globules/on_lose(mob/living/carbon/user)
- remove_ranged_ability()
+// Needs to return TRUE otherwise PreActivate() will fail, see above
+/datum/action/cooldown/globules/Activate(atom/target)
+ return TRUE
///This projectile embeds into mobs and heals them over time.
/obj/projectile/globule
diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm
index 0c9a4e569adcd..0bf43c28e5897 100644
--- a/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm
+++ b/code/modules/mob/living/simple_animal/hostile/retaliate/clown.dm
@@ -35,7 +35,7 @@
unsuitable_atmos_damage = 10
unsuitable_heat_damage = 15
footstep_type = FOOTSTEP_MOB_SHOE
- faction = FACTION_CLOWN
+ faction = list(FACTION_CLOWN)
var/attack_reagent
/mob/living/simple_animal/hostile/retaliate/clown/Initialize(mapload)
@@ -393,13 +393,12 @@
deathsound = 'sound/misc/sadtrombone.ogg'
///This is the list of items we are ready to regurgitate,
var/list/prank_pouch = list()
- ///This ability lets you fire a single random item from your pouch.
- var/obj/effect/proc_holder/regurgitate/my_regurgitate
/mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton/Initialize(mapload)
. = ..()
- my_regurgitate = new
- AddAbility(my_regurgitate)
+ var/datum/action/cooldown/regurgitate/spit = new(src)
+ spit.Grant(src)
+
add_cell_sample()
AddComponent(/datum/component/tameable, food_types = list(/obj/item/food/cheesiehonkers, /obj/item/food/cornchips), tame_chance = 30, bonus_tame_chance = 0, after_tame = CALLBACK(src, .proc/tamed))
@@ -457,7 +456,6 @@
flick("glutton_mouth", src)
/mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton/proc/tamed(mob/living/tamer)
- can_buckle = TRUE
buckle_lying = 0
AddElement(/datum/element/ridable, /datum/component/riding/creature/glutton)
@@ -469,48 +467,56 @@
prank_pouch -= gone
///This ability will let you fire one random item from your pouch,
-/obj/effect/proc_holder/regurgitate
+/datum/action/cooldown/regurgitate
name = "Regurgitate"
desc = "Regurgitates a single item from the depths of your pouch."
- action_background_icon_state = "bg_changeling"
- action_icon = 'icons/mob/actions/actions_animal.dmi'
- action_icon_state = "regurgitate"
- active = FALSE
+ background_icon_state = "bg_changeling"
+ icon_icon = 'icons/mob/actions/actions_animal.dmi'
+ button_icon_state = "regurgitate"
+ check_flags = AB_CHECK_CONSCIOUS
+ click_to_activate = TRUE
-/obj/effect/proc_holder/regurgitate/Click(location, control, params)
+/datum/action/cooldown/regurgitate/set_click_ability(mob/on_who)
. = ..()
- if(!isliving(usr))
- return TRUE
- var/mob/living/user = usr
- fire(user)
+ if(!.)
+ return
-/obj/effect/proc_holder/regurgitate/fire(mob/living/carbon/user)
- if(active)
- user.icon_state = initial(user.icon_state)
- remove_ranged_ability(span_notice("Your throat muscles relax."))
- else
- user.icon_state = "glutton_tongue"
- add_ranged_ability(user, span_notice("Your throat muscles tense up. Left-click to regurgitate a funny morsel!"), TRUE)
+ to_chat(on_who, span_notice("Your throat muscles tense up. Left-click to regurgitate a funny morsel!"))
+ on_who.icon_state = "glutton_tongue"
+ on_who.update_appearance(UPDATE_ICON)
-/obj/effect/proc_holder/regurgitate/InterceptClickOn(mob/living/caller, params, atom/target)
+/datum/action/cooldown/regurgitate/unset_click_ability(mob/on_who, refund_cooldown = TRUE)
. = ..()
-
- if(.)
+ if(!.)
return
- if(!istype(ranged_ability_user, /mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton) || ranged_ability_user.stat)
- remove_ranged_ability()
- return
+ if(refund_cooldown)
+ to_chat(on_who, span_notice("Your throat muscles relax."))
+ on_who.icon_state = initial(on_who.icon_state)
+ on_who.update_appearance(UPDATE_ICON)
- var/mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton/pouch_owner = ranged_ability_user
- if(!pouch_owner.prank_pouch.len)
- //active = FALSE
- pouch_owner.icon_state = "glutton"
- remove_ranged_ability(span_notice("Your prank pouch is empty,."))
- return
+/datum/action/cooldown/regurgitate/IsAvailable()
+ . = ..()
+ if(!.)
+ return FALSE
+
+ // Hardcoded to only work with gluttons. Come back next year
+ return istype(owner, /mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton)
+
+/datum/action/cooldown/regurgitate/Activate(atom/spit_at)
+ StartCooldown(cooldown_time / 4)
+
+ var/mob/living/simple_animal/hostile/retaliate/clown/mutant/glutton/pouch_owner = owner
+ if(!length(pouch_owner.prank_pouch))
+ pouch_owner.icon_state = initial(pouch_owner.icon_state)
+ to_chat(pouch_owner, span_notice("Your prank pouch is empty."))
+ return TRUE
var/obj/item/projected_morsel = pick(pouch_owner.prank_pouch)
projected_morsel.forceMove(pouch_owner.loc)
- projected_morsel.throw_at(target, 8, 2, pouch_owner)
+ projected_morsel.throw_at(spit_at, 8, 2, pouch_owner)
flick("glutton_mouth", pouch_owner)
playsound(pouch_owner, 'sound/misc/soggy.ogg', 75)
+
+ StartCooldown()
+ return TRUE
diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/frog.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/frog.dm
index 07399d3575091..93b20fdc295fc 100644
--- a/code/modules/mob/living/simple_animal/hostile/retaliate/frog.dm
+++ b/code/modules/mob/living/simple_animal/hostile/retaliate/frog.dm
@@ -33,6 +33,10 @@
worn_slot_flags = ITEM_SLOT_HEAD
head_icon = 'icons/mob/animal_item_head.dmi'
var/stepped_sound = 'sound/effects/huuu.ogg'
+ ///How much of a reagent the mob injects on attack
+ var/poison_per_bite = 3
+ ///What reagent the mob injects targets with
+ var/poison_type = /datum/reagent/drug/space_drugs
/mob/living/simple_animal/hostile/retaliate/frog/Initialize(mapload)
. = ..()
@@ -46,11 +50,13 @@
icon_living = "rare_frog"
icon_dead = "rare_frog_dead"
butcher_results = list(/obj/item/food/nugget = 5)
+ poison_type = /datum/reagent/drug/mushroomhallucinogen
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = .proc/on_entered,
)
AddElement(/datum/element/connect_loc, loc_connections)
+ AddElement(/datum/element/venomous, poison_type, poison_per_bite)
add_cell_sample()
/mob/living/simple_animal/hostile/retaliate/frog/proc/on_entered(datum/source, AM as mob|obj)
diff --git a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
index 8aff83267ea75..3cf373ac6e7df 100644
--- a/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
+++ b/code/modules/mob/living/simple_animal/hostile/space_dragon.dm
@@ -141,6 +141,7 @@
if(riftTimer >= maxRiftTimer)
to_chat(src, span_boldwarning("You've failed to summon the rift in a timely manner! You're being pulled back from whence you came!"))
destroy_rifts()
+ empty_contents()
playsound(src, 'sound/magic/demon_dies.ogg', 100, TRUE)
QDEL_NULL(src)
diff --git a/code/modules/mob/living/simple_animal/hostile/statue.dm b/code/modules/mob/living/simple_animal/hostile/statue.dm
index 78ab42d3c0e81..26d5bf74ed3b4 100644
--- a/code/modules/mob/living/simple_animal/hostile/statue.dm
+++ b/code/modules/mob/living/simple_animal/hostile/statue.dm
@@ -1,6 +1,6 @@
// A mob which only moves when it isn't being watched by living beings.
-/mob/living/simple_animal/hostile/statue
+/mob/living/simple_animal/hostile/netherworld/statue
name = "statue" // matches the name of the statue with the flesh-to-stone spell
desc = "An incredibly lifelike marble carving. Its eyes seem to follow you..." // same as an ordinary statue with the added "eye following you" description
icon = 'icons/obj/statue.dmi'
@@ -10,6 +10,7 @@
gender = NEUTER
combat_mode = TRUE
mob_biotypes = MOB_HUMANOID
+ gold_core_spawnable = NO_SPAWN
response_help_continuous = "touches"
response_help_simple = "touch"
@@ -56,34 +57,38 @@
// No movement while seen code.
-/mob/living/simple_animal/hostile/statue/Initialize(mapload, mob/living/creator)
+/mob/living/simple_animal/hostile/netherworld/statue/Initialize(mapload, mob/living/creator)
. = ..()
// Give spells
- LAZYINITLIST(mob_spell_list)
- mob_spell_list += new /obj/effect/proc_holder/spell/aoe_turf/flicker_lights(src)
- mob_spell_list += new /obj/effect/proc_holder/spell/aoe_turf/blindness(src)
- mob_spell_list += new /obj/effect/proc_holder/spell/targeted/night_vision(src)
- var/datum/action/innate/creature/teleport/teleport = new(src)
- teleport.Grant(src)
+
+ var/datum/action/cooldown/spell/aoe/flicker_lights/flicker = new(src)
+ flicker.Grant(src)
+ var/datum/action/cooldown/spell/aoe/blindness/blind = new(src)
+ blind.Grant(src)
+ var/datum/action/cooldown/spell/night_vision/night_vision = new(src)
+ night_vision.Grant(src)
// Set creator
if(creator)
src.creator = creator
-/mob/living/simple_animal/hostile/statue/med_hud_set_health()
+/mob/living/simple_animal/hostile/netherworld/statue/add_cell_sample()
+ return
+
+/mob/living/simple_animal/hostile/netherworld/statue/med_hud_set_health()
return //we're a statue we're invincible
-/mob/living/simple_animal/hostile/statue/med_hud_set_status()
+/mob/living/simple_animal/hostile/netherworld/statue/med_hud_set_status()
return //we're a statue we're invincible
-/mob/living/simple_animal/hostile/statue/Move(turf/NewLoc)
+/mob/living/simple_animal/hostile/netherworld/statue/Move(turf/NewLoc)
if(can_be_seen(NewLoc))
if(client)
to_chat(src, span_warning("You cannot move, there are eyes on you!"))
return
return ..()
-/mob/living/simple_animal/hostile/statue/Life(delta_time = SSMOBS_DT, times_fired)
+/mob/living/simple_animal/hostile/netherworld/statue/Life(delta_time = SSMOBS_DT, times_fired)
..()
if(!client && target) // If we have a target and we're AI controlled
var/mob/watching = can_be_seen()
@@ -94,7 +99,7 @@
LoseTarget()
GiveTarget(watching)
-/mob/living/simple_animal/hostile/statue/AttackingTarget()
+/mob/living/simple_animal/hostile/netherworld/statue/AttackingTarget()
if(can_be_seen(get_turf(loc)))
if(client)
to_chat(src, span_warning("You cannot attack, there are eyes on you!"))
@@ -102,60 +107,31 @@
else
return ..()
-/mob/living/simple_animal/hostile/statue/DestroyPathToTarget()
+/mob/living/simple_animal/hostile/netherworld/statue/DestroyPathToTarget()
if(!can_be_seen(get_turf(loc)))
..()
-/mob/living/simple_animal/hostile/statue/face_atom()
+/mob/living/simple_animal/hostile/netherworld/statue/face_atom()
if(!can_be_seen(get_turf(loc)))
..()
-/mob/living/simple_animal/hostile/statue/IsVocal() //we're a statue, of course we can't talk.
+/mob/living/simple_animal/hostile/netherworld/statue/IsVocal() //we're a statue, of course we can't talk.
return FALSE
-/mob/living/simple_animal/hostile/statue/proc/can_be_seen(turf/destination)
- if(!cannot_be_seen)
- return null
- // Check for darkness
- var/turf/T = get_turf(loc)
- if(T && destination && T.lighting_object)
- if(T.get_lumcount()<0.1 && destination.get_lumcount()<0.1) // No one can see us in the darkness, right?
- return null
- if(T == destination)
- destination = null
-
- // We aren't in darkness, loop for viewers.
- var/list/check_list = list(src)
- if(destination)
- check_list += destination
-
- // This loop will, at most, loop twice.
- for(var/atom/check in check_list)
- for(var/mob/living/M in viewers(world.view + 1, check) - src)
- if(M.client && CanAttack(M) && !M.has_unlimited_silicon_privilege)
- if(!M.is_blind())
- return M
- for(var/obj/vehicle/sealed/mecha/M in view(world.view + 1, check)) //assuming if you can see them they can see you
- for(var/O in M.occupants)
- var/mob/mechamob = O
- if(mechamob.client && !mechamob.is_blind())
- return mechamob
- return null
-
// Cannot talk
-/mob/living/simple_animal/hostile/statue/say(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null, filterproof = null)
+/mob/living/simple_animal/hostile/netherworld/statue/say(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language = null, ignore_spam = FALSE, forced = null, filterproof = null)
return
// Turn to dust when gibbed
-/mob/living/simple_animal/hostile/statue/gib()
+/mob/living/simple_animal/hostile/netherworld/statue/gib()
dust()
// Stop attacking clientless mobs
-/mob/living/simple_animal/hostile/statue/CanAttack(atom/the_target)
+/mob/living/simple_animal/hostile/netherworld/statue/CanAttack(atom/the_target)
if(isliving(the_target))
var/mob/living/L = the_target
if(!L.client && !L.ckey)
@@ -164,72 +140,59 @@
// Don't attack your creator if there is one
-/mob/living/simple_animal/hostile/statue/ListTargets()
+/mob/living/simple_animal/hostile/netherworld/statue/ListTargets()
. = ..()
return . - creator
+/mob/living/simple_animal/hostile/netherworld/statue/sentience_act()
+ faction -= "neutral"
+
// Statue powers
// Flicker lights
-/obj/effect/proc_holder/spell/aoe_turf/flicker_lights
+/datum/action/cooldown/spell/aoe/flicker_lights
name = "Flicker Lights"
desc = "You will trigger a large amount of lights around you to flicker."
- charge_max = 300
- clothes_req = 0
- range = 14
+ cooldown_time = 30 SECONDS
+ spell_requirements = NONE
+ aoe_radius = 14
-/obj/effect/proc_holder/spell/aoe_turf/flicker_lights/cast(list/targets,mob/user = usr)
- for(var/turf/T in targets)
- for(var/obj/machinery/light/L in T)
- L.flicker()
- return
+/datum/action/cooldown/spell/aoe/flicker_lights/get_things_to_cast_on(atom/center)
+ var/list/things = list()
+ for(var/obj/machinery/light/nearby_light in range(aoe_radius, center))
+ if(!nearby_light.on)
+ continue
+
+ things += nearby_light
+
+ return things
+
+/datum/action/cooldown/spell/aoe/flicker_lights/cast_on_thing_in_aoe(obj/machinery/light/victim, atom/caster)
+ victim.flicker()
//Blind AOE
-/obj/effect/proc_holder/spell/aoe_turf/blindness
+/datum/action/cooldown/spell/aoe/blindness
name = "Blindness"
desc = "Your prey will be momentarily blind for you to advance on them."
- message = "You glare your eyes."
- charge_max = 600
- clothes_req = 0
- range = 10
+ cooldown_time = 1 MINUTES
+ spell_requirements = NONE
+ aoe_radius = 14
-/obj/effect/proc_holder/spell/aoe_turf/blindness/cast(list/targets,mob/user = usr)
- for(var/mob/living/L in GLOB.alive_mob_list)
- var/turf/T = get_turf(L.loc)
- if(T && (T in targets))
- L.blind_eyes(4)
- return
+/datum/action/cooldown/spell/aoe/blindness/cast(atom/cast_on)
+ cast_on.visible_message(span_danger("[cast_on] glares their eyes."))
+ return ..()
-//Toggle Night Vision
-/obj/effect/proc_holder/spell/targeted/night_vision
- name = "Toggle Nightvision \[ON\]"
- desc = "Toggle your nightvision mode."
-
- charge_max = 10
- clothes_req = 0
-
- message = "You toggle your night vision!"
- range = -1
- include_user = 1
-
-/obj/effect/proc_holder/spell/targeted/night_vision/cast(list/targets, mob/user = usr)
- for(var/mob/living/target in targets)
- switch(target.lighting_alpha)
- if (LIGHTING_PLANE_ALPHA_VISIBLE)
- target.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE
- name = "Toggle Nightvision \[More]"
- if (LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE)
- target.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
- name = "Toggle Nightvision \[Full]"
- if (LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE)
- target.lighting_alpha = LIGHTING_PLANE_ALPHA_INVISIBLE
- name = "Toggle Nightvision \[OFF]"
- else
- target.lighting_alpha = LIGHTING_PLANE_ALPHA_VISIBLE
- name = "Toggle Nightvision \[ON]"
- target.update_sight()
-
-/mob/living/simple_animal/hostile/statue/sentience_act()
- faction -= "neutral"
+/datum/action/cooldown/spell/aoe/blindness/get_things_to_cast_on(atom/center)
+ var/list/things = list()
+ for(var/mob/living/nearby_mob in range(aoe_radius, center))
+ if(nearby_mob == owner || nearby_mob == center)
+ continue
+
+ things += nearby_mob
+
+ return things
+
+/datum/action/cooldown/spell/aoe/blindness/cast_on_thing_in_aoe(mob/living/victim, atom/caster)
+ victim.blind_eyes(4)
diff --git a/code/modules/mob/living/simple_animal/hostile/vatbeast.dm b/code/modules/mob/living/simple_animal/hostile/vatbeast.dm
index 3d866e6008174..2198eabea035c 100644
--- a/code/modules/mob/living/simple_animal/hostile/vatbeast.dm
+++ b/code/modules/mob/living/simple_animal/hostile/vatbeast.dm
@@ -23,21 +23,15 @@
attack_verb_continuous = "slaps"
attack_verb_simple = "slap"
- var/obj/effect/proc_holder/tentacle_slap/tentacle_slap
-
/mob/living/simple_animal/hostile/vatbeast/Initialize(mapload)
. = ..()
- tentacle_slap = new(src, src)
- AddAbility(tentacle_slap)
+ var/datum/action/cooldown/tentacle_slap/slapper = new(src)
+ slapper.Grant(src)
+
add_cell_sample()
AddComponent(/datum/component/tameable, list(/obj/item/food/fries, /obj/item/food/cheesyfries, /obj/item/food/cornchips, /obj/item/food/carrotfries), tame_chance = 30, bonus_tame_chance = 0, after_tame = CALLBACK(src, .proc/tamed))
-/mob/living/simple_animal/hostile/vatbeast/Destroy()
- . = ..()
- QDEL_NULL(tentacle_slap)
-
/mob/living/simple_animal/hostile/vatbeast/proc/tamed(mob/living/tamer)
- can_buckle = TRUE
buckle_lying = 0
AddElement(/datum/element/ridable, /datum/component/riding/creature/vatbeast)
faction = list("neutral")
@@ -45,74 +39,74 @@
/mob/living/simple_animal/hostile/vatbeast/add_cell_sample()
AddElement(/datum/element/swabable, CELL_LINE_TABLE_VATBEAST, CELL_VIRUS_TABLE_GENERIC_MOB, 1, 5)
-///Ability that allows the owner to slap other mobs a short distance away
-/obj/effect/proc_holder/tentacle_slap
+/// Ability that allows the owner to slap other mobs a short distance away.
+/// For vatbeats, this ability is shared with the rider.
+/datum/action/cooldown/tentacle_slap
name = "Tentacle slap"
desc = "Slap a creature with your tentacles."
- active = FALSE
- action_icon = 'icons/mob/actions/actions_animal.dmi'
- action_icon_state = "tentacle_slap"
- action_background_icon_state = "bg_revenant"
+ background_icon_state = "bg_revenant"
+ icon_icon = 'icons/mob/actions/actions_animal.dmi'
+ button_icon_state = "tentacle_slap"
+ check_flags = AB_CHECK_CONSCIOUS
+ cooldown_time = 12 SECONDS
+ click_to_activate = TRUE
ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi'
- base_action = /datum/action/cooldown/spell_like
- ///How long cooldown before we can use the ability again
- var/cooldown = 12 SECONDS
-/obj/effect/proc_holder/tentacle_slap/Initialize(mapload, mob/living/new_owner)
+/datum/action/cooldown/tentacle_slap/UpdateButton(atom/movable/screen/movable/action_button/button, status_only, force)
. = ..()
- if(!action)
+ if(!button)
return
- var/datum/action/cooldown/our_action = action
- our_action.cooldown_time = cooldown
+ if(!status_only && button.our_hud.mymob != owner)
+ button.name = "Command Tentacle Slap"
+ button.desc = "Command your steed to slap a creature with its tentacles."
-/obj/effect/proc_holder/tentacle_slap/Click(location, control, params)
+/datum/action/cooldown/tentacle_slap/set_click_ability(mob/on_who)
. = ..()
- if(!isliving(usr))
- return TRUE
- fire(usr)
+ if(!.)
+ return
-/obj/effect/proc_holder/tentacle_slap/fire(mob/living/user)
- if(active)
- remove_ranged_ability(span_notice("You stop preparing to tentacle slap."))
- else
- add_ranged_ability(user, span_notice("You prepare [(IS_WEAKREF_OF(user, owner)) ? "your" : "their"] pimp-tentacle. Left-click to slap a target!"), TRUE)
+ to_chat(on_who, span_notice("You prepare your [on_who == owner ? "":"steed's "]pimp-tentacle. Left-click to slap a target!"))
-/obj/effect/proc_holder/tentacle_slap/InterceptClickOn(mob/living/caller, params, atom/target)
+/datum/action/cooldown/tentacle_slap/unset_click_ability(mob/on_who, refund_cooldown = TRUE)
. = ..()
- if(.)
+ if(!.)
return
- var/mob/living/beast_owner = owner.resolve()
+ if(refund_cooldown)
+ to_chat(on_who, span_notice("You stop preparing your [on_who == owner ? "":"steed's "]pimp-tentacle."))
- if(!beast_owner)
- return
+/datum/action/cooldown/tentacle_slap/InterceptClickOn(mob/living/caller, params, atom/target)
+ // Check if we can slap
+ if(!isliving(target) || target == owner)
+ return FALSE
- if(beast_owner.stat)
- remove_ranged_ability()
- return
+ if(!owner.Adjacent(target))
+ owner.balloon_alert(caller, "too far!")
+ return FALSE
- if(!beast_owner.Adjacent(target))
- return
-
- if(!isliving(target))
- return
+ // Do the slap
+ . = ..()
+ if(!.)
+ return FALSE
- var/mob/living/living_target = target
+ // Give feedback from the slap.
+ // Additional feedback for if a rider did it
+ if(caller != owner)
+ to_chat(caller, span_notice("You command [owner] to slap [target] with its tentacles."))
- if(!action.IsAvailable()) //extra check for safety since the ability is shared
- remove_ranged_ability()
- to_chat(caller, span_notice("This ability is still on cooldown."))
- return
+ return TRUE
- beast_owner.visible_message("[beast_owner] slaps [living_target] with its tentacle!", span_notice("You slap [living_target] with your tentacle."))
- playsound(beast_owner, 'sound/effects/assslap.ogg', 90)
- var/atom/throw_target = get_edge_target_turf(target, beast_owner.dir)
- living_target.throw_at(throw_target, 6, 4, beast_owner)
- living_target.apply_damage(30)
- remove_ranged_ability()
+/datum/action/cooldown/tentacle_slap/Activate(atom/to_slap)
+ var/mob/living/living_to_slap = to_slap
- var/datum/action/cooldown/our_action = action
- our_action.StartCooldown()
+ owner.visible_message(
+ span_warning("[owner] slaps [to_slap] with its tentacle!"),
+ span_notice("You slap [to_slap] with your tentacle."),
+ )
+ playsound(owner, 'sound/effects/assslap.ogg', 90)
+ var/atom/throw_target = get_edge_target_turf(to_slap, owner.dir)
+ living_to_slap.throw_at(throw_target, 6, 4, owner)
+ living_to_slap.apply_damage(30, BRUTE)
+ StartCooldown()
return TRUE
-
diff --git a/code/modules/mob/living/simple_animal/hostile/wizard.dm b/code/modules/mob/living/simple_animal/hostile/wizard.dm
index 8da7258a4cece..e844ceb12c182 100644
--- a/code/modules/mob/living/simple_animal/hostile/wizard.dm
+++ b/code/modules/mob/living/simple_animal/hostile/wizard.dm
@@ -23,56 +23,60 @@
unsuitable_atmos_damage = 7.5
faction = list(ROLE_WIZARD)
status_flags = CANPUSH
+ footstep_type = FOOTSTEP_MOB_SHOE
retreat_distance = 3 //out of fireball range
minimum_distance = 3
del_on_death = 1
- loot = list(/obj/effect/mob_spawn/corpse/human/wizard,
- /obj/item/staff)
-
- var/obj/effect/proc_holder/spell/aimed/fireball/fireball = null
- var/obj/effect/proc_holder/spell/targeted/turf_teleport/blink/blink = null
- var/obj/effect/proc_holder/spell/targeted/projectile/magic_missile/mm = null
+ loot = list(
+ /obj/effect/mob_spawn/corpse/human/wizard,
+ /obj/item/staff,
+ )
var/next_cast = 0
-
- footstep_type = FOOTSTEP_MOB_SHOE
+ var/datum/action/cooldown/spell/pointed/projectile/fireball/fireball
+ var/datum/action/cooldown/spell/teleport/radius_turf/blink/blink
+ var/datum/action/cooldown/spell/aoe/magic_missile/magic_missile
/mob/living/simple_animal/hostile/wizard/Initialize(mapload)
. = ..()
- fireball = new /obj/effect/proc_holder/spell/aimed/fireball
- fireball.clothes_req = 0
- fireball.human_req = 0
- fireball.player_lock = 0
- AddSpell(fireball)
- implants += new /obj/item/implant/exile(src)
+ var/obj/item/implant/exile/exiled = new /obj/item/implant/exile(src)
+ exiled.implant(src)
- mm = new /obj/effect/proc_holder/spell/targeted/projectile/magic_missile
- mm.clothes_req = 0
- mm.human_req = 0
- mm.player_lock = 0
- AddSpell(mm)
+ fireball = new(src)
+ fireball.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND)
+ fireball.Grant(src)
- blink = new /obj/effect/proc_holder/spell/targeted/turf_teleport/blink
- blink.clothes_req = 0
- blink.human_req = 0
- blink.player_lock = 0
+ magic_missile = new(src)
+ magic_missile.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND)
+ magic_missile.Grant(src)
+
+ blink = new(src)
+ blink.spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_MIND)
blink.outer_tele_radius = 3
- AddSpell(blink)
+ blink.Grant(src)
+
+/mob/living/simple_animal/hostile/wizard/Destroy()
+ QDEL_NULL(fireball)
+ QDEL_NULL(magic_missile)
+ QDEL_NULL(blink)
+ return ..()
/mob/living/simple_animal/hostile/wizard/handle_automated_action()
. = ..()
if(target && next_cast < world.time)
- if((get_dir(src,target) in list(SOUTH,EAST,WEST,NORTH)) && fireball.cast_check(0,src)) //Lined up for fireball
- src.setDir(get_dir(src,target))
- fireball.perform(list(target), user = src)
- next_cast = world.time + 10 //One spell per second
- return .
- if(mm.cast_check(0,src))
- mm.choose_targets(src)
- next_cast = world.time + 10
- return .
- if(blink.cast_check(0,src)) //Spam Blink when you can
- blink.choose_targets(src)
- next_cast = world.time + 10
- return .
+ if((get_dir(src, target) in list(SOUTH, EAST, WEST, NORTH)) && fireball.can_cast_spell(feedback = FALSE))
+ setDir(get_dir(src, target))
+ fireball.Trigger(null, target)
+ next_cast = world.time + 1 SECONDS
+ return
+
+ if(magic_missile.IsAvailable())
+ magic_missile.Trigger(null, target)
+ next_cast = world.time + 1 SECONDS
+ return
+
+ if(blink.IsAvailable()) // Spam Blink when you can
+ blink.Trigger(null, src)
+ next_cast = world.time + 1 SECONDS
+ return
diff --git a/code/modules/mob/living/simple_animal/slime/slime.dm b/code/modules/mob/living/simple_animal/slime/slime.dm
index e4dc91172bba6..1893a09358298 100644
--- a/code/modules/mob/living/simple_animal/slime/slime.dm
+++ b/code/modules/mob/living/simple_animal/slime/slime.dm
@@ -438,7 +438,7 @@
return
/mob/living/simple_animal/slime/examine(mob/user)
- . = list("*---------*\nThis is [icon2html(src, user)] \a [src]!")
+ . = list("This is [icon2html(src, user)] \a [src]!")
if (stat == DEAD)
. += span_deadsay("It is limp and unresponsive.")
else
@@ -465,7 +465,7 @@
if(10)
. += span_warning("It is radiating with massive levels of electrical activity!")
- . += "*---------*"
+ . += ""
/mob/living/simple_animal/slime/proc/discipline_slime(mob/user)
if(stat)
diff --git a/code/modules/mob/living/status_procs.dm b/code/modules/mob/living/status_procs.dm
index e0f8ccc40ba08..bc186c61d76a1 100644
--- a/code/modules/mob/living/status_procs.dm
+++ b/code/modules/mob/living/status_procs.dm
@@ -71,7 +71,7 @@
return 0
/mob/living/proc/Knockdown(amount, ignore_canstun = FALSE) //Can't go below remaining duration
- if(SEND_SIGNAL(src, /datum/status_effect/incapacitating/knockdown, amount, ignore_canstun) & COMPONENT_NO_STUN)
+ if(SEND_SIGNAL(src, COMSIG_LIVING_STATUS_KNOCKDOWN, amount, ignore_canstun) & COMPONENT_NO_STUN)
return
if(IS_STUN_IMMUNE(src, ignore_canstun))
return
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index bb88f2f91bf95..fbe9e09629a61 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -38,6 +38,7 @@
if(observers?.len)
for(var/mob/dead/observe as anything in observers)
observe.reset_perspective(null)
+
qdel(hud_used)
QDEL_LIST(client_colours)
ghostize() //False, since we're deleting it currently
@@ -349,6 +350,14 @@
/mob/proc/get_item_by_slot(slot_id)
return null
+/// Gets what slot the item on the mob is held in.
+/// Returns null if the item isn't in any slots on our mob.
+/// Does not check if the passed item is null, which may result in unexpected outcoms.
+/mob/proc/get_slot_by_item(obj/item/looking_for)
+ if(looking_for in held_items)
+ return ITEM_SLOT_HANDS
+
+ return null
///Is the mob incapacitated
/mob/proc/incapacitated(flags)
@@ -533,7 +542,11 @@
else
result = examinify.examine(src) // if a tree is examined but no client is there to see it, did the tree ever really exist?
- to_chat(src, "[result.Join("\n")]")
+ if(result.len)
+ for(var/i in 1 to (length(result) - 1))
+ result[i] += "\n"
+
+ to_chat(src, examine_block("[result.Join()]"))
SEND_SIGNAL(src, COMSIG_MOB_EXAMINATE, examinify)
@@ -549,10 +562,14 @@
//you can examine things you're holding directly, but you can't examine other things if your hands are full
/// the item in our active hand
- var/active_item = get_active_held_item()
- if(active_item && active_item != examined_thing)
- to_chat(src, span_warning("Your hands are too full to examine this!"))
- return FALSE
+ var/obj/item/active_item = get_active_held_item()
+ var/boosted = FALSE
+ if(active_item)
+ if(HAS_TRAIT(active_item, TRAIT_BLIND_TOOL))
+ boosted = TRUE
+ else if(active_item != examined_thing)
+ to_chat(src, span_warning("Your hands are too full to examine this!"))
+ return FALSE
//you can only initiate exaimines if you have a hand, it's not disabled, and only as many examines as you have hands
/// our active hand, to check if it's disabled/detatched
@@ -570,6 +587,8 @@
/// how long it takes for the blind person to find the thing they're examining
var/examine_delay_length = rand(1 SECONDS, 2 SECONDS)
+ if(boosted)
+ examine_delay_length = 0.5 SECONDS
if(client?.recent_examines && client?.recent_examines[ref(examined_thing)]) //easier to find things we just touched
examine_delay_length = 0.33 SECONDS
else if(isobj(examined_thing))
@@ -822,30 +841,33 @@
/mob/proc/get_status_tab_items()
. = list()
-/// Gets all relevant proc holders for the browser statpenl
-/mob/proc/get_proc_holders()
- . = list()
- if(mind)
- . += get_spells_for_statpanel(mind.spell_list)
- . += get_spells_for_statpanel(mob_spell_list)
-
/**
* Convert a list of spells into a displyable list for the statpanel
*
* Shows charge and other important info
*/
-/mob/proc/get_spells_for_statpanel(list/spells)
- var/list/L = list()
- for(var/obj/effect/proc_holder/spell/S in spells)
- if(S.can_be_cast_by(src))
- switch(S.charge_type)
- if("recharge")
- L[++L.len] = list("[S.panel]", "[S.charge_counter/10.0]/[S.charge_max/10]", S.name, REF(S))
- if("charges")
- L[++L.len] = list("[S.panel]", "[S.charge_counter]/[S.charge_max]", S.name, REF(S))
- if("holdervar")
- L[++L.len] = list("[S.panel]", "[S.holder_var_type] [S.holder_var_amount]", S.name, REF(S))
- return L
+/mob/proc/get_actions_for_statpanel()
+ var/list/data = list()
+ for(var/datum/action/cooldown/action in actions)
+ var/list/action_data = action.set_statpanel_format()
+ if(!length(action_data))
+ return
+
+ data += list(list(
+ // the panel the action gets displayed to
+ // in the future, this could probably be replaced with subtabs (a la admin tabs)
+ action_data[PANEL_DISPLAY_PANEL],
+ // the status of the action, - cooldown, charges, whatever
+ action_data[PANEL_DISPLAY_STATUS],
+ // the name of the action
+ action_data[PANEL_DISPLAY_NAME],
+ // a ref to the action button of this action for this mob
+ // it's a ref to the button specifically, instead of the action itself,
+ // because statpanel href calls click(), which the action button (not the action itself) handles
+ REF(action.viewers[hud_used]),
+ ))
+
+ return data
/mob/proc/swap_hand()
var/obj/item/held_item = get_active_held_item()
@@ -877,32 +899,6 @@
ghost.notify_cloning(message, sound, source, flashwindow)
return ghost
-///Add a spell to the mobs spell list
-/mob/proc/AddSpell(obj/effect/proc_holder/spell/S)
- // HACK: Preferences menu creates one of every selectable species.
- // Some species, like vampires, create spells when they're made.
- // The "action" is created when those spells Initialize.
- // Preferences menu can create these assets at *any* time, primarily before
- // the atoms SS initializes.
- // That means "action" won't exist.
- if (isnull(S.action))
- return
-
- LAZYADD(mob_spell_list, S)
- S.action.Grant(src)
-
-///Remove a spell from the mobs spell list
-/mob/proc/RemoveSpell(obj/effect/proc_holder/spell/spell)
- if(!spell)
- return
- for(var/X in mob_spell_list)
- var/obj/effect/proc_holder/spell/S = X
- if(istype(S, spell))
- LAZYREMOVE(mob_spell_list, S)
- qdel(S)
- if(client)
- client.stat_panel.send_message("check_spells")
-
/**
* Checks to see if the mob can cast normal magic spells.
*
@@ -1152,7 +1148,7 @@
/// This mob is abile to read books
/mob/proc/is_literate()
- return FALSE
+ return HAS_TRAIT(src, TRAIT_LITERATE) && !HAS_TRAIT(src, TRAIT_ILLITERATE)
/// Can this mob write
/mob/proc/can_write(obj/writing_instrument)
@@ -1221,7 +1217,6 @@
VV_DROPDOWN_OPTION(VV_HK_DIRECT_CONTROL, "Assume Direct Control")
VV_DROPDOWN_OPTION(VV_HK_GIVE_DIRECT_CONTROL, "Give Direct Control")
VV_DROPDOWN_OPTION(VV_HK_OFFER_GHOSTS, "Offer Control to Ghosts")
- VV_DROPDOWN_OPTION(VV_HK_SDQL_SPELL, "Give SDQL Spell")
/mob/vv_do_topic(list/href_list)
. = ..()
@@ -1273,10 +1268,7 @@
if(!check_rights(NONE))
return
offer_control(src)
- if(href_list[VV_HK_SDQL_SPELL])
- if(!check_rights(R_DEBUG))
- return
- usr.client.cmd_sdql_spell_menu(src)
+
/**
* extra var handling for the logging var
*/
diff --git a/code/modules/mob/mob_defines.dm b/code/modules/mob/mob_defines.dm
index 1672ff86d612e..15fe1b0f1924b 100644
--- a/code/modules/mob/mob_defines.dm
+++ b/code/modules/mob/mob_defines.dm
@@ -48,8 +48,9 @@
var/cached_multiplicative_actions_slowdown
/// List of action hud items the user has
var/list/datum/action/actions
- /// A special action? No idea why this lives here
- var/list/datum/action/chameleon_item_actions
+ /// A list of chameleon actions we have specifically
+ /// This can be unified with the actions list
+ var/list/datum/action/item_action/chameleon/chameleon_item_actions
///Cursor icon used when holding shift over things
var/examine_cursor_icon = 'icons/effects/mouse_pointers/examine_pointer.dmi'
@@ -163,15 +164,6 @@
///A weakref to the last mob/living/carbon to push/drag/grab this mob (exclusively used by slimes friend recognition)
var/datum/weakref/LAssailant = null
- /**
- * construct spells and mime spells.
- *
- * Spells that do not transfer from one mob to another and can not be lost in mindswap.
- * obviously do not live in the mind
- */
- var/list/mob_spell_list
-
-
/// bitflags defining which status effects can be inflicted (replaces canknockdown, canstun, etc)
var/status_flags = CANSTUN|CANKNOCKDOWN|CANUNCONSCIOUS|CANPUSH
@@ -234,3 +226,13 @@
var/datum/client_interface/mock_client
var/interaction_range = 0 //how far a mob has to be to interact with something without caring about obsctruction, defaulted to 0 tiles
+
+ /// Typing indicator - mob is typing into a input
+ var/typing_indicator = FALSE
+ /// Thinking indicator - mob has input window open
+ var/thinking_indicator = FALSE
+ /// User is thinking in character. Used to revert to thinking state after stop_typing
+ var/thinking_IC = FALSE
+
+ ///how much gravity is slowing us down
+ var/gravity_slowdown = 0
diff --git a/code/modules/mob/mob_movement.dm b/code/modules/mob/mob_movement.dm
index 7b4aba4a17de3..ea80a661150e1 100644
--- a/code/modules/mob/mob_movement.dm
+++ b/code/modules/mob/mob_movement.dm
@@ -70,9 +70,9 @@
next_move_dir_sub = 0
var/old_move_delay = move_delay
move_delay = world.time + world.tick_lag //this is here because Move() can now be called mutiple times per tick
- if(!mob || !mob.loc)
+ if(!direct || !new_loc)
return FALSE
- if(!new_loc || !direct)
+ if(!mob?.loc)
return FALSE
if(mob.notransform)
return FALSE //This is sota the goto stop mobs from moving var
@@ -118,7 +118,9 @@
//We are now going to move
var/add_delay = mob.cached_multiplicative_slowdown
- mob.set_glide_size(DELAY_TO_GLIDE_SIZE(add_delay * ( (NSCOMPONENT(direct) && EWCOMPONENT(direct)) ? SQRT_2 : 1 ) )) // set it now in case of pulled objects
+ var/new_glide_size = DELAY_TO_GLIDE_SIZE(add_delay * ( (NSCOMPONENT(direct) && EWCOMPONENT(direct)) ? SQRT_2 : 1 ) )
+ if(mob.glide_size != new_glide_size)
+ mob.set_glide_size(new_glide_size) // set it now in case of pulled objects
//If the move was recent, count using old_move_delay
//We want fractional behavior and all
if(old_move_delay + world.tick_lag > world.time)
@@ -137,10 +139,15 @@
if((direct & (direct - 1)) && mob.loc == new_loc) //moved diagonally successfully
add_delay *= SQRT_2
+ var/after_glide = 0
if(visual_delay)
- mob.set_glide_size(visual_delay)
+ after_glide = visual_delay
else
- mob.set_glide_size(DELAY_TO_GLIDE_SIZE(add_delay))
+ after_glide = DELAY_TO_GLIDE_SIZE(add_delay)
+
+ if(after_glide != mob.glide_size)
+ mob.set_glide_size(after_glide)
+
move_delay += add_delay
if(.) // If mob is null here, we deserve the runtime
if(mob.throwing)
@@ -358,10 +365,12 @@
/// Update the gravity status of this mob
/mob/proc/update_gravity(has_gravity, override=FALSE)
var/speed_change = max(0, has_gravity - STANDARD_GRAVITY)
- if(!speed_change)
+ if(!speed_change && gravity_slowdown)
remove_movespeed_modifier(/datum/movespeed_modifier/gravity)
- else
+ gravity_slowdown = 0
+ else if(gravity_slowdown != speed_change)
add_or_update_variable_movespeed_modifier(/datum/movespeed_modifier/gravity, multiplicative_slowdown=speed_change)
+ gravity_slowdown = speed_change
//bodypart selection verbs - Cyberboss
//8: repeated presses toggles through head - eyes - mouth
diff --git a/code/modules/mob/mob_say.dm b/code/modules/mob/mob_say.dm
index 1b6f51f1c023d..b84d47cfde31c 100644
--- a/code/modules/mob/mob_say.dm
+++ b/code/modules/mob/mob_say.dm
@@ -28,8 +28,14 @@
if(message)
SSspeech_controller.queue_say_for_mob(src, message, SPEECH_CONTROLLER_QUEUE_WHISPER_VERB)
-///whisper a message
-/mob/proc/whisper(message, datum/language/language=null)
+/**
+ * Whisper a message.
+ *
+ * Basic level implementation just speaks the message, nothing else.
+ */
+/mob/proc/whisper(message, bubble_type, list/spans = list(), sanitize = TRUE, datum/language/language, ignore_spam = FALSE, forced, filterproof)
+ if(!message)
+ return
say(message, language = language)
///The me emote verb
diff --git a/code/modules/mob/transform_procs.dm b/code/modules/mob/transform_procs.dm
index 2fb1f40f9aaa4..f77ef33dafd33 100644
--- a/code/modules/mob/transform_procs.dm
+++ b/code/modules/mob/transform_procs.dm
@@ -27,6 +27,7 @@
set_species(/datum/species/monkey)
SEND_SIGNAL(src, COMSIG_HUMAN_MONKEYIZE)
uncuff()
+ regenerate_icons()
return src
////////////////////////// Humanize //////////////////////////////
@@ -57,6 +58,7 @@
invisibility = 0
set_species(species)
SEND_SIGNAL(src, COMSIG_MONKEY_HUMANIZE)
+ regenerate_icons()
return src
/mob/proc/AIize(client/preference_source, move = TRUE)
diff --git a/code/modules/mob_spawn/corpses/mob_corpses.dm b/code/modules/mob_spawn/corpses/mob_corpses.dm
index 33888fbc12616..ce7809e87a44d 100644
--- a/code/modules/mob_spawn/corpses/mob_corpses.dm
+++ b/code/modules/mob_spawn/corpses/mob_corpses.dm
@@ -38,6 +38,15 @@
id = /obj/item/card/id/advanced/chameleon
id_trim = /datum/id_trim/chameleon/operative
+/obj/effect/mob_spawn/corpse/human/syndicatecommando/lessenedgear
+ outfit = /datum/outfit/syndicatecommandocorpse/lessenedgear
+
+/datum/outfit/syndicatecommandocorpse/lessenedgear
+ name = "Syndicate Commando Corpse"
+ gloves = /obj/item/clothing/gloves/tackler
+ back = null
+ id = null
+ id_trim = null
/obj/effect/mob_spawn/corpse/human/syndicatestormtrooper
name = "Syndicate Stormtrooper"
diff --git a/code/modules/mob_spawn/ghost_roles/space_roles.dm b/code/modules/mob_spawn/ghost_roles/space_roles.dm
index a74ac6167a988..d3c65ee5459b5 100644
--- a/code/modules/mob_spawn/ghost_roles/space_roles.dm
+++ b/code/modules/mob_spawn/ghost_roles/space_roles.dm
@@ -96,7 +96,7 @@
/obj/effect/mob_spawn/ghost_role/human/lavaland_syndicate/comms/space/Initialize(mapload)
. = ..()
- if(prob(90)) //only has a 10% chance of existing, otherwise it'll just be a NPC syndie.
+ if(prob(85)) //only has a 15% chance of existing, otherwise it'll just be a NPC syndie.
new /mob/living/simple_animal/hostile/syndicate/ranged(get_turf(src))
return INITIALIZE_HINT_QDEL
diff --git a/code/modules/mob_spawn/ghost_roles/unused_roles.dm b/code/modules/mob_spawn/ghost_roles/unused_roles.dm
index 00390d533bec9..cfc49a57207e7 100644
--- a/code/modules/mob_spawn/ghost_roles/unused_roles.dm
+++ b/code/modules/mob_spawn/ghost_roles/unused_roles.dm
@@ -263,9 +263,6 @@
outfit = /datum/outfit/syndicatespace/syndicrew
spawner_job_path = /datum/job/syndicate_cybersun
-/datum/outfit/syndicatespace/syndicrew/post_equip(mob/living/carbon/human/H)
- H.faction |= ROLE_SYNDICATE
-
/obj/effect/mob_spawn/ghost_role/human/syndicatespace/special(mob/living/new_spawn)
. = ..()
new_spawn.grant_language(/datum/language/codespeak, TRUE, TRUE, LANGUAGE_MIND)
@@ -283,39 +280,37 @@
outfit = /datum/outfit/syndicatespace/syndicaptain
spawner_job_path = /datum/job/syndicate_cybersun_captain
-/datum/outfit/syndicatespace/syndicaptain/post_equip(mob/living/carbon/human/H)
- H.faction |= ROLE_SYNDICATE
-
/obj/effect/mob_spawn/ghost_role/human/syndicatespace/captain/Destroy()
new /obj/structure/fluff/empty_sleeper/syndicate/captain(get_turf(src))
return ..()
-/datum/outfit/syndicatespace/syndicrew
- name = "Syndicate Ship Crew Member"
+/datum/outfit/syndicatespace
+ name = "Syndicate Ship Base"
uniform = /obj/item/clothing/under/syndicate/combat
- glasses = /obj/item/clothing/glasses/night
- mask = /obj/item/clothing/mask/gas/syndicate
ears = /obj/item/radio/headset/syndicate/alt
shoes = /obj/item/clothing/shoes/combat
gloves = /obj/item/clothing/gloves/combat
back = /obj/item/storage/backpack
- l_pocket = /obj/item/gun/ballistic/automatic/pistol
- r_pocket = /obj/item/knife/combat/survival
belt = /obj/item/storage/belt/military/assault
id = /obj/item/card/id/advanced/black/syndicate_command/crew_id
implants = list(/obj/item/implant/weapons_auth)
+/datum/outfit/syndicatespace/post_equip(mob/living/carbon/human/syndie_scum)
+ syndie_scum.faction |= ROLE_SYNDICATE
+
+/datum/outfit/syndicatespace/syndicrew
+ name = "Syndicate Ship Crew Member"
+ glasses = /obj/item/clothing/glasses/night
+ mask = /obj/item/clothing/mask/gas/syndicate
+ l_pocket = /obj/item/gun/ballistic/automatic/pistol
+ r_pocket = /obj/item/knife/combat/survival
+
/datum/outfit/syndicatespace/syndicaptain
name = "Syndicate Ship Captain"
uniform = /obj/item/clothing/under/syndicate/combat
suit = /obj/item/clothing/suit/armor/vest/capcarapace/syndicate
head = /obj/item/clothing/head/hos/beret/syndicate
ears = /obj/item/radio/headset/syndicate/alt/leader
- shoes = /obj/item/clothing/shoes/combat
- gloves = /obj/item/clothing/gloves/combat
- back = /obj/item/storage/backpack
r_pocket = /obj/item/knife/combat/survival
- belt = /obj/item/storage/belt/military/assault
id = /obj/item/card/id/advanced/black/syndicate_command/captain_id
- implants = list(/obj/item/implant/weapons_auth)
backpack_contents = list(/obj/item/documents/syndicate/red, /obj/item/paper/fluff/ruins/forgottenship/password, /obj/item/gun/ballistic/automatic/pistol/aps)
diff --git a/code/modules/mod/mod_control.dm b/code/modules/mod/mod_control.dm
index 51c0a6924d82d..db091a89939dd 100644
--- a/code/modules/mod/mod_control.dm
+++ b/code/modules/mod/mod_control.dm
@@ -545,13 +545,7 @@
new_module.on_install()
if(wearer)
new_module.on_equip()
- var/datum/action/item_action/mod/pinned_module/action = new_module.pinned_to[REF(wearer)]
- if(action)
- action.Grant(wearer)
- if(ai)
- var/datum/action/item_action/mod/pinned_module/action = new_module.pinned_to[REF(ai)]
- if(action)
- action.Grant(ai)
+
if(user)
balloon_alert(user, "[new_module] added")
playsound(src, 'sound/machines/click.ogg', 50, TRUE, SILENCED_SOUND_EXTRARANGE)
@@ -564,7 +558,7 @@
if(old_module.active)
old_module.on_deactivation(display_message = !deleting, deleting = deleting)
old_module.on_uninstall(deleting = deleting)
- QDEL_LIST(old_module.pinned_to)
+ QDEL_LIST_ASSOC_VAL(old_module.pinned_to)
old_module.mod = null
/obj/item/mod/control/proc/update_access(mob/user, obj/item/card/id/card)
@@ -676,10 +670,12 @@
uninstall(part)
return
if(part in mod_parts)
+ if(!wearer)
+ part.forceMove(src)
+ return
retract(wearer, part)
if(active)
INVOKE_ASYNC(src, .proc/toggle_activate, wearer, TRUE)
- return
/obj/item/mod/control/proc/on_part_destruction(obj/item/part, damage_flag)
SIGNAL_HANDLER
diff --git a/code/modules/mod/mod_theme.dm b/code/modules/mod/mod_theme.dm
index d6ca4f8303bbf..b09812ddaf8b7 100644
--- a/code/modules/mod/mod_theme.dm
+++ b/code/modules/mod/mod_theme.dm
@@ -609,10 +609,8 @@
hostile situations. These suits have been layered with plating worthy enough for fires or corrosive environments, \
and come with composite cushioning and an advanced honeycomb structure underneath the hull to ensure protection \
against broken bones or possible avulsions. The suit's legs have been given more rugged actuators, \
- allowing the suit to do more work in carrying the weight. Lastly, these have been given a shock-absorbing \
- insulating layer on the gauntlets, making sure the user isn't under risk of electricity. \
- However, the systems used in these suits are more than a few years out of date, \
- leading to an overall lower capacity for modules."
+ allowing the suit to do more work in carrying the weight. However, the systems used in these suits are more than \
+ a few years out of date, leading to an overall lower capacity for modules."
default_skin = "security"
armor = list(MELEE = 15, BULLET = 15, LASER = 15, ENERGY = 15, BOMB = 25, BIO = 100, FIRE = 75, ACID = 75, WOUND = 15)
complexity_max = DEFAULT_MAX_COMPLEXITY - 3
diff --git a/code/modules/mod/mod_types.dm b/code/modules/mod/mod_types.dm
index 05943ab7ae322..1f149cf7024ac 100644
--- a/code/modules/mod/mod_types.dm
+++ b/code/modules/mod/mod_types.dm
@@ -218,12 +218,13 @@
theme = /datum/mod_theme/ninja
applied_cell = /obj/item/stock_parts/cell/ninja
initial_modules = list(
+ /obj/item/mod/module/storage,
/obj/item/mod/module/noslip,
+ /obj/item/mod/module/status_readout,
/obj/item/mod/module/stealth/ninja,
/obj/item/mod/module/dispenser/ninja,
/obj/item/mod/module/dna_lock/reinforced,
/obj/item/mod/module/emp_shield/pulse,
- /obj/item/mod/module/status_readout,
)
/obj/item/mod/control/pre_equipped/prototype
diff --git a/code/modules/mod/modules/_module.dm b/code/modules/mod/modules/_module.dm
index 8296ac3a11ee7..20ac702343979 100644
--- a/code/modules/mod/modules/_module.dm
+++ b/code/modules/mod/modules/_module.dm
@@ -294,12 +294,13 @@
/// Pins the module to the user's action buttons
/obj/item/mod/module/proc/pin(mob/user)
- var/datum/action/item_action/mod/pinned_module/action = pinned_to[REF(user)]
- if(action)
- qdel(action)
- else
- action = new(mod, src, user)
- action.Grant(user)
+ var/datum/action/item_action/mod/pinned_module/existing_action = pinned_to[REF(user)]
+ if(existing_action)
+ mod.remove_item_action(existing_action)
+ return
+
+ var/datum/action/item_action/mod/pinned_module/new_action = new(mod, src, user)
+ mod.add_item_action(new_action)
/// On drop key, concels a device item.
/obj/item/mod/module/proc/dropkey(mob/living/user)
diff --git a/code/modules/mod/modules/modules_general.dm b/code/modules/mod/modules/modules_general.dm
index dbdfa80569509..2dfeebafd1a9a 100644
--- a/code/modules/mod/modules/modules_general.dm
+++ b/code/modules/mod/modules/modules_general.dm
@@ -42,8 +42,9 @@
if(!deleting)
SEND_SIGNAL(src, COMSIG_TRY_STORAGE_QUICK_EMPTY, drop_location())
SEND_SIGNAL(src, COMSIG_TRY_STORAGE_SET_LOCKSTATE, TRUE)
+
/obj/item/mod/module/storage/proc/on_chestplate_unequip(obj/item/source, force, atom/newloc, no_move, invdrop, silent)
- if(QDELETED(source) || newloc == mod.wearer || !mod.wearer.s_store)
+ if(QDELETED(source) || !mod.wearer || newloc == mod.wearer || !mod.wearer.s_store)
return
to_chat(mod.wearer, span_notice("[src] tries to store [mod.wearer.s_store] inside itself."))
SEND_SIGNAL(src, COMSIG_TRY_STORAGE_INSERT, mod.wearer.s_store, mod.wearer, TRUE)
@@ -75,7 +76,6 @@
max_combined_w_class = 60
max_items = 21
-
///Ion Jetpack - Lets the user fly freely through space using battery charge.
/obj/item/mod/module/jetpack
name = "MOD ion jetpack module"
diff --git a/code/modules/mod/modules/modules_maint.dm b/code/modules/mod/modules/modules_maint.dm
index da8d5e5578d89..24e9beb8fc7b7 100644
--- a/code/modules/mod/modules/modules_maint.dm
+++ b/code/modules/mod/modules/modules_maint.dm
@@ -105,7 +105,7 @@
rave_screen = mod.wearer.add_client_colour(/datum/client_colour/rave)
rave_screen.update_colour(rainbow_order[rave_number])
if(selection)
- SEND_SOUND(mod.wearer, sound(selection.song_path, volume = 50, channel = CHANNEL_JUKEBOX))
+ mod.wearer.playsound_local(get_turf(src), null, 50, channel = CHANNEL_JUKEBOX, sound_to_use = sound(selection.song_path), use_reverb = FALSE)
/obj/item/mod/module/visor/rave/on_deactivation(display_message = TRUE, deleting = FALSE)
. = ..()
diff --git a/code/modules/mod/modules/modules_security.dm b/code/modules/mod/modules/modules_security.dm
index 3055c242cbdb0..4f987c1b18de7 100644
--- a/code/modules/mod/modules/modules_security.dm
+++ b/code/modules/mod/modules/modules_security.dm
@@ -80,8 +80,8 @@
var/datum/reagents/capsaicin_holder = new(10)
capsaicin_holder.add_reagent(/datum/reagent/consumable/condensedcapsaicin, 10)
var/datum/effect_system/fluid_spread/smoke/chem/quick/smoke = new
- smoke.set_up(1, location = get_turf(src), carry = capsaicin_holder)
- smoke.start()
+ smoke.set_up(1, holder = src, location = get_turf(src), carry = capsaicin_holder)
+ smoke.start(log = TRUE)
QDEL_NULL(capsaicin_holder) // Reagents have a ref to their holder which has a ref to them. No leaks please.
/obj/item/mod/module/pepper_shoulders/proc/on_check_shields()
@@ -337,3 +337,32 @@
projectile.damage /= damage_multiplier
projectile.speed /= speed_multiplier
projectile.cut_overlay(projectile_effect)
+
+///Active Sonar - Displays a hud circle on the turf of any living creatures in the given radius
+/obj/item/mod/module/active_sonar
+ name = "MOD active sonar"
+ desc = "Ancient tech from the 20th century, this module uses sonic waves to detect living creatures within the user's radius. \
+ Its loud ping is much harder to hide in an indoor station than in the outdoor operations it was designed for."
+ icon_state = "active_sonar"
+ module_type = MODULE_USABLE
+ use_power_cost = DEFAULT_CHARGE_DRAIN * 5
+ complexity = 3
+ incompatible_modules = list(/obj/item/mod/module/active_sonar)
+ cooldown_time = 25 SECONDS
+
+/obj/item/mod/module/active_sonar/on_use()
+ . = ..()
+ if(!.)
+ return
+ balloon_alert(mod.wearer, "readying sonar...")
+ playsound(mod.wearer, 'sound/mecha/skyfall_power_up.ogg', vol = 20, vary = TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
+ if(!do_after(mod.wearer, 1.1 SECONDS))
+ return
+ var/creatures_detected = 0
+ for(var/mob/living/creature in range(9, mod.wearer))
+ if(creature == mod.wearer || creature.stat == DEAD)
+ continue
+ new /obj/effect/temp_visual/sonar_ping(mod.wearer.loc, mod.wearer, creature)
+ creatures_detected++
+ playsound(mod.wearer, 'sound/effects/ping_hit.ogg', vol = 75, vary = TRUE, extrarange = MEDIUM_RANGE_SOUND_EXTRARANGE) // Should be audible for the radius of the sonar
+ to_chat(mod.wearer, span_notice("You slam your fist into the ground, sending out a sonic wave that detects [creatures_detected] living beings nearby!"))
diff --git a/code/modules/mod/modules/modules_supply.dm b/code/modules/mod/modules/modules_supply.dm
index 87fe631eeb671..19224adfa22be 100644
--- a/code/modules/mod/modules/modules_supply.dm
+++ b/code/modules/mod/modules/modules_supply.dm
@@ -12,6 +12,7 @@
use_power_cost = DEFAULT_CHARGE_DRAIN * 0.2
incompatible_modules = list(/obj/item/mod/module/gps)
cooldown_time = 0.5 SECONDS
+ allowed_inactive = TRUE
/obj/item/mod/module/gps/Initialize(mapload)
. = ..()
@@ -172,6 +173,7 @@
use_power_cost = DEFAULT_CHARGE_DRAIN * 0.2
incompatible_modules = list(/obj/item/mod/module/orebag)
cooldown_time = 0.5 SECONDS
+ allowed_inactive = TRUE
/// The ores stored in the bag.
var/list/ores = list()
diff --git a/code/modules/mod/modules/modules_timeline.dm b/code/modules/mod/modules/modules_timeline.dm
index 14143d749352e..d84a1c8c1015c 100644
--- a/code/modules/mod/modules/modules_timeline.dm
+++ b/code/modules/mod/modules/modules_timeline.dm
@@ -174,12 +174,12 @@
mod.visible_message(span_warning("[mod.wearer] leaps out of the timeline!"))
mod.wearer.SetAllImmobility(0)
mod.wearer.setStaminaLoss(0, 0)
- phased_mob = new(get_turf(mod.wearer.loc))
- mod.wearer.forceMove(phased_mob)
+ phased_mob = new(get_turf(mod.wearer.loc), mod.wearer)
RegisterSignal(mod, COMSIG_MOD_ACTIVATE, .proc/on_activate_block)
else
//phasing in
- QDEL_NULL(phased_mob)
+ phased_mob.eject_jaunter()
+ phased_mob = null
UnregisterSignal(mod, COMSIG_MOD_ACTIVATE)
mod.visible_message(span_warning("[mod.wearer] drops into the timeline!"))
diff --git a/code/modules/modular_computers/computers/item/computer.dm b/code/modules/modular_computers/computers/item/computer.dm
index 39d3003b9193f..0c6ef4786dbc2 100644
--- a/code/modules/modular_computers/computers/item/computer.dm
+++ b/code/modules/modular_computers/computers/item/computer.dm
@@ -80,8 +80,6 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar
/// Stored pAI in the computer
var/obj/item/paicard/inserted_pai = null
- var/datum/action/item_action/toggle_computer_light/light_butt
-
/obj/item/modular_computer/Initialize(mapload)
. = ..()
@@ -95,7 +93,8 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar
soundloop = new(src, enabled)
UpdateDisplay()
if(has_light)
- light_butt = new(src)
+ add_item_action(/datum/action/item_action/toggle_computer_light)
+
update_appearance()
register_context()
Add_Messenger()
@@ -116,19 +115,10 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar
if(istype(inserted_pai))
QDEL_NULL(inserted_pai)
- if(istype(light_butt))
- QDEL_NULL(light_butt)
physical = null
return ..()
-/obj/item/modular_computer/ui_action_click(mob/user, actiontype)
- if(istype(actiontype, light_butt))
- toggle_flashlight()
- else
- ..()
-
-
/obj/item/modular_computer/pre_attack_secondary(atom/A, mob/living/user, params)
if(active_program?.tap(A, user, params))
user.do_attack_animation(A) //Emulate this animation since we kill the attack in three lines
@@ -558,6 +548,13 @@ GLOBAL_LIST_EMPTY(TabletMessengers) // a list of all active messengers, similar
enabled = 0
update_appearance()
+/obj/item/modular_computer/ui_action_click(mob/user, actiontype)
+ if(istype(actiontype, /datum/action/item_action/toggle_computer_light))
+ toggle_flashlight()
+ return
+
+ return ..()
+
/**
* Toggles the computer's flashlight, if it has one.
*
diff --git a/code/modules/modular_computers/computers/item/role_tablet_presets.dm b/code/modules/modular_computers/computers/item/role_tablet_presets.dm
index 961d16ed01ffc..90248a1df9678 100644
--- a/code/modules/modular_computers/computers/item/role_tablet_presets.dm
+++ b/code/modules/modular_computers/computers/item/role_tablet_presets.dm
@@ -103,6 +103,29 @@
/datum/computer_file/program/signal_commander,
)
+/obj/item/modular_computer/tablet/pda/heads/quartermaster/Initialize(mapload)
+ . = ..()
+ install_component(new /obj/item/computer_hardware/printer/mini)
+
+/obj/item/modular_computer/tablet/pda/heads/quartermaster
+ name = "quartermaster PDA"
+ greyscale_config = /datum/greyscale_config/tablet/stripe_thick
+ greyscale_colors = "#D6B328#6506CA#927444"
+ insert_type = /obj/item/pen/survival
+ default_applications = list(
+ /datum/computer_file/program/crew_manifest,
+ /datum/computer_file/program/status,
+ /datum/computer_file/program/science,
+ /datum/computer_file/program/robocontrol,
+ /datum/computer_file/program/budgetorders,
+ /datum/computer_file/program/shipping,
+ /datum/computer_file/program/robocontrol,
+ )
+
+/obj/item/modular_computer/tablet/pda/heads/quartermaster/Initialize(mapload)
+ . = ..()
+ install_component(new /obj/item/computer_hardware/printer/mini)
+
/**
* Security
*/
@@ -232,25 +255,6 @@
/datum/computer_file/program/robocontrol,
)
-/obj/item/modular_computer/tablet/pda/quartermaster/Initialize(mapload)
- . = ..()
- install_component(new /obj/item/computer_hardware/printer/mini)
-
-/obj/item/modular_computer/tablet/pda/quartermaster
- name = "quartermaster PDA"
- greyscale_config = /datum/greyscale_config/tablet/stripe_thick
- greyscale_colors = "#D6B328#6506CA#927444"
- insert_type = /obj/item/pen/survival
- default_applications = list(
- /datum/computer_file/program/shipping,
- /datum/computer_file/program/budgetorders,
- /datum/computer_file/program/robocontrol,
- )
-
-/obj/item/modular_computer/tablet/pda/quartermaster/Initialize(mapload)
- . = ..()
- install_component(new /obj/item/computer_hardware/printer/mini)
-
/obj/item/modular_computer/tablet/pda/shaftminer
name = "shaft miner PDA"
greyscale_config = /datum/greyscale_config/tablet/stripe_thick
diff --git a/code/modules/modular_computers/file_system/program.dm b/code/modules/modular_computers/file_system/program.dm
index 23a5216f6df3d..02dccac47893f 100644
--- a/code/modules/modular_computers/file_system/program.dm
+++ b/code/modules/modular_computers/file_system/program.dm
@@ -77,7 +77,7 @@
return 0
/**
- *Runs when the device is used to attack an atom in non-combat mode.
+ *Runs when the device is used to attack an atom in non-combat mode using right click (secondary).
*
*Simulates using the device to read or scan something. Tap is called by the computer during pre_attack
*and sends us all of the related info. If we return TRUE, the computer will stop the attack process
diff --git a/code/modules/modular_computers/file_system/programs/atmosscan.dm b/code/modules/modular_computers/file_system/programs/atmosscan.dm
index 9fec1e410325a..c4867955ee1a1 100644
--- a/code/modules/modular_computers/file_system/programs/atmosscan.dm
+++ b/code/modules/modular_computers/file_system/programs/atmosscan.dm
@@ -1,3 +1,8 @@
+/// Scan the turf where the computer is on.
+#define ATMOZPHERE_SCAN_ENV "env"
+/// Scan the objects that the tablet clicks.
+#define ATMOZPHERE_SCAN_CLICK "click"
+
/datum/computer_file/program/atmosscan
filename = "atmosscan"
filedesc = "AtmoZphere"
@@ -8,18 +13,75 @@
tgui_id = "NtosGasAnalyzer"
program_icon = "thermometer-half"
+ /// Whether we scan the current turf automatically (env) or scan tapped objects manually (click).
+ var/atmozphere_mode = ATMOZPHERE_SCAN_ENV
+ /// Saved [GasmixParser][/proc/gas_mixture_parser] data of the last thing we scanned.
+ var/list/last_gasmix_data
+
+/// Secondary attack self.
+/datum/computer_file/program/atmosscan/proc/turf_analyze(datum/source, mob/user)
+ SIGNAL_HANDLER
+ if(atmozphere_mode != ATMOZPHERE_SCAN_CLICK)
+ return
+ atmos_scan(user=user, target=get_turf(computer), silent=FALSE)
+ on_analyze(source=source, target=get_turf(computer))
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+/// Keep this in sync with it's tool based counterpart [/obj/proc/analyzer_act] and [/atom/proc/tool_act]
+/datum/computer_file/program/atmosscan/tap(atom/A, mob/living/user, params)
+ if(atmozphere_mode != ATMOZPHERE_SCAN_CLICK)
+ return FALSE
+ if(!atmos_scan(user=user, target=A, silent=FALSE))
+ return FALSE
+ on_analyze(source=computer, target=A)
+ return TRUE
+
+/// Updates our gasmix data if on click mode.
+/datum/computer_file/program/atmosscan/proc/on_analyze(datum/source, atom/target)
+ var/mixture = target.return_analyzable_air()
+ if(!mixture)
+ return FALSE
+ var/list/airs = islist(mixture) ? mixture : list(mixture)
+ var/list/new_gasmix_data = list()
+ for(var/datum/gas_mixture/air as anything in airs)
+ var/mix_name = capitalize(lowertext(target.name))
+ if(airs.len != 1) //not a unary gas mixture
+ mix_name += " - Node [airs.Find(air)]"
+ new_gasmix_data += list(gas_mixture_parser(air, mix_name))
+ last_gasmix_data = new_gasmix_data
+
/datum/computer_file/program/atmosscan/ui_static_data(mob/user)
return return_atmos_handbooks()
/datum/computer_file/program/atmosscan/ui_data(mob/user)
var/list/data = get_header_data()
var/turf/turf = get_turf(computer)
- var/datum/gas_mixture/air = turf?.return_air()
-
- data["gasmixes"] = list(gas_mixture_parser(air, "Sensor Reading")) //Null air wont cause errors, don't worry.
+ data["atmozphereMode"] = atmozphere_mode
+ data["clickAtmozphereCompatible"] = computer.hardware_flag == PROGRAM_TABLET
+ switch (atmozphere_mode) //Null air wont cause errors, don't worry.
+ if(ATMOZPHERE_SCAN_ENV)
+ var/datum/gas_mixture/air = turf?.return_air()
+ data["gasmixes"] = list(gas_mixture_parser(air, "Location Reading"))
+ if(ATMOZPHERE_SCAN_CLICK)
+ LAZYINITLIST(last_gasmix_data)
+ data["gasmixes"] = last_gasmix_data
return data
/datum/computer_file/program/atmosscan/ui_act(action, list/params)
. = ..()
if(.)
return
+ switch(action)
+ if("scantoggle")
+ if(atmozphere_mode == ATMOZPHERE_SCAN_CLICK)
+ atmozphere_mode = ATMOZPHERE_SCAN_ENV
+ UnregisterSignal(computer, COMSIG_ITEM_ATTACK_SELF_SECONDARY)
+ return TRUE
+ if(computer.hardware_flag != PROGRAM_TABLET)
+ computer.say("Device incompatible for scanning objects!")
+ return FALSE
+ atmozphere_mode = ATMOZPHERE_SCAN_CLICK
+ RegisterSignal(computer, COMSIG_ITEM_ATTACK_SELF_SECONDARY, .proc/turf_analyze)
+ var/turf/turf = get_turf(computer)
+ last_gasmix_data = list(gas_mixture_parser(turf?.return_air(), "Location Reading"))
+ return TRUE
diff --git a/code/modules/modular_computers/file_system/programs/budgetordering.dm b/code/modules/modular_computers/file_system/programs/budgetordering.dm
index 9962eee9157b7..017bbaa99aa1e 100644
--- a/code/modules/modular_computers/file_system/programs/budgetordering.dm
+++ b/code/modules/modular_computers/file_system/programs/budgetordering.dm
@@ -73,7 +73,7 @@
var/obj/item/computer_hardware/card_slot/card_slot = computer.all_components[MC_CARD]
var/obj/item/card/id/id_card = card_slot?.GetID()
if(id_card?.registered_account)
- if((ACCESS_COMMAND in id_card.access) || (ACCESS_QM in id_card.access))
+ if((ACCESS_COMMAND in id_card.access))
requestonly = FALSE
buyer = SSeconomy.get_dep_account(id_card.registered_account.account_job.paycheck_department)
can_approve_requests = TRUE
@@ -100,7 +100,7 @@
"name" = P.group,
"packs" = list()
)
- if((P.hidden && (P.contraband && !contraband) || (P.special && !P.special_enabled) || P.DropPodOnly))
+ if((P.hidden && (P.contraband && !contraband) || (P.special && !P.special_enabled) || P.drop_pod_only))
continue
data["supplies"][P.group]["packs"] += list(list(
"name" = P.name,
@@ -194,7 +194,7 @@
var/datum/supply_pack/pack = SSshuttle.supply_packs[id]
if(!istype(pack))
return
- if(pack.hidden || pack.contraband || pack.DropPodOnly || (pack.special && !pack.special_enabled))
+ if(pack.hidden || pack.contraband || pack.drop_pod_only || (pack.special && !pack.special_enabled))
return
var/name = "*None Provided*"
diff --git a/code/modules/modular_computers/file_system/programs/ntmessenger.dm b/code/modules/modular_computers/file_system/programs/ntmessenger.dm
index 3d6eb7b631019..dbf8c6a428f63 100644
--- a/code/modules/modular_computers/file_system/programs/ntmessenger.dm
+++ b/code/modules/modular_computers/file_system/programs/ntmessenger.dm
@@ -351,7 +351,7 @@
if(signal.data["emojis"] == TRUE)//so will not parse emojis as such from pdas that don't send emojis
inbound_message = emoji_parse(inbound_message)
- if(ringer_status)
+ if(ringer_status && L.is_literate())
to_chat(L, "[icon2html(src)] PDA message from [hrefstart][signal.data["name"]] ([signal.data["job"]])[hrefend], [inbound_message] [reply]")
diff --git a/code/modules/modular_computers/file_system/programs/techweb.dm b/code/modules/modular_computers/file_system/programs/techweb.dm
index 1ee417b93d1eb..28c91a44e75be 100644
--- a/code/modules/modular_computers/file_system/programs/techweb.dm
+++ b/code/modules/modular_computers/file_system/programs/techweb.dm
@@ -8,12 +8,12 @@
size = 10
tgui_id = "NtosTechweb"
program_icon = "atom"
- required_access = list(ACCESS_COMMAND, ACCESS_SCIENCE)
+ required_access = list(ACCESS_COMMAND, ACCESS_RESEARCH)
transfer_access = list(ACCESS_RESEARCH)
/// Reference to global science techweb
var/datum/techweb/stored_research
/// Access needed to lock/unlock the console
- var/lock_access = ACCESS_SCIENCE
+ var/lock_access = ACCESS_RESEARCH
/// Determines if the console is locked, and consequently if actions can be performed with it
var/locked = FALSE
/// Used for compressing data sent to the UI via static_data as payload size is of concern
diff --git a/code/modules/modular_computers/hardware/program_disks.dm b/code/modules/modular_computers/hardware/program_disks.dm
index bc7776af9d085..63f10a9d2ca06 100644
--- a/code/modules/modular_computers/hardware/program_disks.dm
+++ b/code/modules/modular_computers/hardware/program_disks.dm
@@ -128,6 +128,15 @@
. = ..()
store_file(new /datum/computer_file/program/signal_commander(src))
+/obj/item/computer_hardware/hard_drive/portable/scipaper_program
+ name = "NT Frontier data disk"
+ desc = "Data disk containing NT Frontier. Simply insert to a computer and open File Manager!"
+ icon_state = "datadisk5"
+
+/obj/item/computer_hardware/hard_drive/portable/scipaper_program/install_default_programs()
+ . = ..()
+ store_file(new /datum/computer_file/program/scipaper_program(src))
+
/**
* Engineering
*/
diff --git a/code/modules/movespeed/modifiers/reagent.dm b/code/modules/movespeed/modifiers/reagent.dm
index 1d4898e662b1d..6700854b11dd5 100644
--- a/code/modules/movespeed/modifiers/reagent.dm
+++ b/code/modules/movespeed/modifiers/reagent.dm
@@ -31,6 +31,9 @@
/datum/movespeed_modifier/reagent/halon
multiplicative_slowdown = 1.8
+/datum/movespeed_modifier/reagent/hypernoblium
+ multiplicative_slowdown = 0.5
+
/datum/movespeed_modifier/reagent/lenturi
multiplicative_slowdown = 1.5
diff --git a/code/modules/ninja/ninjaDrainAct.dm b/code/modules/ninja/ninjaDrainAct.dm
index 02e66652792e5..314ec728ab61f 100644
--- a/code/modules/ninja/ninjaDrainAct.dm
+++ b/code/modules/ninja/ninjaDrainAct.dm
@@ -169,16 +169,13 @@
return NONE
if(hacking_module.communication_console_hack_success)
return NONE
- if(machine_stat & (NOPOWER|BROKEN))
- return NONE
- AI_notify_hack()
INVOKE_ASYNC(src, .proc/ninjadrain_charge, ninja, hacking_module)
return COMPONENT_CANCEL_ATTACK_CHAIN
/obj/machinery/computer/communications/proc/ninjadrain_charge(mob/living/carbon/human/ninja, obj/item/mod/module/hacker/hacking_module)
- if(!do_after(ninja, 30 SECONDS, src))
+ if(!try_hack_console(ninja))
return
- hack_console(ninja)
+
hacking_module.communication_console_hack_success = TRUE
var/datum/antagonist/ninja/ninja_antag = ninja.mind.has_antag_datum(/datum/antagonist/ninja)
if(!ninja_antag)
diff --git a/code/modules/ninja/outfit.dm b/code/modules/ninja/outfit.dm
index 7d96a44638914..f91f660a3e8b9 100644
--- a/code/modules/ninja/outfit.dm
+++ b/code/modules/ninja/outfit.dm
@@ -28,6 +28,8 @@
recall.set_weapon(weapon)
/datum/outfit/ninja_preview
+ name = "Space Ninja (Preview only)"
+
uniform = /obj/item/clothing/under/syndicate/ninja
back = /obj/item/mod/control/pre_equipped/empty/ninja
belt = /obj/item/energy_katana
diff --git a/code/modules/paperwork/desk_bell.dm b/code/modules/paperwork/desk_bell.dm
index 3b08769aff3d0..46725315ac177 100644
--- a/code/modules/paperwork/desk_bell.dm
+++ b/code/modules/paperwork/desk_bell.dm
@@ -106,3 +106,18 @@
/obj/structure/desk_bell/speed_demon
desc = "The cornerstone of any customer service job. This one's been modified for hyper-performance."
ring_cooldown_length = 0
+
+/obj/structure/desk_bell/MouseDrop(obj/over_object, src_location, over_location)
+ if(!istype(over_object, /obj/vehicle/ridden/wheelchair))
+ return
+ if(!Adjacent(over_object) || !Adjacent(usr))
+ return
+ var/obj/vehicle/ridden/wheelchair/target = over_object
+ if(target.bell_attached)
+ usr.balloon_alert(usr, "already has a bell!")
+ return
+ usr.balloon_alert(usr, "attaching bell...")
+ if(!do_after(usr, 0.5 SECONDS))
+ return
+ target.attach_bell(src)
+ return ..()
diff --git a/code/modules/paperwork/pen.dm b/code/modules/paperwork/pen.dm
index 954ba49f78e70..e87e0ec59fa73 100644
--- a/code/modules/paperwork/pen.dm
+++ b/code/modules/paperwork/pen.dm
@@ -309,3 +309,10 @@
toolspeed = 10 //You will never willingly choose to use one of these over a shovel.
font = FOUNTAIN_PEN_FONT
colour = "#0000FF"
+
+/obj/item/pen/destroyer
+ name = "Fine Tipped Pen"
+ desc = "A pen with an infinitly sharpened tip. Capable of striking the weakest point of a strucutre or robot and annihilating it instantly. Good at putting holes in people too."
+ force = 5
+ wound_bonus = 100
+ demolition_mod = 9000
diff --git a/code/modules/photography/photos/album.dm b/code/modules/photography/photos/album.dm
index 3d004f5e8fca8..bbf315f5e8f7e 100644
--- a/code/modules/photography/photos/album.dm
+++ b/code/modules/photography/photos/album.dm
@@ -111,6 +111,11 @@
icon_state = "album_blue"
persistence_id = "chapel"
+/obj/item/storage/photo_album/listeningstation
+ name = "photo album (Listening Station)"
+ icon_state = "album_red"
+ persistence_id = "listeningstation"
+
/obj/item/storage/photo_album/prison
name = "photo album (Prison)"
icon_state = "album_blue"
diff --git a/code/modules/plumbing/ducts.dm b/code/modules/plumbing/ducts.dm
index 295cc7b790dbb..6faa86ab48812 100644
--- a/code/modules/plumbing/ducts.dm
+++ b/code/modules/plumbing/ducts.dm
@@ -7,6 +7,7 @@ All the important duct code:
name = "fluid duct"
icon = 'icons/obj/plumbing/fluid_ducts.dmi'
icon_state = "nduct"
+ layer = PLUMBING_PIPE_VISIBILE_LAYER
use_power = NO_POWER_USE
@@ -22,7 +23,7 @@ All the important duct code:
var/capacity = 10
///the color of our duct
- var/duct_color = null
+ var/duct_color = COLOR_VERY_LIGHT_GRAY
///TRUE to ignore colors, so yeah we also connect with other colors without issue
var/ignore_colors = FALSE
///1,2,4,8,16
@@ -35,122 +36,113 @@ All the important duct code:
var/active = TRUE
///track ducts we're connected to. Mainly for ducts we connect to that we normally wouldn't, like different layers and colors, for when we regenerate the ducts
var/list/neighbours = list()
- ///wheter we just unanchored or drop whatever is in the variable. either is safe
+ ///what stack to drop when disconnected. Must be /obj/item/stack/ducts or a subtype
var/drop_on_wrench = /obj/item/stack/ducts
-/obj/machinery/duct/Initialize(mapload, no_anchor, color_of_duct = "#ffffff", layer_of_duct = DUCT_LAYER_DEFAULT, force_connects)
+/obj/machinery/duct/Initialize(mapload, no_anchor, color_of_duct = null, layer_of_duct = null, force_connects, force_ignore_colors)
. = ..()
- if(no_anchor)
- active = FALSE
- set_anchored(FALSE)
- else if(!can_anchor())
- qdel(src)
- CRASH("Overlapping ducts detected")
-
if(force_connects)
connects = force_connects //skip change_connects() because we're still initializing and we need to set our connects at one point
- if(!lock_layers)
+ if(!lock_layers && layer_of_duct)
duct_layer = layer_of_duct
- if(!ignore_colors)
+ if(force_ignore_colors)
+ ignore_colors = force_ignore_colors
+ if(!ignore_colors && color_of_duct)
duct_color = color_of_duct
if(duct_color)
add_atom_colour(duct_color, FIXED_COLOUR_PRIORITY)
- handle_layer()
+ if(no_anchor)
+ active = FALSE
+ set_anchored(FALSE)
+ else if(!can_anchor())
+ if(mapload)
+ log_mapping("Overlapping ducts detected at [AREACOORD(src)], unanchoring one.")
+ // Note that qdeling automatically drops a duct stack
+ return INITIALIZE_HINT_QDEL
- for(var/obj/machinery/duct/D in loc)
- if(D == src)
- continue
- if(D.duct_layer & duct_layer)
- return INITIALIZE_HINT_QDEL //If we have company, end it all
+ handle_layer()
attempt_connect()
AddElement(/datum/element/undertile, TRAIT_T_RAY_VISIBLE)
///start looking around us for stuff to connect to
/obj/machinery/duct/proc/attempt_connect()
-
- for(var/atom/movable/AM in loc)
- for(var/datum/component/plumbing/plumber as anything in AM.GetComponents(/datum/component/plumbing))
- if(plumber.active)
- disconnect_duct() //let's not built under plumbing machinery
- return
-
- for(var/D in GLOB.cardinals)
- if(dumb && !(D & connects))
+ for(var/direction in GLOB.cardinals)
+ if(dumb && !(direction & connects))
continue
- for(var/atom/movable/AM in get_step(src, D))
- if(connect_network(AM, D))
- add_connects(D)
+ for(var/atom/movable/duct_candidate in get_step(src, direction))
+ if(connect_network(duct_candidate, direction))
+ add_connects(direction)
update_appearance()
///see if whatever we found can be connected to
-/obj/machinery/duct/proc/connect_network(atom/movable/AM, direction, ignore_color)
- if(istype(AM, /obj/machinery/duct))
- return connect_duct(AM, direction, ignore_color)
+/obj/machinery/duct/proc/connect_network(atom/movable/plumbable, direction)
+ if(istype(plumbable, /obj/machinery/duct))
+ return connect_duct(plumbable, direction)
- for(var/datum/component/plumbing/plumber as anything in AM.GetComponents(/datum/component/plumbing))
+ for(var/datum/component/plumbing/plumber as anything in plumbable.GetComponents(/datum/component/plumbing))
. += connect_plumber(plumber, direction) //so that if one is true, all is true. beautiful.
///connect to a duct
-/obj/machinery/duct/proc/connect_duct(obj/machinery/duct/D, direction, ignore_color)
+/obj/machinery/duct/proc/connect_duct(obj/machinery/duct/other, direction)
var/opposite_dir = turn(direction, 180)
- if(!active || !D.active)
+ if(!active || !other.active)
return
- if(!dumb && D.dumb && !(opposite_dir & D.connects))
+ if(!dumb && other.dumb && !(opposite_dir & other.connects))
return
- if(dumb && D.dumb && !(connects & D.connects)) //we eliminated a few more scenarios in attempt connect
+ if(dumb && other.dumb && !(connects & other.connects)) //we eliminated a few more scenarios in attempt connect
return
- if((duct == D.duct) && duct)//check if we're not just comparing two null values
- add_neighbour(D, direction)
+ if((duct == other.duct) && duct)//check if we're not just comparing two null values
+ add_neighbour(other, direction)
- D.add_connects(opposite_dir)
- D.update_appearance()
+ other.add_connects(opposite_dir)
+ other.update_appearance()
return TRUE //tell the current pipe to also update it's sprite
- if(!(D in neighbours)) //we cool
- if((duct_color != D.duct_color) && !(ignore_colors || D.ignore_colors))
+ if(!(other in neighbours)) //we cool
+ if((duct_color != other.duct_color) && !(ignore_colors || other.ignore_colors))
return
- if(!(duct_layer & D.duct_layer))
+ if(!(duct_layer & other.duct_layer))
return
- if(D.duct)
+ if(other.duct)
if(duct)
- duct.assimilate(D.duct)
+ duct.assimilate(other.duct)
else
- D.duct.add_duct(src)
+ other.duct.add_duct(src)
else
if(duct)
- duct.add_duct(D)
+ duct.add_duct(other)
else
create_duct()
- duct.add_duct(D)
+ duct.add_duct(other)
- add_neighbour(D, direction)
+ add_neighbour(other, direction)
//Delegate to timer subsystem so its handled the next tick and doesnt cause byond to mistake it for an infinite loop and kill the game
- addtimer(CALLBACK(D, .proc/attempt_connect))
+ addtimer(CALLBACK(other, .proc/attempt_connect))
return TRUE
///connect to a plumbing object
-/obj/machinery/duct/proc/connect_plumber(datum/component/plumbing/P, direction)
+/obj/machinery/duct/proc/connect_plumber(datum/component/plumbing/plumbing, direction)
var/opposite_dir = turn(direction, 180)
- if(duct_layer != P.ducting_layer)
+ if(!(duct_layer & plumbing.ducting_layer))
return FALSE
- if(!P.active)
+ if(!plumbing.active)
return
- var/comp_directions = P.supply_connects + P.demand_connects //they should never, ever have supply and demand connects overlap or catastrophic failure
+ var/comp_directions = plumbing.supply_connects + plumbing.demand_connects //they should never, ever have supply and demand connects overlap or catastrophic failure
if(opposite_dir & comp_directions)
if(!duct)
create_duct()
- if(duct.add_plumber(P, opposite_dir))
- neighbours[P.parent] = direction
+ if(duct.add_plumber(plumbing, opposite_dir))
+ neighbours[plumbing.parent] = direction
return TRUE
///we disconnect ourself from our neighbours. we also destroy our ductnet and tell our neighbours to make a new one
@@ -164,9 +156,12 @@ All the important duct code:
reset_connects(0)
update_appearance()
if(ispath(drop_on_wrench))
- new drop_on_wrench(drop_location())
+ var/obj/item/stack/ducts/duct_stack = new drop_on_wrench(drop_location())
+ duct_stack.duct_color = GLOB.pipe_color_name[duct_color] || DUCT_COLOR_OMNI
+ duct_stack.duct_layer = GLOB.plumbing_layer_names["[duct_layer]"] || GLOB.plumbing_layer_names["[DUCT_LAYER_DEFAULT]"]
+ duct_stack.add_atom_colour(duct_color, FIXED_COLOUR_PRIORITY)
drop_on_wrench = null
- if(!QDELETED(src))
+ if(!QDELING(src))
qdel(src)
///Special proc to draw a new connect frame based on neighbours. not the norm so we can support multiple duct kinds
@@ -184,16 +179,17 @@ All the important duct code:
duct.add_duct(src)
///add a duct as neighbour. this means we're connected and will connect again if we ever regenerate
-/obj/machinery/duct/proc/add_neighbour(obj/machinery/duct/D, direction)
- if(!(D in neighbours))
- neighbours[D] = direction
- if(!(src in D.neighbours))
- D.neighbours[src] = turn(direction, 180)
+/obj/machinery/duct/proc/add_neighbour(obj/machinery/duct/other, direction)
+ if(!(other in neighbours))
+ neighbours[other] = direction
+ if(!(src in other.neighbours))
+ other.neighbours[src] = turn(direction, 180)
///remove all our neighbours, and remove us from our neighbours aswell
/obj/machinery/duct/proc/lose_neighbours()
- for(var/obj/machinery/duct/D in neighbours)
- D.neighbours.Remove(src)
+ for(var/obj/machinery/duct/other in neighbours)
+ other.neighbours.Remove(src)
+ other.generate_connects()
neighbours = list()
///add a connect direction
@@ -214,24 +210,24 @@ All the important duct code:
///get a list of the ducts we can connect to if we are dumb
/obj/machinery/duct/proc/get_adjacent_ducts()
var/list/adjacents = list()
- for(var/A in GLOB.cardinals)
- if(A & connects)
- for(var/obj/machinery/duct/D in get_step(src, A))
- if((turn(A, 180) & D.connects) && D.active)
- adjacents += D
+ for(var/direction in GLOB.cardinals)
+ if(direction & connects)
+ for(var/obj/machinery/duct/other in get_step(src, direction))
+ if((turn(direction, 180) & other.connects) && other.active)
+ adjacents += other
return adjacents
/obj/machinery/duct/update_icon_state()
var/temp_icon = initial(icon_state)
- for(var/D in GLOB.cardinals)
- if(D & connects)
- if(D == NORTH)
+ for(var/direction in GLOB.cardinals)
+ switch(direction & connects)
+ if(NORTH)
temp_icon += "_n"
- if(D == SOUTH)
+ if(SOUTH)
temp_icon += "_s"
- if(D == EAST)
+ if(EAST)
temp_icon += "_e"
- if(D == WEST)
+ if(WEST)
temp_icon += "_w"
icon_state = temp_icon
return ..()
@@ -239,7 +235,8 @@ All the important duct code:
///update the layer we are on
/obj/machinery/duct/proc/handle_layer()
var/offset
- switch(duct_layer)//it's a bitfield, but it's fine because it only works when there's one layer, and multiple layers should be handled differently
+ //it's a bitfield, but it's fine because ducts themselves are only on one layer
+ switch(duct_layer)
if(FIRST_DUCT_LAYER)
offset = -10
if(SECOND_DUCT_LAYER)
@@ -253,6 +250,7 @@ All the important duct code:
pixel_x = offset
pixel_y = offset
+ layer = initial(layer) + duct_layer * 0.0003
/obj/machinery/duct/set_anchored(anchorvalue)
. = ..()
@@ -264,10 +262,10 @@ All the important duct code:
else
disconnect_duct(TRUE)
-/obj/machinery/duct/wrench_act(mob/living/user, obj/item/I) //I can also be the RPD
+/obj/machinery/duct/wrench_act(mob/living/user, obj/item/wrench) //I can also be the RPD
..()
add_fingerprint(user)
- I.play_tool_sound(src)
+ wrench.play_tool_sound(src)
if(anchored || can_anchor())
set_anchored(!anchored)
user.visible_message( \
@@ -277,14 +275,15 @@ All the important duct code:
return TRUE
///collection of all the sanity checks to prevent us from stacking ducts that shouldn't be stacked
-/obj/machinery/duct/proc/can_anchor(turf/T)
- if(!T)
- T = get_turf(src)
- for(var/obj/machinery/duct/D in T)
- if(!anchored || D == src)
- continue
- for(var/A in GLOB.cardinals)
- if(A & connects && A & D.connects)
+/obj/machinery/duct/proc/can_anchor(turf/destination)
+ if(!destination)
+ destination = get_turf(src)
+ for(var/obj/machinery/duct/other in destination)
+ if(other.anchored && other != src && (duct_layer & other.duct_layer))
+ return FALSE
+ for(var/obj/machinery/machine in destination)
+ for(var/datum/component/plumbing/plumber as anything in machine.GetComponents(/datum/component/plumbing))
+ if(plumber.ducting_layer & duct_layer)
return FALSE
return TRUE
@@ -297,26 +296,29 @@ All the important duct code:
disconnect_duct()
return ..()
-/obj/machinery/duct/MouseDrop_T(atom/A, mob/living/user)
- if(!istype(A, /obj/machinery/duct))
- return
- var/obj/machinery/duct/D = A
- var/obj/item/I = user.get_active_held_item()
- if(I?.tool_behaviour != TOOL_WRENCH)
- to_chat(user, span_warning("You need to be holding a wrench in your active hand to do that!"))
+/obj/machinery/duct/MouseDrop_T(atom/drag_source, mob/living/user)
+ if(!istype(drag_source, /obj/machinery/duct))
return
- if(get_dist(src, D) != 1)
+ var/obj/machinery/duct/other = drag_source
+ if(get_dist(src, other) != 1)
return
- var/direction = get_dir(src, D)
+ var/direction = get_dir(src, other)
if(!(direction in GLOB.cardinals))
return
- if(duct_layer != D.duct_layer)
+ if(!(duct_layer & other.duct_layer))
+ to_chat(user, span_warning("The ducts must be on the same layer to connect them!"))
+ return
+ var/obj/item/held_item = user.get_active_held_item()
+ if(held_item?.tool_behaviour != TOOL_WRENCH)
+ to_chat(user, span_warning("You need to be holding a wrench in your active hand to do that!"))
return
add_connects(direction) //the connect of the other duct is handled in connect_network, but do this here for the parent duct because it's not necessary in normal cases
- add_neighbour(D, direction)
- connect_network(D, direction, TRUE)
+ add_neighbour(other, direction)
+ connect_network(other, direction)
update_appearance()
+ held_item.play_tool_sound(src)
+ to_chat(user, span_notice("You connect the two plumbing ducts."))
/obj/item/stack/ducts
name = "stack of duct"
@@ -330,22 +332,21 @@ All the important duct code:
max_amount = 50
item_flags = NOBLUDGEON
merge_type = /obj/item/stack/ducts
+ matter_amount = 1
///Color of our duct
var/duct_color = "omni"
///Default layer of our duct
var/duct_layer = "Default Layer"
- ///Assoc index with all the available layers. yes five might be a bit much. Colors uses a global by the way
- var/list/layers = list("Second Layer" = SECOND_DUCT_LAYER, "Default Layer" = DUCT_LAYER_DEFAULT, "Fourth Layer" = FOURTH_DUCT_LAYER)
/obj/item/stack/ducts/examine(mob/user)
. = ..()
. += span_notice("It's current color and layer are [duct_color] and [duct_layer]. Use in-hand to change.")
/obj/item/stack/ducts/attack_self(mob/user)
- var/new_layer = tgui_input_list(user, "Select a layer", "Layer", layers)
+ var/new_layer = tgui_input_list(user, "Select a layer", "Layer", GLOB.plumbing_layers, duct_layer)
if(new_layer)
duct_layer = new_layer
- var/new_color = tgui_input_list(user, "Select a color", "Color", GLOB.pipe_paint_colors)
+ var/new_color = tgui_input_list(user, "Select a color", "Color", GLOB.pipe_paint_colors, duct_color)
if(new_color)
duct_color = new_color
add_atom_colour(GLOB.pipe_paint_colors[new_color], FIXED_COLOUR_PRIORITY)
@@ -355,16 +356,25 @@ All the important duct code:
if(!proximity)
return
if(istype(target, /obj/machinery/duct))
- var/obj/machinery/duct/D = target
- if(!D.anchored)
- add(1)
- qdel(D)
+ var/obj/machinery/duct/duct = target
+ if(duct.anchored)
+ to_chat(user, span_warning("The duct must be unanchored before it can be picked up."))
+ return
+
+ // Turn into a duct stack and then merge to the in-hand stack.
+ var/obj/item/stack/ducts/stack = new(duct.loc, 1, FALSE)
+ qdel(duct)
+ if(stack.can_merge(src))
+ stack.merge(src)
+ return
+
check_attach_turf(target)
/obj/item/stack/ducts/proc/check_attach_turf(atom/target)
- if(istype(target, /turf/open) && use(1))
+ if(isopenturf(target) && use(1))
var/turf/open/open_turf = target
- new /obj/machinery/duct(open_turf, FALSE, GLOB.pipe_paint_colors[duct_color], layers[duct_layer])
+ var/is_omni = duct_color == DUCT_COLOR_OMNI
+ new /obj/machinery/duct(open_turf, FALSE, GLOB.pipe_paint_colors[duct_color], GLOB.plumbing_layers[duct_layer], null, is_omni)
playsound(get_turf(src), 'sound/machines/click.ogg', 50, TRUE)
/obj/item/stack/ducts/fifty
diff --git a/code/modules/plumbing/plumbers/_plumb_machinery.dm b/code/modules/plumbing/plumbers/_plumb_machinery.dm
index f9930bf5f6bce..54cea5f5ca2bb 100644
--- a/code/modules/plumbing/plumbers/_plumb_machinery.dm
+++ b/code/modules/plumbing/plumbers/_plumb_machinery.dm
@@ -14,7 +14,6 @@
var/buffer = 50
///Flags for reagents, like INJECTABLE, TRANSPARENT bla bla everything thats in DEFINES/reagents.dm
var/reagent_flags = TRANSPARENT
- ///wheter we partake in rcd construction or not
/obj/machinery/plumbing/Initialize(mapload, bolt = TRUE)
. = ..()
@@ -98,6 +97,8 @@
/obj/machinery/plumbing/layer_manifold/Initialize(mapload, bolt, layer)
. = ..()
+ AddComponent(/datum/component/plumbing/manifold, bolt, FIRST_DUCT_LAYER)
AddComponent(/datum/component/plumbing/manifold, bolt, SECOND_DUCT_LAYER)
AddComponent(/datum/component/plumbing/manifold, bolt, THIRD_DUCT_LAYER)
AddComponent(/datum/component/plumbing/manifold, bolt, FOURTH_DUCT_LAYER)
+ AddComponent(/datum/component/plumbing/manifold, bolt, FIFTH_DUCT_LAYER)
diff --git a/code/modules/plumbing/plumbers/pill_press.dm b/code/modules/plumbing/plumbers/pill_press.dm
index 94028b9f17712..409ceb5824784 100644
--- a/code/modules/plumbing/plumbers/pill_press.dm
+++ b/code/modules/plumbing/plumbers/pill_press.dm
@@ -136,7 +136,11 @@
if("change_current_volume")
current_volume = clamp(text2num(params["volume"]), min_volume, max_volume)
if("change_product_name")
- product_name = html_encode(params["name"])
+ var/formatted_name = html_encode(params["name"])
+ if (length(formatted_name) > MAX_NAME_LEN)
+ product_name = copytext(formatted_name, 1, MAX_NAME_LEN+1)
+ else
+ product_name = formatted_name
if("change_product")
product = params["product"]
if (product == "pill")
diff --git a/code/modules/power/apc/apc_attack.dm b/code/modules/power/apc/apc_attack.dm
index 7bfe508481946..36605c3f8b144 100644
--- a/code/modules/power/apc/apc_attack.dm
+++ b/code/modules/power/apc/apc_attack.dm
@@ -24,16 +24,16 @@
if(istype(attacking_object, /obj/item/stock_parts/cell) && opened)
if(cell)
- to_chat(user, span_warning("There is a power cell already installed!"))
+ balloon_alert(user, "cell already installed!")
return
if(machine_stat & MAINT)
- to_chat(user, span_warning("There is no connector for your power cell!"))
+ balloon_alert(user, "no connector for a cell!")
return
if(!user.transferItemToLoc(attacking_object, src))
return
cell = attacking_object
- user.visible_message(span_notice("[user.name] inserts the power cell to [src.name]!"),\
- span_notice("You insert the power cell."))
+ user.visible_message(span_notice("[user.name] inserts the power cell to [src.name]!"))
+ balloon_alert(user, "cell inserted")
chargecount = 0
update_appearance()
return
@@ -47,22 +47,22 @@
if(!host_turf)
CRASH("attackby on APC when it's not on a turf")
if(host_turf.underfloor_accessibility < UNDERFLOOR_INTERACTABLE)
- to_chat(user, span_warning("You must remove the floor plating in front of the APC first!"))
+ balloon_alert(user, "remove the floor plating!")
return
if(terminal)
- to_chat(user, span_warning("This APC is already wired!"))
+ balloon_alert(user, "APC is already wired!")
return
if(!has_electronics)
- to_chat(user, span_warning("There is nothing to wire!"))
+ balloon_alert(user, "no board to wire!")
return
var/obj/item/stack/cable_coil/installing_cable = attacking_object
if(installing_cable.get_amount() < 10)
- to_chat(user, span_warning("You need ten lengths of cable for APC!"))
+ balloon_alert(user, "need ten lengths of cable!")
return
- user.visible_message(span_notice("[user.name] adds cables to the APC frame."), \
- span_notice("You start adding cables to the APC frame..."))
+ user.visible_message(span_notice("[user.name] adds cables to the APC frame."))
+ balloon_alert(user, "adding cables to the frame...")
playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
if(!do_after(user, 20, target = src))
return
@@ -76,22 +76,22 @@
do_sparks(5, TRUE, src)
return
installing_cable.use(10)
- to_chat(user, span_notice("You add cables to the APC frame."))
+ balloon_alert(user, "cables added to the frame")
make_terminal()
terminal.connect_to_network()
return
if(istype(attacking_object, /obj/item/electronics/apc) && opened)
if(has_electronics)
- to_chat(user, span_warning("There is already a board inside the [src]!"))
+ balloon_alert(user, "there is already a board!")
return
if(machine_stat & BROKEN)
- to_chat(user, span_warning("You cannot put the board inside, the frame is damaged!"))
+ balloon_alert(user, "the frame is damaged!")
return
- user.visible_message(span_notice("[user.name] inserts the power control board into [src]."), \
- span_notice("You start to insert the power control board into the frame..."))
+ user.visible_message(span_notice("[user.name] inserts the power control board into [src]."))
+ balloon_alert(user, "you start to insert the board...")
playsound(loc, 'sound/items/deconstruct.ogg', 50, TRUE)
if(!do_after(user, 10, target = src) || has_electronics)
@@ -99,7 +99,7 @@
has_electronics = APC_ELECTRONICS_INSTALLED
locked = FALSE
- to_chat(user, span_notice("You place the power control board inside the frame."))
+ balloon_alert(user, "board installed")
qdel(attacking_object)
return
@@ -107,7 +107,7 @@
var/obj/item/electroadaptive_pseudocircuit/pseudocircuit = attacking_object
if(!has_electronics)
if(machine_stat & BROKEN)
- to_chat(user, span_warning("[src]'s frame is too damaged to support a circuit."))
+ balloon_alert(user, "frame is too damaged!")
return
if(!pseudocircuit.adapt_circuit(user, 50))
return
@@ -119,7 +119,7 @@
if(!cell)
if(machine_stat & MAINT)
- to_chat(user, span_warning("There's no connector for a power cell."))
+ balloon_alert(user, "no board for a cell!")
return
if(!pseudocircuit.adapt_circuit(user, 500))
return
@@ -132,29 +132,29 @@
update_appearance()
return
- to_chat(user, span_warning("[src] has both electronics and a cell."))
+ balloon_alert(user, "has both board and cell!")
return
if(istype(attacking_object, /obj/item/wallframe/apc) && opened)
if(!(machine_stat & BROKEN || opened==APC_COVER_REMOVED || atom_integrity < max_integrity)) // There is nothing to repair
- to_chat(user, span_warning("You found no reason for repairing this APC!"))
+ balloon_alert(user, "no reason for repairs!")
return
if(!(machine_stat & BROKEN) && opened==APC_COVER_REMOVED) // Cover is the only thing broken, we do not need to remove elctronicks to replace cover
- user.visible_message(span_notice("[user.name] replaces missing APC's cover."), \
- span_notice("You begin to replace APC's cover..."))
+ user.visible_message(span_notice("[user.name] replaces missing APC's cover."))
+ balloon_alert(user, "replacing APC's cover...")
if(do_after(user, 20, target = src)) // replacing cover is quicker than replacing whole frame
- to_chat(user, span_notice("You replace missing APC's cover."))
+ balloon_alert(user, "cover replaced")
qdel(attacking_object)
opened = APC_COVER_OPENED
update_appearance()
return
if(has_electronics)
- to_chat(user, span_warning("You cannot repair this APC until you remove the electronics still inside!"))
+ balloon_alert(user, "remove the board inside!")
return
- user.visible_message(span_notice("[user.name] replaces the damaged APC frame with a new one."), \
- span_notice("You begin to replace the damaged APC frame..."))
+ user.visible_message(span_notice("[user.name] replaces the damaged APC frame with a new one."))
+ balloon_alert(user, "replacing damaged frame...")
if(do_after(user, 50, target = src))
- to_chat(user, span_notice("You replace the damaged APC frame with a new one."))
+ balloon_alert(user, "APC frame replaced")
qdel(attacking_object)
set_machine_stat(machine_stat & ~BROKEN)
atom_integrity = max_integrity
@@ -201,40 +201,40 @@
return
if(ethereal.combat_mode)
if(cell.charge <= (cell.maxcharge / 2)) // ethereals can't drain APCs under half charge, this is so that they are forced to look to alternative power sources if the station is running low
- to_chat(ethereal, span_warning("The APC's syphon safeties prevent you from draining power!"))
+ balloon_alert(ethereal, "safeties prevent draining!")
return
if(stomach.crystal_charge > charge_limit)
- to_chat(ethereal, span_warning("Your charge is full!"))
+ balloon_alert(ethereal, "charge is full!")
return
stomach.drain_time = world.time + APC_DRAIN_TIME
- to_chat(ethereal, span_notice("You start channeling some power through the APC into your body."))
+ balloon_alert(ethereal, "draining power")
if(do_after(user, APC_DRAIN_TIME, target = src))
if(cell.charge <= (cell.maxcharge / 2) || (stomach.crystal_charge > charge_limit))
return
- to_chat(ethereal, span_notice("You receive some charge from the APC."))
+ balloon_alert(ethereal, "received charge")
stomach.adjust_charge(APC_POWER_GAIN)
cell.use(APC_POWER_GAIN)
return
if(cell.charge >= cell.maxcharge - APC_POWER_GAIN)
- to_chat(ethereal, span_warning("The APC can't receive anymore power!"))
+ balloon_alert(ethereal, "APC can't receive more power!")
return
if(stomach.crystal_charge < APC_POWER_GAIN)
- to_chat(ethereal, span_warning("Your charge is too low!"))
+ balloon_alert(ethereal, "charge is too low!")
return
stomach.drain_time = world.time + APC_DRAIN_TIME
- to_chat(ethereal, span_notice("You start channeling power through your body into the APC."))
+ balloon_alert(ethereal, "transfering power")
if(!do_after(user, APC_DRAIN_TIME, target = src))
return
if((cell.charge >= (cell.maxcharge - APC_POWER_GAIN)) || (stomach.crystal_charge < APC_POWER_GAIN))
- to_chat(ethereal, span_warning("You can't transfer power to the APC!"))
+ balloon_alert(ethereal, "can't transfer power!")
return
if(istype(stomach))
- to_chat(ethereal, span_notice("You transfer some power to the APC."))
+ balloon_alert(ethereal, "transfered power")
stomach.adjust_charge(-APC_POWER_GAIN)
cell.give(APC_POWER_GAIN)
else
- to_chat(ethereal, span_warning("You can't transfer power to the APC!"))
+ balloon_alert(ethereal, "can't transfer power!")
// attack with hand - remove cell (if cover open) or interact with the APC
/obj/machinery/power/apc/attack_hand(mob/user, list/modifiers)
@@ -244,7 +244,8 @@
if(opened && (!issilicon(user)))
if(cell)
- user.visible_message(span_notice("[user] removes \the [cell] from [src]!"), span_notice("You remove \the [cell]."))
+ user.visible_message(span_notice("[user] removes \the [cell] from [src]!"))
+ balloon_alert(user, "cell removed")
user.put_in_hands(cell)
cell.update_appearance()
cell = null
@@ -284,7 +285,7 @@
var/mob/living/silicon/robot/robot = user
if(aidisabled || malfhack && istype(malfai) && ((istype(AI) && (malfai!=AI && malfai != AI.parent)) || (istype(robot) && (robot in malfai.connected_robots))))
if(!loud)
- to_chat(user, span_danger("\The [src] has been disabled!"))
+ balloon_alert(user, "APC has been disabled!")
return FALSE
return TRUE
diff --git a/code/modules/power/apc/apc_main.dm b/code/modules/power/apc/apc_main.dm
index 08d4358da6a47..58c5db2e7bec4 100644
--- a/code/modules/power/apc/apc_main.dm
+++ b/code/modules/power/apc/apc_main.dm
@@ -370,8 +370,8 @@
if("emergency_lighting")
emergency_lights = !emergency_lights
for(var/obj/machinery/light/L in area)
- if(!initial(L.no_emergency)) //If there was an override set on creation, keep that override
- L.no_emergency = emergency_lights
+ if(!initial(L.no_low_power)) //If there was an override set on creation, keep that override
+ L.no_low_power = emergency_lights
INVOKE_ASYNC(L, /obj/machinery/light/.proc/update, FALSE)
CHECK_TICK
return TRUE
diff --git a/code/modules/power/apc/apc_power_proc.dm b/code/modules/power/apc/apc_power_proc.dm
index 7e69cce9b33c8..b346c6ac0d833 100644
--- a/code/modules/power/apc/apc_power_proc.dm
+++ b/code/modules/power/apc/apc_power_proc.dm
@@ -16,7 +16,7 @@
/obj/machinery/power/apc/proc/toggle_nightshift_lights(mob/living/user)
if(last_nightshift_switch > world.time - 100) //~10 seconds between each toggle to prevent spamming
- to_chat(usr, span_warning("[src]'s night lighting circuit breaker is still cycling!"))
+ balloon_alert(user, "night breaker is cycling!")
return
last_nightshift_switch = world.time
set_nightshift(!nightshift_lights)
diff --git a/code/modules/power/apc/apc_tool_act.dm b/code/modules/power/apc/apc_tool_act.dm
index 2750cd9e4c240..e1e71fff6a3a7 100644
--- a/code/modules/power/apc/apc_tool_act.dm
+++ b/code/modules/power/apc/apc_tool_act.dm
@@ -3,10 +3,10 @@
. = TRUE
if((!opened && opened != APC_COVER_REMOVED) && !(machine_stat & BROKEN))
if(coverlocked && !(machine_stat & MAINT)) // locked...
- to_chat(user, span_warning("The cover is locked and cannot be opened!"))
+ balloon_alert(user, "cover is locked!")
return
else if(panel_open)
- to_chat(user, span_warning("Exposed wires prevents you from opening it!"))
+ balloon_alert(user, "wires prevents opening it!")
return
else
opened = APC_COVER_OPENED
@@ -16,39 +16,40 @@
if((opened && has_electronics == APC_ELECTRONICS_SECURED) && !(machine_stat & BROKEN))
opened = APC_COVER_CLOSED
coverlocked = TRUE //closing cover relocks it
+ balloon_alert(user, "locking the cover")
update_appearance()
return
if(!opened || has_electronics != APC_ELECTRONICS_INSTALLED)
return
if(terminal)
- to_chat(user, span_warning("Disconnect the wires first!"))
+ balloon_alert(user, "disconnect the wires first!")
return
crowbar.play_tool_sound(src)
- to_chat(user, span_notice("You attempt to remove the power control board...") )
+ balloon_alert(user, "removing the board")
if(!crowbar.use_tool(src, user, 50))
return
if(has_electronics != APC_ELECTRONICS_INSTALLED)
return
has_electronics = APC_ELECTRONICS_MISSING
if(machine_stat & BROKEN)
- user.visible_message(span_notice("[user.name] breaks the power control board inside [name]!"),\
- span_notice("You break the charred power control board and remove the remains."),
+ user.visible_message(span_notice("[user.name] breaks the power control board inside [name]!"), \
span_hear("You hear a crack."))
+ balloon_alert(user, "charred board breaks")
return
else if(obj_flags & EMAGGED)
obj_flags &= ~EMAGGED
- user.visible_message(span_notice("[user.name] discards an emagged power control board from [name]!"),\
- span_notice("You discard the emagged power control board."))
+ user.visible_message(span_notice("[user.name] discards an emagged power control board from [name]!"))
+ balloon_alert(user, "emagged board discarded")
return
else if(malfhack)
- user.visible_message(span_notice("[user.name] discards a strangely programmed power control board from [name]!"),\
- span_notice("You discard the strangely programmed board."))
+ user.visible_message(span_notice("[user.name] discards a strangely programmed power control board from [name]!"))
+ balloon_alert(user, "reprogrammed board discarded")
malfai = null
malfhack = 0
return
- user.visible_message(span_notice("[user.name] removes the power control board from [name]!"),\
- span_notice("You remove the power control board."))
+ user.visible_message(span_notice("[user.name] removes the power control board from [name]!"))
+ balloon_alert(user, "removed the board")
new /obj/item/electronics/apc(loc)
return
@@ -59,15 +60,16 @@
if(!opened)
if(obj_flags & EMAGGED)
- to_chat(user, span_warning("The interface is broken!"))
+ balloon_alert(user, "the interface is broken!")
return
panel_open = !panel_open
- to_chat(user, span_notice("The wires have been [panel_open ? "exposed" : "unexposed"]."))
+ balloon_alert(user, "wires are [panel_open ? "exposed" : "unexposed"]")
update_appearance()
return
if(cell)
- user.visible_message(span_notice("[user] removes \the [cell] from [src]!"), span_notice("You remove \the [cell]."))
+ user.visible_message(span_notice("[user] removes \the [cell] from [src]!"))
+ balloon_alert(user, "cell removed")
var/turf/user_turf = get_turf(user)
cell.forceMove(user_turf)
cell.update_appearance()
@@ -81,14 +83,14 @@
has_electronics = APC_ELECTRONICS_SECURED
set_machine_stat(machine_stat & ~MAINT)
W.play_tool_sound(src)
- to_chat(user, span_notice("You screw the circuit electronics into place."))
+ balloon_alert(user, "board fastened")
if(APC_ELECTRONICS_SECURED)
has_electronics = APC_ELECTRONICS_INSTALLED
set_machine_stat(machine_stat | MAINT)
W.play_tool_sound(src)
- to_chat(user, span_notice("You unfasten the electronics."))
+ balloon_alert(user, "board unfastened")
else
- to_chat(user, span_warning("There is nothing to secure!"))
+ balloon_alert(user, "no board to faster!")
return
update_appearance()
@@ -105,18 +107,18 @@
if(!welder.tool_start_check(user, amount=3))
return
user.visible_message(span_notice("[user.name] welds [src]."), \
- span_notice("You start welding the APC frame..."), \
span_hear("You hear welding."))
+ balloon_alert(user, "welding the APC frame")
if(!welder.use_tool(src, user, 50, volume=50, amount=3))
return
if((machine_stat & BROKEN) || opened==APC_COVER_REMOVED)
new /obj/item/stack/sheet/iron(loc)
- user.visible_message(span_notice("[user.name] cuts [src] apart with [welder]."),\
- span_notice("You disassembled the broken APC frame."))
+ user.visible_message(span_notice("[user.name] cuts [src] apart with [welder]."))
+ balloon_alert(user, "disassembled the broken frame")
else
new /obj/item/wallframe/apc(loc)
- user.visible_message(span_notice("[user.name] cuts [src] from the wall with [welder]."),\
- span_notice("You cut the APC frame from the wall."))
+ user.visible_message(span_notice("[user.name] cuts [src] from the wall with [welder]."))
+ balloon_alert(user, "cut the frame from the wall")
qdel(src)
return TRUE
@@ -126,17 +128,17 @@
if(!has_electronics)
if(machine_stat & BROKEN)
- to_chat(user, span_warning("[src]'s frame is too damaged to support a circuit."))
+ balloon_alert(user, "frame is too damaged!")
return FALSE
return list("mode" = RCD_UPGRADE_SIMPLE_CIRCUITS, "delay" = 20, "cost" = 1)
if(!cell)
if(machine_stat & MAINT)
- to_chat(user, span_warning("There's no connector for a power cell."))
+ balloon_alert(user, "no board for a cell!")
return FALSE
return list("mode" = RCD_UPGRADE_SIMPLE_CIRCUITS, "delay" = 50, "cost" = 10) //16 for a wall
- to_chat(user, span_warning("[src] has both electronics and a cell."))
+ balloon_alert(user, "has both board and cell!")
return FALSE
/obj/machinery/power/apc/rcd_act(mob/user, obj/item/construction/rcd/the_rcd, passed_mode)
@@ -144,17 +146,17 @@
return FALSE
if(!has_electronics)
if(machine_stat & BROKEN)
- to_chat(user, span_warning("[src]'s frame is too damaged to support a circuit."))
+ balloon_alert(user, "frame is too damaged!")
return
- user.visible_message(span_notice("[user] fabricates a circuit and places it into [src]."), \
- span_notice("You adapt a power control board and click it into place in [src]'s guts."))
+ user.visible_message(span_notice("[user] fabricates a circuit and places it into [src]."))
+ balloon_alert(user, "control board placed")
has_electronics = TRUE
locked = TRUE
return TRUE
if(!cell)
if(machine_stat & MAINT)
- to_chat(user, span_warning("There's no connector for a power cell."))
+ balloon_alert(user, "no board for a cell!")
return FALSE
var/obj/item/stock_parts/cell/crap/empty/C = new(src)
C.forceMove(src)
@@ -165,7 +167,7 @@
update_appearance()
return TRUE
- to_chat(user, span_warning("[src] has both electronics and a cell."))
+ balloon_alert(user, "has both board and cell!")
return FALSE
/obj/machinery/power/apc/emag_act(mob/user)
@@ -173,17 +175,17 @@
return
if(opened)
- to_chat(user, span_warning("You must close the cover to swipe an ID card!"))
+ balloon_alert(user, "must close the cover to swipe!")
else if(panel_open)
- to_chat(user, span_warning("You must close the panel first!"))
+ balloon_alert(user, "must close the panel first!")
else if(machine_stat & (BROKEN|MAINT))
- to_chat(user, span_warning("Nothing happens!"))
+ balloon_alert(user, "nothing happens!")
else
flick("apc-spark", src)
playsound(src, SFX_SPARKS, 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
obj_flags |= EMAGGED
locked = FALSE
- to_chat(user, span_notice("You emag the APC interface."))
+ balloon_alert(user, "you emag the APC")
update_appearance()
// damage and destruction acts
@@ -205,19 +207,19 @@
/obj/machinery/power/apc/proc/togglelock(mob/living/user)
if(obj_flags & EMAGGED)
- to_chat(user, span_warning("The interface is broken!"))
+ balloon_alert(user, "interface is broken!")
else if(opened)
- to_chat(user, span_warning("You must close the cover to swipe an ID card!"))
+ balloon_alert(user, "must close the cover to swipe!")
else if(panel_open)
- to_chat(user, span_warning("You must close the panel!"))
+ balloon_alert(user, "must close the panel!")
else if(machine_stat & (BROKEN|MAINT))
- to_chat(user, span_warning("Nothing happens!"))
+ balloon_alert(user, "nothing happens!")
else
if(allowed(usr) && !wires.is_cut(WIRE_IDSCAN) && !malfhack)
locked = !locked
- to_chat(user, span_notice("You [ locked ? "lock" : "unlock"] the APC interface."))
+ balloon_alert(user, "APC [ locked ? "locked" : "unlocked"]")
update_appearance()
if(!locked)
ui_interact(user)
else
- to_chat(user, span_warning("Access denied."))
+ balloon_alert(user, "access denied!")
diff --git a/code/modules/power/cell.dm b/code/modules/power/cell.dm
index d68aca06d6cc6..3841b47bd1dd3 100644
--- a/code/modules/power/cell.dm
+++ b/code/modules/power/cell.dm
@@ -71,7 +71,7 @@
*
* If we, or the item we're located in, is subject to the charge spell, gain some charge back
*/
-/obj/item/stock_parts/cell/proc/on_magic_charge(datum/source, obj/effect/proc_holder/spell/targeted/charge/spell, mob/living/caster)
+/obj/item/stock_parts/cell/proc/on_magic_charge(datum/source, datum/action/cooldown/spell/charge/spell, mob/living/caster)
SIGNAL_HANDLER
// This shouldn't be running if we're not being held by a mob,
diff --git a/code/modules/power/gravitygenerator.dm b/code/modules/power/gravitygenerator.dm
index 5f4e0fd813ff4..249b093eca782 100644
--- a/code/modules/power/gravitygenerator.dm
+++ b/code/modules/power/gravitygenerator.dm
@@ -3,7 +3,8 @@
// Gravity Generator
//
-GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding new gravity generators to the list, and keying it with the z level.
+/// We will keep track of this by adding new gravity generators to the list, and keying it with the z level.
+GLOBAL_LIST_EMPTY(gravity_generators)
#define POWER_IDLE 0
#define POWER_UP 1
@@ -27,22 +28,8 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
use_power = NO_POWER_USE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
- var/datum/proximity_monitor/advanced/gravity/gravity_field
-
var/sprite_number = 0
- ///Audio for when the gravgen is on
- var/datum/looping_sound/gravgen/soundloop
-
-/obj/machinery/gravity_generator/Initialize(mapload)
- . = ..()
- soundloop = new(src, TRUE)
-
-/obj/machinery/gravity_generator/Destroy()
- QDEL_NULL(gravity_field)
- QDEL_NULL(soundloop)
- return ..()
-
/obj/machinery/gravity_generator/safe_throw_at(atom/target, range, speed, mob/thrower, spin = TRUE, diagonals_first = FALSE, datum/callback/callback, force = MOVE_FORCE_STRONG, gentle = FALSE)
return FALSE
@@ -77,59 +64,52 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
/obj/machinery/gravity_generator/proc/set_fix()
set_machine_stat(machine_stat & ~BROKEN)
+/**
+ * Generator part
+ *
+ * Parts of the gravity generator used to have a proper sprite.
+ */
+/obj/machinery/gravity_generator/part
+ var/obj/machinery/gravity_generator/main/main_part
+
/obj/machinery/gravity_generator/part/Destroy()
+ atom_break()
if(main_part)
- qdel(main_part)
- set_broken()
- QDEL_NULL(soundloop)
+ UnregisterSignal(main_part, COMSIG_ATOM_UPDATED_ICON)
+ main_part = null
return ..()
-//
-// Part generator which is mostly there for looks
-//
-
-/obj/machinery/gravity_generator/part
- var/obj/machinery/gravity_generator/main/main_part = null
-
-/obj/machinery/gravity_generator/part/attackby(obj/item/I, mob/user, params)
- return main_part.attackby(I, user)
+/obj/machinery/gravity_generator/part/attackby(obj/item/weapon, mob/user, params)
+ if(!main_part)
+ return
+ return main_part.attackby(weapon, user)
/obj/machinery/gravity_generator/part/get_status()
- return main_part?.get_status()
+ if(!main_part)
+ return
+ return main_part.get_status()
/obj/machinery/gravity_generator/part/attack_hand(mob/user, list/modifiers)
+ if(!main_part)
+ return
return main_part.attack_hand(user, modifiers)
/obj/machinery/gravity_generator/part/set_broken()
..()
- if(main_part && !(main_part.machine_stat & BROKEN))
- main_part.set_broken()
+ if(!main_part || (main_part.machine_stat & BROKEN))
+ return
+ main_part.set_broken()
/// Used to eat args
/obj/machinery/gravity_generator/part/proc/on_update_icon(obj/machinery/gravity_generator/source, updates, updated)
SIGNAL_HANDLER
return update_appearance(updates)
-//
-// Generator which spawns with the station.
-//
-
-/obj/machinery/gravity_generator/main/station/Initialize(mapload)
- . = ..()
- setup_parts()
- middle.add_overlay("activated")
- update_list()
-
-//
-// Generator an admin can spawn
-//
-/obj/machinery/gravity_generator/main/station/admin
- use_power = NO_POWER_USE
-
-//
-// Main Generator with the main code
-//
-
+/**
+ * Main gravity generator
+ *
+ * The actual gravity generator, that actually holds the UI, contains the grav gen parts, ect.
+ */
/obj/machinery/gravity_generator/main
icon_state = "on_8"
idle_power_usage = 0
@@ -138,30 +118,53 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
sprite_number = 8
use_power = IDLE_POWER_USE
interaction_flags_machine = INTERACT_MACHINE_ALLOW_SILICON | INTERACT_MACHINE_OFFLINE
+
+ /// List of all gravity generator parts
+ var/list/generator_parts = list()
+ /// The gravity generator part in the very center, the fifth one, where we place the overlays.
+ var/obj/machinery/gravity_generator/part/center_part
+
+ /// Whether the gravity generator is currently active.
var/on = TRUE
+ /// If the main breaker is on/off, to enable/disable gravity.
var/breaker = TRUE
- var/list/parts = list()
- var/obj/middle = null
+ /// If the generatir os idle, charging, or down.
var/charging_state = POWER_IDLE
+ /// How much charge the gravity generator has, goes down when breaker is shut, and shuts down at 0.
var/charge_count = 100
+
+ /// The gravity overlay currently used.
var/current_overlay = null
- var/broken_state = 0
- var/setting = 1 //Gravity value when on
+ /// When broken, what stage it is at (GRAV_NEEDS_SCREWDRIVER:0) (GRAV_NEEDS_WELDING:1) (GRAV_NEEDS_PLASTEEL:2) (GRAV_NEEDS_WRENCH:3)
+ var/broken_state = GRAV_NEEDS_SCREWDRIVER
+ /// Gravity value when on, honestly I don't know why it does it like this, but it does.
+ var/setting = 1
+
+ /// The gravity field created by the generator.
+ var/datum/proximity_monitor/advanced/gravity/gravity_field
+ /// Audio for when the gravgen is on
+ var/datum/looping_sound/gravgen/soundloop
///Station generator that spawns with gravity turned off.
-/obj/machinery/gravity_generator/main/station/off
+/obj/machinery/gravity_generator/main/off
on = FALSE
breaker = FALSE
charge_count = 0
+/obj/machinery/gravity_generator/main/Initialize(mapload)
+ . = ..()
+ soundloop = new(src, start_immediately = FALSE)
+ setup_parts()
+ if(on)
+ enable()
+ center_part.add_overlay("activated")
+
/obj/machinery/gravity_generator/main/Destroy() // If we somehow get deleted, remove all of our other parts.
investigate_log("was destroyed!", INVESTIGATE_GRAVITY)
- on = FALSE
- update_list()
- for(var/obj/machinery/gravity_generator/part/O in parts)
- O.main_part = null
- if(!QDESTROYING(O))
- qdel(O)
+ disable()
+ QDEL_NULL(soundloop)
+ QDEL_NULL(center_part)
+ QDEL_LIST(generator_parts)
return ..()
/obj/machinery/gravity_generator/main/proc/setup_parts()
@@ -175,26 +178,23 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
continue
var/obj/machinery/gravity_generator/part/part = new(T)
if(count == 5) // Middle
- middle = part
+ center_part = part
if(count <= 3) // Their sprite is the top part of the generator
part.set_density(FALSE)
part.layer = WALL_OBJ_LAYER
part.plane = GAME_PLANE_UPPER
part.sprite_number = count
part.main_part = src
- parts += part
+ generator_parts += part
part.update_appearance()
part.RegisterSignal(src, COMSIG_ATOM_UPDATED_ICON, /obj/machinery/gravity_generator/part/proc/on_update_icon)
-/obj/machinery/gravity_generator/main/proc/connected_parts()
- return parts.len == 8
-
/obj/machinery/gravity_generator/main/set_broken()
- ..()
- for(var/obj/machinery/gravity_generator/M in parts)
- if(!(M.machine_stat & BROKEN))
- M.set_broken()
- middle.cut_overlays()
+ . = ..()
+ for(var/obj/machinery/gravity_generator/internal_parts as anything in generator_parts)
+ if(!(internal_parts.machine_stat & BROKEN))
+ internal_parts.set_broken()
+ center_part.cut_overlays()
charge_count = 0
breaker = FALSE
set_power()
@@ -202,10 +202,10 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
investigate_log("has broken down.", INVESTIGATE_GRAVITY)
/obj/machinery/gravity_generator/main/set_fix()
- ..()
- for(var/obj/machinery/gravity_generator/M in parts)
- if(M.machine_stat & BROKEN)
- M.set_fix()
+ . = ..()
+ for(var/obj/machinery/gravity_generator/internal_parts as anything in generator_parts)
+ if(internal_parts.machine_stat & BROKEN)
+ internal_parts.set_fix()
broken_state = FALSE
update_appearance()
set_power()
@@ -213,26 +213,26 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
// Interaction
// Fixing the gravity generator.
-/obj/machinery/gravity_generator/main/attackby(obj/item/I, mob/user, params)
+/obj/machinery/gravity_generator/main/attackby(obj/item/weapon, mob/user, params)
if(machine_stat & BROKEN)
switch(broken_state)
if(GRAV_NEEDS_SCREWDRIVER)
- if(I.tool_behaviour == TOOL_SCREWDRIVER)
+ if(weapon.tool_behaviour == TOOL_SCREWDRIVER)
to_chat(user, span_notice("You secure the screws of the framework."))
- I.play_tool_sound(src)
+ weapon.play_tool_sound(src)
broken_state++
update_appearance()
return
if(GRAV_NEEDS_WELDING)
- if(I.tool_behaviour == TOOL_WELDER)
- if(I.use_tool(src, user, 0, volume=50, amount=1))
+ if(weapon.tool_behaviour == TOOL_WELDER)
+ if(weapon.use_tool(src, user, 0, volume=50, amount=1))
to_chat(user, span_notice("You mend the damaged framework."))
broken_state++
update_appearance()
return
if(GRAV_NEEDS_PLASTEEL)
- if(istype(I, /obj/item/stack/sheet/plasteel))
- var/obj/item/stack/sheet/plasteel/PS = I
+ if(istype(weapon, /obj/item/stack/sheet/plasteel))
+ var/obj/item/stack/sheet/plasteel/PS = weapon
if(PS.get_amount() >= 10)
PS.use(10)
to_chat(user, span_notice("You add the plating to the framework."))
@@ -243,9 +243,9 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
to_chat(user, span_warning("You need 10 sheets of plasteel!"))
return
if(GRAV_NEEDS_WRENCH)
- if(I.tool_behaviour == TOOL_WRENCH)
+ if(weapon.tool_behaviour == TOOL_WRENCH)
to_chat(user, span_notice("You secure the plating to the framework."))
- I.play_tool_sound(src)
+ weapon.play_tool_sound(src)
set_fix()
return
return ..()
@@ -308,9 +308,6 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
on = TRUE
update_use_power(ACTIVE_POWER_USE)
- if (!SSticker.IsRoundInProgress())
- return
-
soundloop.start()
if (!gravity_in_level())
investigate_log("was brought online and is now producing gravity for this level.", INVESTIGATE_GRAVITY)
@@ -325,89 +322,69 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
on = FALSE
update_use_power(IDLE_POWER_USE)
- if (!SSticker.IsRoundInProgress())
- return
-
soundloop.stop()
+ QDEL_NULL(gravity_field)
if (gravity_in_level())
investigate_log("was brought offline and there is now no gravity for this level.", INVESTIGATE_GRAVITY)
message_admins("The gravity generator was brought offline with no backup generator. [ADMIN_VERBOSEJMP(src)]")
shake_everyone()
- QDEL_NULL(gravity_field)
complete_state_update()
/obj/machinery/gravity_generator/main/proc/complete_state_update()
update_appearance()
update_list()
-// Set the state of the gravity.
-/obj/machinery/gravity_generator/main/proc/set_state(new_state)
-
- on = new_state
- update_use_power(on ? ACTIVE_POWER_USE : IDLE_POWER_USE)
- // Sound the alert if gravity was just enabled or disabled.
- var/alert = FALSE
- if(SSticker.IsRoundInProgress())
- if(on) // If we turned on and the game is live.
-
- else
-
-
-
- if(alert)
- shake_everyone()
-
// Charge/Discharge and turn on/off gravity when you reach 0/100 percent.
/obj/machinery/gravity_generator/main/process()
if(machine_stat & BROKEN)
return
- if(charging_state != POWER_IDLE)
- if(charging_state == POWER_UP && charge_count >= 100)
- enable()
- else if(charging_state == POWER_DOWN && charge_count <= 0)
- disable()
- else
- if(charging_state == POWER_UP)
- charge_count += 2
- else if(charging_state == POWER_DOWN)
- charge_count -= 2
-
- if(charge_count % 4 == 0 && prob(75)) // Let them know it is charging/discharging.
- playsound(src.loc, 'sound/effects/empulse.ogg', 100, TRUE)
-
- var/overlay_state = null
- switch(charge_count)
- if(0 to 20)
- overlay_state = null
- if(21 to 40)
- overlay_state = "startup"
- if(41 to 60)
- overlay_state = "idle"
- if(61 to 80)
- overlay_state = "activating"
- if(81 to 100)
- overlay_state = "activated"
-
- if(overlay_state != current_overlay)
- if(middle)
- middle.cut_overlays()
- if(overlay_state)
- middle.add_overlay(overlay_state)
- current_overlay = overlay_state
-
-// Shake everyone on the z level to let them know that gravity was enagaged/disenagaged.
+ if(charging_state == POWER_IDLE)
+ return
+ if(charging_state == POWER_UP && charge_count >= 100)
+ enable()
+ else if(charging_state == POWER_DOWN && charge_count <= 0)
+ disable()
+ else
+ if(charging_state == POWER_UP)
+ charge_count += 2
+ else if(charging_state == POWER_DOWN)
+ charge_count -= 2
+
+ if(charge_count % 4 == 0 && prob(75)) // Let them know it is charging/discharging.
+ playsound(src.loc, 'sound/effects/empulse.ogg', 100, TRUE)
+
+ var/overlay_state = null
+ switch(charge_count)
+ if(0 to 20)
+ overlay_state = null
+ if(21 to 40)
+ overlay_state = "startup"
+ if(41 to 60)
+ overlay_state = "idle"
+ if(61 to 80)
+ overlay_state = "activating"
+ if(81 to 100)
+ overlay_state = "activated"
+
+ if(overlay_state != current_overlay)
+ if(center_part)
+ center_part.cut_overlays()
+ if(overlay_state)
+ center_part.add_overlay(overlay_state)
+ current_overlay = overlay_state
+
+/// Shake everyone on the z level to let them know that gravity was enagaged/disengaged.
/obj/machinery/gravity_generator/main/proc/shake_everyone()
var/turf/T = get_turf(src)
var/sound/alert_sound = sound('sound/effects/alert.ogg')
- for(var/i in GLOB.mob_list)
- var/mob/M = i
- if(M.z != z && !(SSmapping.level_trait(z, ZTRAITS_STATION) && SSmapping.level_trait(M.z, ZTRAITS_STATION)))
+ for(var/mob/mobs as anything in GLOB.mob_list)
+ if(mobs.z != z && !(SSmapping.level_trait(z, ZTRAITS_STATION) && SSmapping.level_trait(mobs.z, ZTRAITS_STATION)))
continue
- M.update_gravity(M.has_gravity())
- if(M.client)
- shake_camera(M, 15, 1)
- M.playsound_local(T, null, 100, 1, 0.5, S = alert_sound)
+ mobs.update_gravity(mobs.has_gravity())
+ if(mobs.client)
+ shake_camera(mobs, 15, 1)
+ mobs.playsound_local(T, null, 100, 1, 0.5, sound_to_use = alert_sound)
/obj/machinery/gravity_generator/main/proc/gravity_in_level()
var/turf/T = get_turf(src)
@@ -418,30 +395,54 @@ GLOBAL_LIST_EMPTY(gravity_generators) // We will keep track of this by adding ne
return FALSE
/obj/machinery/gravity_generator/main/proc/update_list()
- var/turf/T = get_turf(src.loc)
- if(T)
- var/list/z_list = list()
- // Multi-Z, station gravity generator generates gravity on all ZTRAIT_STATION z-levels.
- if(SSmapping.level_trait(T.z, ZTRAIT_STATION))
- for(var/z in SSmapping.levels_by_trait(ZTRAIT_STATION))
- z_list += z
+ var/turf/T = get_turf(src)
+ if(!T)
+ return
+ var/list/z_list = list()
+ // Multi-Z, station gravity generator generates gravity on all ZTRAIT_STATION z-levels.
+ if(SSmapping.level_trait(T.z, ZTRAIT_STATION))
+ for(var/z in SSmapping.levels_by_trait(ZTRAIT_STATION))
+ z_list += z
+ else
+ z_list += T.z
+ for(var/z in z_list)
+ if(!GLOB.gravity_generators["[z]"])
+ GLOB.gravity_generators["[z]"] = list()
+ if(on)
+ GLOB.gravity_generators["[z]"] |= src
else
- z_list += T.z
- for(var/z in z_list)
- if(!GLOB.gravity_generators["[z]"])
- GLOB.gravity_generators["[z]"] = list()
- if(on)
- GLOB.gravity_generators["[z]"] |= src
- else
- GLOB.gravity_generators["[z]"] -= src
+ GLOB.gravity_generators["[z]"] -= src
+ SSmapping.calculate_z_level_gravity(z)
/obj/machinery/gravity_generator/main/proc/change_setting(value)
if(value != setting)
setting = value
shake_everyone()
+/obj/machinery/gravity_generator/main/proc/blackout()
+ charge_count = 0
+ breaker = FALSE
+ set_power()
+ disable()
+ investigate_log("was turned off by blackout event or a gravity anomaly detonation.", INVESTIGATE_GRAVITY)
+
+/obj/machinery/gravity_generator/main/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock)
+ . = ..()
+ disable()
+
+/obj/machinery/gravity_generator/main/afterShuttleMove(turf/oldT, list/movement_force, shuttle_dir, shuttle_preferred_direction, move_dir, rotation)
+ . = ..()
+ if(charge_count != 0 && charging_state != POWER_UP)
+ enable()
+
+//prevents shuttles attempting to rotate this since it messes up sprites
+/obj/machinery/gravity_generator/main/shuttleRotate(rotation, params)
+ params = NONE
+ return ..()
+
// Misc
+/// Gravity generator instruction guide
/obj/item/paper/guides/jobs/engi/gravity_gen
name = "paper- 'Generate your own gravity!'"
info = {"
Gravity Generator Instructions For Dummies
diff --git a/code/modules/power/lighting/light.dm b/code/modules/power/lighting/light.dm
index 2a2d7e4658199..4d08c8ac496eb 100644
--- a/code/modules/power/lighting/light.dm
+++ b/code/modules/power/lighting/light.dm
@@ -18,8 +18,6 @@
var/base_state = "tube"
///Is the light on?
var/on = FALSE
- ///compared to the var/on for static calculations
- var/on_gs = FALSE
///Amount of power used
var/static_power_used = 0
///Luminosity when on, also used in power calculation
@@ -54,18 +52,26 @@
var/nightshift_light_power = 0.45
///Basecolor of the nightshift light
var/nightshift_light_color = "#FFDDCC"
- ///If true, the light is in emergency mode
- var/emergency_mode = FALSE
- ///If true, this light cannot ever have an emergency mode
- var/no_emergency = FALSE
- ///Multiplier for this light's base brightness in emergency power mode
- var/bulb_emergency_brightness_mul = 0.25
- ///Determines the colour of the light while it's in emergency mode
- var/bulb_emergency_colour = "#FF3232"
- ///The multiplier for determining the light's power in emergency mode
- var/bulb_emergency_pow_mul = 0.75
- ///The minimum value for the light's power in emergency mode
- var/bulb_emergency_pow_min = 0.5
+ ///If true, the light is in low power mode
+ var/low_power_mode = FALSE
+ ///If true, this light cannot ever be in low power mode
+ var/no_low_power = FALSE
+ ///If true, overrides lights to use emergency lighting
+ var/major_emergency = FALSE
+ ///Multiplier for this light's base brightness during a cascade
+ var/bulb_major_emergency_brightness_mul = 0.75
+ ///Colour of the light when major emergency mode is on
+ var/bulb_emergency_colour = "#ff4e4e"
+ ///Multiplier for this light's base brightness in low power power mode
+ var/bulb_low_power_brightness_mul = 0.25
+ ///Determines the colour of the light while it's in low power mode
+ var/bulb_low_power_colour = "#FF3232"
+ ///The multiplier for determining the light's power in low power mode
+ var/bulb_low_power_pow_mul = 0.75
+ ///The minimum value for the light's power in low power mode
+ var/bulb_low_power_pow_min = 0.5
+ ///Power usage - W per unit of luminosity
+ var/power_consumption_rate = 20
/obj/machinery/light/Move()
if(status != LIGHT_BROKEN)
@@ -81,7 +87,7 @@
var/obj/machinery/power/apc/temp_apc = our_area.apc
nightshift_enabled = temp_apc?.nightshift_lights
- if(start_with_cell && !no_emergency)
+ if(start_with_cell && !no_low_power)
cell = new/obj/item/stock_parts/cell/emergency_light(src)
RegisterSignal(src, COMSIG_LIGHT_EATER_ACT, .proc/on_light_eater)
@@ -110,7 +116,7 @@
switch(status) // set icon_states
if(LIGHT_OK)
var/area/local_area = get_area(src)
- if(emergency_mode || (local_area?.fire))
+ if(low_power_mode || major_emergency || (local_area?.fire))
icon_state = "[base_state]_emergency"
else
icon_state = "[base_state]"
@@ -128,7 +134,7 @@
return
var/area/local_area = get_area(src)
- if(emergency_mode || (local_area?.fire))
+ if(low_power_mode || major_emergency || (local_area?.fire))
. += mutable_appearance(overlay_icon, "[base_state]_emergency")
return
if(nightshift_enabled)
@@ -163,7 +169,7 @@
switch(status)
if(LIGHT_BROKEN,LIGHT_BURNED,LIGHT_EMPTY)
on = FALSE
- emergency_mode = FALSE
+ low_power_mode = FALSE
if(on)
var/brightness_set = brightness
var/power_set = bulb_power
@@ -172,12 +178,15 @@
color_set = color
var/area/local_area = get_area(src)
if (local_area?.fire)
- color_set = bulb_emergency_colour
+ color_set = bulb_low_power_colour
else if (nightshift_enabled)
brightness_set = nightshift_brightness
power_set = nightshift_light_power
if(!color)
color_set = nightshift_light_color
+ else if (major_emergency)
+ color_set = bulb_low_power_colour
+ brightness_set = brightness * bulb_major_emergency_brightness_mul
var/matching = light && brightness_set == light.light_range && power_set == light.light_power && color_set == light.light_color
if(!matching)
switchcount++
@@ -196,22 +205,30 @@
)
else if(has_emergency_power(LIGHT_EMERGENCY_POWER_USE) && !turned_off())
use_power = IDLE_POWER_USE
- emergency_mode = TRUE
+ low_power_mode = TRUE
START_PROCESSING(SSmachines, src)
else
use_power = IDLE_POWER_USE
set_light(l_range = 0)
update_appearance()
+ update_current_power_usage()
+ broken_sparks(start_only=TRUE)
- if(on != on_gs)
- on_gs = on
- if(on)
- static_power_used = brightness * 20 //20W per unit luminosity
- addStaticPower(static_power_used, AREA_USAGE_STATIC_LIGHT)
+/obj/machinery/light/update_current_power_usage()
+ if(!on && static_power_used > 0) //Light is off but still powered
+ removeStaticPower(static_power_used, AREA_USAGE_STATIC_LIGHT)
+ static_power_used = 0
+ else if(on) //Light is on, just recalculate usage
+ var/static_power_used_new = 0
+ var/area/local_area = get_area(src)
+ if (nightshift_enabled && !local_area?.fire)
+ static_power_used_new = nightshift_brightness * nightshift_light_power * power_consumption_rate
else
+ static_power_used_new = brightness * bulb_power * power_consumption_rate
+ if(static_power_used != static_power_used_new) //Consumption changed - update
removeStaticPower(static_power_used, AREA_USAGE_STATIC_LIGHT)
-
- broken_sparks(start_only=TRUE)
+ static_power_used = static_power_used_new
+ addStaticPower(static_power_used, AREA_USAGE_STATIC_LIGHT)
/obj/machinery/light/update_atom_colour()
..()
@@ -231,7 +248,7 @@
if (cell.charge == cell.maxcharge)
return PROCESS_KILL
cell.charge = min(cell.maxcharge, cell.charge + LIGHT_EMERGENCY_POWER_USE) //Recharge emergency power automatically while not using it
- if(emergency_mode && !use_emergency_power(LIGHT_EMERGENCY_POWER_USE))
+ if(low_power_mode && !use_emergency_power(LIGHT_EMERGENCY_POWER_USE))
update(FALSE) //Disables emergency mode and sets the color to normal
/obj/machinery/light/proc/burn_out()
@@ -399,7 +416,7 @@
// returns whether this light has emergency power
// can also return if it has access to a certain amount of that power
/obj/machinery/light/proc/has_emergency_power(power_usage_amount)
- if(no_emergency || !cell)
+ if(no_low_power || !cell)
return FALSE
if(power_usage_amount ? cell.charge >= power_usage_amount : cell.charge)
return status == LIGHT_OK
@@ -414,9 +431,9 @@
return FALSE
cell.use(power_usage_amount)
set_light(
- l_range = brightness * bulb_emergency_brightness_mul,
- l_power = max(bulb_emergency_pow_min, bulb_emergency_pow_mul * (cell.charge / cell.maxcharge)),
- l_color = bulb_emergency_colour
+ l_range = brightness * bulb_low_power_brightness_mul,
+ l_power = max(bulb_low_power_pow_min, bulb_low_power_pow_mul * (cell.charge / cell.maxcharge)),
+ l_color = bulb_low_power_colour
)
return TRUE
@@ -441,8 +458,8 @@
// ai attack - make lights flicker, because why not
/obj/machinery/light/attack_ai(mob/user)
- no_emergency = !no_emergency
- to_chat(user, span_notice("Emergency lights for this fixture have been [no_emergency ? "disabled" : "enabled"]."))
+ no_low_power = !no_low_power
+ to_chat(user, span_notice("Emergency lights for this fixture have been [no_low_power ? "disabled" : "enabled"]."))
update(FALSE)
return
@@ -515,6 +532,14 @@
// create a light tube/bulb item and put it in the user's hand
drop_light_tube(user)
+/obj/machinery/light/proc/set_major_emergency_light()
+ major_emergency = TRUE
+ update()
+
+/obj/machinery/light/proc/unset_major_emergency_light()
+ major_emergency = FALSE
+ update()
+
/obj/machinery/light/proc/drop_light_tube(mob/user)
var/obj/item/light/light_object = new light_type()
light_object.status = status
diff --git a/code/modules/power/lighting/light_mapping_helpers.dm b/code/modules/power/lighting/light_mapping_helpers.dm
index 4774241db2265..dccf1f3c65513 100644
--- a/code/modules/power/lighting/light_mapping_helpers.dm
+++ b/code/modules/power/lighting/light_mapping_helpers.dm
@@ -30,7 +30,7 @@
/obj/machinery/light/red
bulb_colour = "#FF3232"
nightshift_allowed = FALSE
- no_emergency = TRUE
+ no_low_power = TRUE
brightness = 4
bulb_power = 0.7
@@ -72,7 +72,7 @@
/obj/machinery/light/small/red
bulb_colour = "#FF3232"
- no_emergency = TRUE
+ no_low_power = TRUE
nightshift_allowed = FALSE
brightness = 2
bulb_power = 0.8
diff --git a/code/modules/power/port_gen.dm b/code/modules/power/port_gen.dm
index 95ee471e72cb7..a401cc1f21862 100644
--- a/code/modules/power/port_gen.dm
+++ b/code/modules/power/port_gen.dm
@@ -278,4 +278,4 @@
sheet_path = /obj/item/stack/sheet/mineral/uranium
/obj/machinery/power/port_gen/pacman/pre_loaded
- sheets = 50
+ sheets = 15
diff --git a/code/modules/power/singularity/narsie.dm b/code/modules/power/singularity/narsie.dm
index d8e9ae1b2ab7d..bcb2a8b51ff24 100644
--- a/code/modules/power/singularity/narsie.dm
+++ b/code/modules/power/singularity/narsie.dm
@@ -235,7 +235,7 @@
///security level and shuttle lockdowns for [/proc/begin_the_end()]
/proc/narsie_start_destroy_station()
- set_security_level("delta")
+ SSsecurity_level.set_level(SEC_LEVEL_DELTA)
SSshuttle.registerHostileEnvironment(GLOB.cult_narsie)
SSshuttle.lockdown = TRUE
addtimer(CALLBACK(GLOBAL_PROC, .proc/narsie_apocalypse), 1 MINUTES)
@@ -255,7 +255,7 @@
///Called only if the crew managed to destroy narsie at the very last second for [/proc/begin_the_end()]
/proc/narsie_last_second_win()
- set_security_level("red")
+ SSsecurity_level.set_level(SEC_LEVEL_RED)
SSshuttle.lockdown = FALSE
INVOKE_ASYNC(GLOBAL_PROC, .proc/cult_ending_helper, CULT_FAILURE_NARSIE_KILLED)
diff --git a/code/modules/power/supermatter/supermatter.dm b/code/modules/power/supermatter/supermatter.dm
index 5eef6bb898f54..2f3623d7f3e00 100644
--- a/code/modules/power/supermatter/supermatter.dm
+++ b/code/modules/power/supermatter/supermatter.dm
@@ -12,6 +12,10 @@
#define OBJECT (LOWEST + 1)
#define LOWEST (1)
+#define CASCADING_ADMIN "Admin"
+#define CASCADING_CRITICAL_GAS "Critical gas point"
+#define CASCADING_DESTAB_CRYSTAL "Destabilizing crystal"
+
GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
/obj/machinery/power/supermatter_crystal
@@ -313,7 +317,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
/obj/machinery/power/supermatter_crystal/Destroy()
if(warp)
vis_contents -= warp
- warp = null
+ QDEL_NULL(warp)
investigate_log("has been destroyed.", INVESTIGATE_ENGINE)
SSair.stop_processing_machine(src)
QDEL_NULL(radio)
@@ -334,6 +338,8 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
var/immune = HAS_TRAIT(user, TRAIT_MADNESS_IMMUNE) || (user.mind && HAS_TRAIT(user.mind, TRAIT_MADNESS_IMMUNE))
if(isliving(user) && !immune && (get_dist(user, src) < HALLUCINATION_RANGE(power)))
. += span_danger("You get headaches just from looking at it.")
+ if(cascade_initiated)
+ . += span_bolddanger("The crystal is vibrating at immense speeds, warping space around it!")
// SupermatterMonitor UI for ghosts only. Inherited attack_ghost will call this.
/obj/machinery/power/supermatter_crystal/ui_interact(mob/user, datum/tgui/ui)
@@ -431,7 +437,7 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
/obj/machinery/power/supermatter_crystal/update_overlays()
. = ..()
- if(final_countdown)
+ if(final_countdown && !cascade_initiated)
. += "casuality_field"
/obj/machinery/power/supermatter_crystal/proc/countdown()
@@ -442,11 +448,22 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
final_countdown = TRUE
update_appearance()
- var/speaking = "[emergency_alert] The supermatter has reached critical integrity failure. Emergency causality destabilization field has been activated."
+ var/cascading = cascade_initiated
+
+ var/speaking = "[emergency_alert] The supermatter has reached critical integrity failure."
+
+ if(cascading)
+ speaking += " Harmonic frequency limits exceeded. Causality destabilization field could not be engaged."
+ else
+ speaking += " Emergency causality destabilization field has been activated."
+
radio.talk_into(src, speaking, common_channel, language = get_selected_language())
for(var/i in SUPERMATTER_COUNTDOWN_TIME to 0 step -10)
if(damage < explosion_point) // Cutting it a bit close there engineers
- radio.talk_into(src, "[safe_alert] Failsafe has been disengaged.", common_channel)
+ if(cascading)
+ radio.talk_into(src, "[safe_alert] Harmonic frequency restored within emergency bounds. Anti-resonance filter initiated.", common_channel)
+ else
+ radio.talk_into(src, "[safe_alert] Failsafe has been disengaged.", common_channel)
final_countdown = FALSE
update_appearance()
return
@@ -454,18 +471,21 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
sleep(10)
continue
else if(i > 50)
- speaking = "[DisplayTimeText(i, TRUE)] remain before causality stabilization."
+ if(cascading)
+ speaking = "[DisplayTimeText(i, TRUE)] remain before resonance-induced stabilization."
+ else
+ speaking = "[DisplayTimeText(i, TRUE)] remain before causality stabilization."
else
speaking = "[i*0.1]..."
radio.talk_into(src, speaking, common_channel)
- sleep(10)
+ sleep(1 SECONDS)
delamination_event()
/obj/machinery/power/supermatter_crystal/proc/delamination_event()
var/can_spawn_anomalies = is_station_level(loc.z) && is_main_engine && anomaly_event
- var/is_cascading = check_cascade_requirements(anomaly_event)
+ var/is_cascading = cascade_initiated
new /datum/supermatter_delamination(power, combined_gas, get_turf(src), explosion_power, gasmix_power_ratio, can_spawn_anomalies, is_cascading)
qdel(src)
@@ -476,29 +496,34 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
delamination_event()
/**
- * Checks if the supermatter is in a state where it can cascade
+ * Checks if and why the supermatter is in a state where it can cascade
*
- * Arguments: can_trigger = TRUE if the supermatter can trigger the cascade
- * Returns: TRUE if the supermatter can cascade
+ * Returns: cause of the cascade, for logging
*/
-/obj/machinery/power/supermatter_crystal/proc/check_cascade_requirements(can_trigger)
+/obj/machinery/power/supermatter_crystal/proc/check_cascade_requirements()
+ if(admin_cascade)
+ return CASCADING_ADMIN
- if(get_integrity_percent() < SUPERMATTER_CASCADE_PERCENT && !cascade_initiated && !admin_cascade && can_trigger)
+ if(!anomaly_event)
return FALSE
- var/supermatter_cascade = can_trigger
+ if(has_destabilizing_crystal)
+ return CASCADING_DESTAB_CRYSTAL
+
+ var/critical_gas_exceeded = TRUE
var/list/required_gases = list(/datum/gas/hypernoblium, /datum/gas/antinoblium)
- for(var/gas_path in required_gases)
- if(has_destabilizing_crystal)
- break // We have a destabilizing crystal, we're good
- if(gas_comp[gas_path] < 0.4 || environment_total_moles < MOLE_PENALTY_THRESHOLD)
- supermatter_cascade = FALSE
- break
+ if(environment_total_moles < MOLE_PENALTY_THRESHOLD)
+ critical_gas_exceeded = FALSE
+ else
+ for(var/gas_path in required_gases)
+ if(gas_comp[gas_path] < 0.4)
+ critical_gas_exceeded = FALSE
+ break
- if(admin_cascade)
- supermatter_cascade = TRUE
+ if(critical_gas_exceeded)
+ return CASCADING_CRITICAL_GAS
- return supermatter_cascade
+ return FALSE
/obj/machinery/power/supermatter_crystal/proc/supermatter_pull(turf/center, pull_range = 3)
playsound(center, 'sound/weapons/marauder.ogg', 100, TRUE, extrarange = pull_range - world.view)
@@ -530,8 +555,8 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
new /obj/effect/anomaly/hallucination(local_turf, has_changed_lifespan ? rand(150, 250) : null, FALSE)
if(VORTEX_ANOMALY)
new /obj/effect/anomaly/bhole(local_turf, 20, FALSE)
- if(DELIMBER_ANOMALY)
- new /obj/effect/anomaly/delimber(local_turf, null, FALSE)
+ if(BIOSCRAMBLER_ANOMALY)
+ new /obj/effect/anomaly/bioscrambler(local_turf, null, FALSE)
/obj/machinery/proc/supermatter_zap(atom/zapstart = src, range = 5, zap_str = 4000, zap_flags = ZAP_SUPERMATTER_FLAGS, list/targets_hit = list(), zap_cutoff = 1500, power_level = 0, zap_icon = DEFAULT_ZAP_ICON_STATE, color = null)
if(QDELETED(zapstart))
@@ -728,6 +753,10 @@ GLOBAL_DATUM(main_supermatter_engine, /obj/machinery/power/supermatter_crystal)
pixel_x = -176
pixel_y = -176
+#undef CASCADING_ADMIN
+#undef CASCADING_CRITICAL_GAS
+#undef CASCADING_DESTAB_CRYSTAL
+
#undef BIKE
#undef COIL
#undef ROD
diff --git a/code/modules/power/supermatter/supermatter_cascade_components.dm b/code/modules/power/supermatter/supermatter_cascade_components.dm
index fae4dd5d9d116..6851760b6acf6 100644
--- a/code/modules/power/supermatter/supermatter_cascade_components.dm
+++ b/code/modules/power/supermatter/supermatter_cascade_components.dm
@@ -10,11 +10,13 @@
anchored = TRUE
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF
light_power = 1
- light_range = 7
+ light_range = 5
light_color = COLOR_VIVID_YELLOW
move_resist = INFINITY
///All dirs we can expand to
var/list/available_dirs = list(NORTH,SOUTH,EAST,WEST,UP,DOWN)
+ ///Handler that helps with properly killing mobs that the crystal grows over
+ var/datum/component/supermatter_crystal/sm_comp
///Cooldown on the expansion process
COOLDOWN_DECLARE(sm_wall_cooldown)
@@ -23,13 +25,14 @@
icon_state = "crystal_cascade_[rand(1,6)]"
START_PROCESSING(SSsupermatter_cascade, src)
- AddComponent(/datum/component/supermatter_crystal, null, null)
+ sm_comp = AddComponent(/datum/component/supermatter_crystal, null, null)
playsound(src, 'sound/misc/cracking_crystal.ogg', 45, TRUE)
available_dirs -= dir_to_remove
var/turf/our_turf = get_turf(src)
+
if(our_turf)
our_turf.opacity = FALSE
@@ -39,7 +42,6 @@
return
if(!available_dirs || available_dirs.len <= 0)
- light_range = 0
return PROCESS_KILL
COOLDOWN_START(src, sm_wall_cooldown, rand(0, 3 SECONDS))
@@ -53,9 +55,18 @@
return
for(var/atom/movable/checked_atom as anything in next_turf)
- if(!isliving(checked_atom) && !istype(checked_atom, /obj/cascade_portal))
- continue
- qdel(checked_atom)
+ if(isliving(checked_atom))
+ sm_comp.dust_mob(src, checked_atom, span_danger("\The [src] lunges out on [checked_atom], touching [checked_atom.p_them()]... \
+ [checked_atom.p_their()] body begins to shine with a brilliant light before crystallizing from the inside out and joining \the [src]!"),
+ span_userdanger("The crystal mass lunges on you and hits you in the chest. As your vision is filled with a blinding light, you think to yourself \"Damn it.\""))
+ else if(istype(checked_atom, /obj/cascade_portal))
+ checked_atom.visible_message(span_userdanger("\The [checked_atom] screeches and closes away as it is hit by \a [src]! Too late!"))
+ playsound(get_turf(checked_atom), 'sound/magic/charge.ogg', 50, TRUE)
+ playsound(get_turf(checked_atom), 'sound/effects/supermatter.ogg', 50, TRUE)
+ qdel(checked_atom)
+ else if(isitem(checked_atom))
+ playsound(get_turf(checked_atom), 'sound/effects/supermatter.ogg', 50, TRUE)
+ qdel(checked_atom)
new /obj/crystal_mass(next_turf, get_dir(next_turf, src))
@@ -78,9 +89,13 @@
qdel(rip_u)
return COMPONENT_CANCEL_ATTACK_CHAIN
+/obj/crystal_mass/Destroy()
+ sm_comp = null
+ return ..()
+
/obj/cascade_portal
name = "Bluespace Rift"
- desc = "Your mind begins to bubble and ooze as it tries to comprehend what it sees."
+ desc = "Your mind begins to spin as it tries to comprehend what it sees."
icon = 'icons/effects/224x224.dmi'
icon_state = "reality"
anchored = TRUE
@@ -96,18 +111,9 @@
resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF | FREEZE_PROOF
/obj/cascade_portal/Bumped(atom/movable/hit_object)
- if(isliving(hit_object))
- hit_object.visible_message(span_danger("\The [hit_object] slams into \the [src] inducing a resonance... [hit_object.p_their()] body starts to glow and burst into flames before flashing into dust!"),
- span_userdanger("You slam into \the [src] as your ears are filled with unearthly ringing. Your last thought is \"Oh, fuck.\""),
- span_hear("You hear an unearthly noise as a wave of heat washes over you."))
- else if(isobj(hit_object) && !iseffect(hit_object))
- hit_object.visible_message(span_danger("\The [hit_object] smacks into \the [src] and rapidly flashes to ash."), null,
- span_hear("You hear a loud crack as you are washed with a wave of heat."))
- else
- return
-
- playsound(get_turf(src), 'sound/effects/supermatter.ogg', 50, TRUE)
consume(hit_object)
+ new /obj/effect/particle_effect/sparks(loc)
+ playsound(loc, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
/**
* Proc to consume the objects colliding with the portal
@@ -116,17 +122,30 @@
*/
/obj/cascade_portal/proc/consume(atom/movable/consumed_object)
if(isliving(consumed_object))
+ consumed_object.visible_message(span_danger("\The [consumed_object] walks into \the [src]... \
+ A blinding light covers [consumed_object.p_their()] body before disappearing completely!"),
+ span_userdanger("You walk into \the [src] as your body is washed with a powerful blue light. \
+ You contemplate about this decision before landing face first onto the cold, hard floor."),
+ span_hear("You hear a loud crack as a distortion passes through you."))
+
var/list/arrival_turfs = get_area_turfs(/area/centcom/central_command_areas/evacuation)
- var/turf/arrival_turf = pick(arrival_turfs)
+ var/turf/arrival_turf
+ do
+ arrival_turf = pick_n_take(arrival_turfs)
+ while(!is_safe_turf(arrival_turf))
+
var/mob/living/consumed_mob = consumed_object
- if(consumed_mob.status_flags & GODMODE)
- return
message_admins("[key_name_admin(consumed_mob)] has entered [src] [ADMIN_JMP(src)].")
investigate_log("was entered by [key_name(consumed_mob)].", INVESTIGATE_ENGINE)
consumed_mob.forceMove(arrival_turf)
consumed_mob.Paralyze(100)
consumed_mob.adjustBruteLoss(30)
- else if(consumed_object.flags_1 & SUPERMATTER_IGNORES_1)
- return
- else if(isobj(consumed_object))
+ consumed_mob.flash_act(1, TRUE, TRUE)
+
+ new /obj/effect/particle_effect/sparks(consumed_object)
+ playsound(consumed_object, SFX_SPARKS, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ else if(isitem(consumed_object))
+ consumed_object.visible_message(span_danger("\The [consumed_object] smacks into \the [src] and disappears out of sight."), null,
+ span_hear("You hear a loud crack as a small distortion passes through you."))
+
qdel(consumed_object)
diff --git a/code/modules/power/supermatter/supermatter_delamination.dm b/code/modules/power/supermatter/supermatter_delamination.dm
index 42d418d0d912a..1fb0335e94747 100644
--- a/code/modules/power/supermatter/supermatter_delamination.dm
+++ b/code/modules/power/supermatter/supermatter_delamination.dm
@@ -1,3 +1,6 @@
+///Minimum distance that a crystal mass must have from the rift
+#define MIN_RIFT_SAFE_DIST 30
+
/datum/supermatter_delamination
///Power amount of the SM at the moment of death
var/supermatter_power = 0
@@ -170,76 +173,168 @@
* Setup for the cascade delamination
*/
/datum/supermatter_delamination/proc/start_supermatter_cascade()
- SSshuttle.registerHostileEnvironment(src)
+ // buncha shuttle manipulation incoming
+
+ // set timer to infinity, so shuttle never arrives
+ SSshuttle.emergency.setTimer(INFINITY)
+ // disallow shuttle recalls, so people cannot cheese the timer
+ SSshuttle.emergency_no_recall = TRUE
+ // set supermatter cascade to true, to prevent auto evacuation due to no way of calling the shuttle
SSshuttle.supermatter_cascade = TRUE
+ // This logic is to keep uncalled shuttles uncalled
+ // In SSshuttle, there is not much of a way to prevent shuttle calls, unless we mess with admin panel vars
+ // SHUTTLE_STRANDED is different here, because it *can* block the shuttle from being called, however if we don't register a hostile
+ // environment, it gets unset immediately. Internally, it checks if the count of HEs is zero
+ // and that the shuttle is in stranded mode, then frees it with an announcement.
+ // This is a botched solution to a problem that could be solved with a small change in shuttle code, however-
+ if(SSshuttle.emergency.mode == SHUTTLE_IDLE)
+ SSshuttle.emergency.mode = SHUTTLE_STRANDED
+ SSshuttle.registerHostileEnvironment(src)
+ // set hijack completion timer to infinity, so that you cant prematurely end the round with a hijack
+ for(var/obj/machinery/computer/emergency_shuttle/console in GLOB.machines)
+ console.hijack_completion_flight_time_set = INFINITY
+
+ for(var/mob/player as anything in GLOB.player_list)
+ if(!isdead(player))
+ to_chat(player, span_boldannounce("Everything around you is resonating with a powerful energy. This can't be good."))
+ SEND_SIGNAL(player, COMSIG_ADD_MOOD_EVENT, "cascade", /datum/mood_event/cascade)
+ SEND_SOUND(player, 'sound/magic/charge.ogg')
+
call_explosion()
create_cascade_ambience()
- pick_rift_location()
warn_crew()
+
+ var/rift_loc = pick_rift_location()
new /obj/crystal_mass(supermatter_turf)
+
+ var/list/mass_loc_candidates = GLOB.generic_event_spawns.Copy()
+ mass_loc_candidates.Remove(rift_loc) // this should now actually get rid of stalemates
for(var/i in 1 to rand(4,6))
- new /obj/crystal_mass(get_turf(pick(GLOB.generic_event_spawns)))
+ var/list/loc_list = mass_loc_candidates.Copy()
+ var/mass_loc
+ do
+ mass_loc = pick_n_take(loc_list)
+ while(get_dist(mass_loc, rift_loc) < MIN_RIFT_SAFE_DIST)
+ new /obj/crystal_mass(get_turf(mass_loc))
+
+ SSsupermatter_cascade.cascade_initiated = TRUE
/**
* Adds a bit of spiciness to the cascade by breaking lights and turning emergency maint access on
*/
/datum/supermatter_delamination/proc/create_cascade_ambience()
- break_lights_on_station()
+ if(SSsecurity_level.get_current_level_as_number() != SEC_LEVEL_DELTA)
+ SSsecurity_level.set_level(SEC_LEVEL_DELTA) // skip the announcement and shuttle timer adjustment in set_security_level()
make_maint_all_access()
+ break_lights_on_station()
/**
* Picks a random location for the rift
+ * Returns: ref to rift location
*/
/datum/supermatter_delamination/proc/pick_rift_location()
- var/turf/rift_location = get_turf(pick(GLOB.generic_event_spawns))
- cascade_rift = new /obj/cascade_portal(rift_location)
+ var/rift_spawn = pick(GLOB.generic_event_spawns)
+ var/turf/rift_turf = get_turf(rift_spawn)
+ cascade_rift = new /obj/cascade_portal(rift_turf)
+ message_admins("Exit rift created at [get_area_name(rift_turf)]. [ADMIN_JMP(cascade_rift)]")
+ log_game("Bluespace Exit Rift was created at [get_area_name(rift_turf)].")
+ cascade_rift.investigate_log("created at [get_area_name(rift_turf)].", INVESTIGATE_ENGINE)
RegisterSignal(cascade_rift, COMSIG_PARENT_QDELETING, .proc/deleted_portal)
+ return rift_spawn
/**
* Warns the crew about the cascade start and the rift location
*/
/datum/supermatter_delamination/proc/warn_crew()
- for(var/mob/player as anything in GLOB.alive_player_list)
- to_chat(player, span_boldannounce("You feel a strange presence in the air around you. You feel unsafe."))
-
- priority_announce("Unknown harmonance affecting local spatial substructure, all nearby matter is starting to crystallize.", "Central Command Higher Dimensional Affairs", 'sound/misc/bloblarm.ogg')
- priority_announce("There's been a sector-wide electromagnetic pulse. All of our systems are heavily damaged, including those required for emergency shuttle navigation. \
- We can only reasonably conclude that a supermatter cascade has been initiated on or near your station. \
- Evacuation is no longer possible by conventional means; however, we managed to open a rift near the [get_area_name(cascade_rift)]. \
- All personnel are hereby advised to enter the rift using all means available. Retrieval of survivors will be conducted upon recovery of necessary facilities. \
- Good l\[\[###!!!-")
+ priority_announce("A Type-C resonance shift event has occurred in your sector. Scans indicate local oscillation flux affecting spatial and gravitational substructure. \
+ Multiple resonance hotspots have formed. Please standby.", "Nanotrasen Star Observation Association", ANNOUNCER_SPANOMALIES)
+ if(SSshuttle.emergency.mode != SHUTTLE_STRANDED)
+ addtimer(CALLBACK(src, .proc/announce_shuttle_gone), 2 SECONDS)
- addtimer(CALLBACK(src, .proc/delta), 10 SECONDS)
+ addtimer(CALLBACK(src, .proc/announce_beginning), 5 SECONDS)
+/**
+ * Logs the deletion of the bluespace rift, and starts countdown to the end of the round.
+ */
/datum/supermatter_delamination/proc/deleted_portal()
SIGNAL_HANDLER
+ message_admins("[cascade_rift] deleted at [get_area_name(cascade_rift.loc)]. [ADMIN_JMP(cascade_rift.loc)]")
+ log_game("[cascade_rift] was deleted.")
+ cascade_rift.investigate_log("was deleted.", INVESTIGATE_ENGINE)
- priority_announce("The rift has been destroyed, we can no longer help you...", "Warning", 'sound/misc/bloblarm.ogg')
+ priority_announce("[Gibberish("The rift has been destroyed, we can no longer help you.", FALSE, 5)]")
+ addtimer(CALLBACK(src, .proc/announce_gravitation_shift), 25 SECONDS)
addtimer(CALLBACK(src, .proc/last_message), 50 SECONDS)
+ if(SSshuttle.emergency.mode != SHUTTLE_ESCAPE) // if the shuttle is enroute to centcom, we let the shuttle end the round
+ addtimer(CALLBACK(src, .proc/the_end), 1 MINUTES)
- addtimer(CALLBACK(src, .proc/the_end), 1 MINUTES)
+/**
+ * Announces the halfway point to the end.
+ */
+/datum/supermatter_delamination/proc/announce_gravitation_shift()
+ priority_announce("Reports indicate formation of crystalline seeds following resonance shift event. \
+ Rapid expansion of crystal mass proportional to rising gravitational force. \
+ Matter collapse due to gravitational pull foreseeable.",
+ "Nanotrasen Star Observation Association")
/**
- * Increases the security level to the highest level
+ * This proc manipulates the shuttle if it's enroute to centcom, to remain in hyperspace. Otherwise, it just plays an announcement if
+ * the shuttle was in any other state except stranded (idle)
*/
-/datum/supermatter_delamination/proc/delta()
- set_security_level("delta")
- sound_to_playing_players('sound/misc/notice1.ogg')
+/datum/supermatter_delamination/proc/announce_shuttle_gone()
+ // say goodbye to that shuttle of yours
+ if(SSshuttle.emergency.mode != SHUTTLE_ESCAPE)
+ priority_announce("Fatal error occurred in emergency shuttle uplink during transit. Unable to reestablish connection.",
+ "Emergency Shuttle Uplink Alert", 'sound/misc/announce_dig.ogg')
+ else
+ // except if you are on it already, then you are safe c:
+ minor_announce("ERROR: Corruption detected in navigation protocols. Connection with Transponder #XCC-P5831-ES13 lost. \
+ Backup exit route protocol decrypted. Calibrating route...",
+ "Emergency Shuttle", TRUE) // wait out until the rift on the station gets destroyed and the final message plays
+ var/list/mobs = mobs_in_area_type(list(/area/shuttle/escape))
+ for(var/mob/living/mob as anything in mobs) // emulate mob/living/lateShuttleMove() behaviour
+ if(mob.buckled)
+ continue
+ if(mob.client)
+ shake_camera(mob, 3 SECONDS * 0.25, 1)
+ mob.Paralyze(3 SECONDS, TRUE)
/**
- * Announces the last message to the station
+ * Announces the last message to the station, frees the shuttle from purgatory if applicable
*/
/datum/supermatter_delamination/proc/last_message()
- priority_announce("To the remaining survivors of [station_name()], We're sorry.", " ", 'sound/misc/bloop.ogg')
+ priority_announce("[Gibberish("All attempts at evacuation have now ceased, and all assets have been retrieved from your sector.\n \
+ To the remaining survivors of [station_name()], farewell.", FALSE, 5)]")
+
+ if(SSshuttle.emergency.mode == SHUTTLE_ESCAPE)
+ // special message for hijacks
+ var/shuttle_msg = "Navigation protocol set to [SSshuttle.emergency.is_hijacked() ? "\[ERROR\]" : "backup route"]. \
+ Reorienting bluespace vessel to exit vector. ETA 15 seconds."
+ // garble the special message
+ if(SSshuttle.emergency.is_hijacked())
+ shuttle_msg = Gibberish(shuttle_msg, TRUE, 15)
+ minor_announce(shuttle_msg, "Emergency Shuttle", TRUE)
+ SSshuttle.emergency.setTimer(15 SECONDS)
+
+/**
+ * Announce detail about the event, as well as rift location
+ */
+/datum/supermatter_delamination/proc/announce_beginning()
+ priority_announce("We have been hit by a sector-wide electromagnetic pulse. All of our systems are heavily damaged, including those \
+ required for shuttle navigation. We can only reasonably conclude that a supermatter cascade is occurring on or near your station.\n\n\
+ Evacuation is no longer possible by conventional means; however, we managed to open a rift near the [get_area_name(cascade_rift)]. \
+ All personnel are hereby required to enter the rift by any means available.\n\n\
+ [Gibberish("Retrieval of survivors will be conducted upon recovery of necessary facilities.", FALSE, 5)] \
+ [Gibberish("Good luck--", FALSE, 25)]")
/**
* Ends the round
*/
/datum/supermatter_delamination/proc/the_end()
SSticker.news_report = SUPERMATTER_CASCADE
- SSticker.force_ending = 1
+ SSticker.force_ending = TRUE
/**
* Break the lights on the station, have 35% of them be set to emergency
@@ -247,7 +342,8 @@
/datum/supermatter_delamination/proc/break_lights_on_station()
for(var/obj/machinery/light/light_to_break in GLOB.machines)
if(prob(35))
- light_to_break.emergency_mode = TRUE
- light_to_break.update_appearance()
+ light_to_break.set_major_emergency_light()
continue
light_to_break.break_light_tube()
+
+#undef MIN_RIFT_SAFE_DIST
diff --git a/code/modules/power/supermatter/supermatter_hit_procs.dm b/code/modules/power/supermatter/supermatter_hit_procs.dm
index 4ec79dcd555bb..4824e69168b1d 100644
--- a/code/modules/power/supermatter/supermatter_hit_procs.dm
+++ b/code/modules/power/supermatter/supermatter_hit_procs.dm
@@ -89,20 +89,24 @@
var/obj/item/destabilizing_crystal/destabilizing_crystal = item
if(!anomaly_event)
- to_chat(user, span_warning("You can't use \the [destabilizing_crystal] on a Shard."))
+ to_chat(user, span_warning("You can't use \the [destabilizing_crystal] on \a [name]."))
return
if(get_integrity_percent() < SUPERMATTER_CASCADE_PERCENT)
- to_chat(user, span_warning("You can only apply \the [destabilizing_crystal] to a Supermatter src that is at least [SUPERMATTER_CASCADE_PERCENT]% intact."))
+ to_chat(user, span_warning("You can only apply \the [destabilizing_crystal] to \a [name] that is at least [SUPERMATTER_CASCADE_PERCENT]% intact."))
return
- to_chat(user, span_notice("You begin to attach \the [destabilizing_crystal] to \the [src]..."))
+ to_chat(user, span_warning("You begin to attach \the [destabilizing_crystal] to \the [src]..."))
if(do_after(user, 3 SECONDS, src))
- to_chat(user, span_notice("You attach \the [destabilizing_crystal] to \the [src]."))
+ message_admins("[ADMIN_LOOKUPFLW(user)] attached [destabilizing_crystal] to the supermatter at [ADMIN_VERBOSEJMP(src)]")
+ log_game("[key_name(user)] attached [destabilizing_crystal] to the supermatter at [AREACOORD(src)]")
+ investigate_log("[key_name(user)] attached [destabilizing_crystal] to a supermatter crystal.", INVESTIGATE_ENGINE)
+ to_chat(user, span_danger("\The [destabilizing_crystal] snaps onto \the [src]."))
has_destabilizing_crystal = TRUE
cascade_initiated = TRUE
damage += 100
matter_power += 500
+ addtimer(CALLBACK(src, .proc/announce_incoming_cascade), 2 MINUTES)
qdel(destabilizing_crystal)
return
diff --git a/code/modules/power/supermatter/supermatter_process.dm b/code/modules/power/supermatter/supermatter_process.dm
index dd605c1df0a37..1633fcee76d89 100644
--- a/code/modules/power/supermatter/supermatter_process.dm
+++ b/code/modules/power/supermatter/supermatter_process.dm
@@ -69,19 +69,28 @@
//handles temperature increase and gases made by the crystal
temperature_gas_production(env, removed)
- if(check_cascade_requirements(anomaly_event))
+ var/cascading = check_cascade_requirements()
+ if(cascading)
+ if(!cascade_initiated)
+ addtimer(CALLBACK(src, .proc/announce_incoming_cascade), 2 MINUTES, TIMER_UNIQUE | TIMER_OVERRIDE)
+ log_game("[src] has begun a cascade.")
+ message_admins("[src] has begun a cascade, reasons: [cascading]. [ADMIN_JMP(src)]")
+ investigate_log("has begun a cascade, reasons: [cascading].", INVESTIGATE_ENGINE)
cascade_initiated = TRUE
if(!warp)
warp = new(src)
vis_contents += warp
animate(warp, time = 1, transform = matrix().Scale(0.5,0.5))
animate(time = 9, transform = matrix())
-
else
if(warp)
vis_contents -= warp
- warp = null
- cascade_initiated = FALSE
+ QDEL_NULL(warp)
+ if(cascade_initiated)
+ log_game("[src] has stopped its cascade.")
+ message_admins("[src] has stopped its cascade. [ADMIN_JMP(src)]")
+ investigate_log("has stopped its cascade.", INVESTIGATE_ENGINE)
+ cascade_initiated = FALSE
//handles hallucinations and the presence of a psychiatrist
psychological_examination()
@@ -89,7 +98,10 @@
//Transitions between one function and another, one we use for the fast inital startup, the other is used to prevent errors with fusion temperatures.
//Use of the second function improves the power gain imparted by using co2
if(power_changes)
- power = max(power - min(((power/500)**3) * powerloss_inhibitor, power * 0.83 * powerloss_inhibitor) * (1 - (0.2 * psyCoeff)),0)
+ ///The power that is getting lost this tick.
+ var/power_loss = power < POWERLOSS_LINEAR_THRESHOLD ? ((power / POWERLOSS_CUBIC_DIVISOR) ** 3) : (POWERLOSS_LINEAR_OFFSET + POWERLOSS_LINEAR_RATE * (power - POWERLOSS_LINEAR_THRESHOLD))
+ power_loss *= powerloss_inhibitor * (1 - (PSYCHOLOGIST_POWERLOSS_REDUCTION * psyCoeff))
+ power = max(power - power_loss, 0)
//After this point power is lowered
//This wraps around to the begining of the function
//Handle high power zaps/anomaly generation
@@ -429,19 +441,25 @@
if(combined_gas > MOLE_PENALTY_THRESHOLD)
radio.talk_into(src, "Warning: Critical coolant mass reached.", engineering_channel)
- if(check_cascade_requirements(anomaly_event))
+ if(check_cascade_requirements())
var/channel_to_talk_to = damage > emergency_point ? common_channel : engineering_channel
- radio.talk_into(src, "DANGER: RESONANCE CASCADE INITIATED.", channel_to_talk_to)
+ radio.talk_into(src, "DANGER: HYPERSTRUCTURE OSCILLATION FREQUENCY OUT OF BOUNDS.", channel_to_talk_to)
for(var/mob/victim as anything in GLOB.player_list)
var/list/messages = list(
- "You feel a strange presence in the air coming from engineering.",
- "Something is wrong, there are weird sounds coming from engineering.",
- "You don't like the smell of the SM.",
- "The SM is emitting strange noises.",
- "Crystals sounds are echoing through the station.",
+ "Space seems to be shifting around you...",
+ "You hear a high-pitched ringing sound.",
+ "You feel tingling going down your back.",
+ "Something feels very off.",
+ "A drowning sense of dread washes over you."
)
- to_chat(victim, span_boldannounce(pick(messages)))
+ to_chat(victim, span_danger(pick(messages)))
//Boom (Mind blown)
if(damage > explosion_point)
countdown()
+
+/obj/machinery/power/supermatter_crystal/proc/announce_incoming_cascade()
+ if(check_cascade_requirements())
+ priority_announce("Attention: Long range anomaly scans indicate abnormal quantities of harmonic flux originating from \
+ a subject within [station_name()], a resonance collapse may occur.",
+ "Nanotrasen Star Observation Association")
diff --git a/code/modules/power/terminal.dm b/code/modules/power/terminal.dm
index 1a3eeb8ae1d67..5cd825c12a986 100644
--- a/code/modules/power/terminal.dm
+++ b/code/modules/power/terminal.dm
@@ -43,14 +43,14 @@
if(isturf(loc))
var/turf/T = loc
if(T.underfloor_accessibility < UNDERFLOOR_INTERACTABLE)
- to_chat(user, span_warning("You must first expose the power terminal!"))
+ balloon_alert(user, "must expose the cable terminal!")
return
if(master && !master.can_terminal_dismantle())
return
- user.visible_message(span_notice("[user.name] dismantles the power terminal from [master]."),
- span_notice("You begin to cut the cables..."))
+ user.visible_message(span_notice("[user.name] dismantles the cable terminal from [master]."))
+ balloon_alert(user, "cutting the cables...")
playsound(src.loc, 'sound/items/deconstruct.ogg', 50, TRUE)
if(I.use_tool(src, user, 50))
@@ -62,7 +62,7 @@
return
new /obj/item/stack/cable_coil(drop_location(), 10)
- to_chat(user, span_notice("You cut the cables and dismantle the power terminal."))
+ balloon_alert(user, "cable terminal dismantled")
qdel(src)
/obj/machinery/power/terminal/wirecutter_act(mob/living/user, obj/item/I)
diff --git a/code/modules/projectiles/gun.dm b/code/modules/projectiles/gun.dm
index e3c7e9797171c..44de369162752 100644
--- a/code/modules/projectiles/gun.dm
+++ b/code/modules/projectiles/gun.dm
@@ -56,10 +56,6 @@
var/obj/item/firing_pin/pin = /obj/item/firing_pin //standard firing pin for most guns
/// True if a gun dosen't need a pin, mostly used for abstract guns like tentacles and meathooks
var/pinless = FALSE
- var/can_flashlight = FALSE //if a flashlight can be added or removed if it already has one.
- var/obj/item/flashlight/seclite/gun_light
- var/datum/action/item_action/toggle_gunlight/alight
- var/gunlight_state = "flight"
var/can_bayonet = FALSE //if a bayonet can be added or removed if it already has one.
var/obj/item/knife/bayonet
@@ -68,8 +64,6 @@
var/ammo_x_offset = 0 //used for positioning ammo count overlay on sprite
var/ammo_y_offset = 0
- var/flight_x_offset = 0
- var/flight_y_offset = 0
var/pb_knockback = 0
@@ -77,14 +71,12 @@
. = ..()
if(pin)
pin = new pin(src)
- if(gun_light)
- alight = new(src)
+
+ add_seclight_point()
/obj/item/gun/Destroy()
if(isobj(pin)) //Can still be the initial path, then we skip
QDEL_NULL(pin)
- if(gun_light)
- QDEL_NULL(gun_light)
if(bayonet)
QDEL_NULL(bayonet)
if(chambered) //Not all guns are chambered (EMP'ed energy guns etc)
@@ -93,6 +85,12 @@
QDEL_NULL(suppressed)
return ..()
+/// Handles adding [the seclite mount component][/datum/component/seclite_attachable] to the gun.
+/// If the gun shouldn't have a seclight mount, override this with a return.
+/// Or, if a child of a gun with a seclite mount has slightly different behavior or icons, extend this.
+/obj/item/gun/proc/add_seclight_point()
+ return
+
/obj/item/gun/handle_atom_del(atom/A)
if(A == pin)
pin = null
@@ -101,8 +99,6 @@
update_appearance()
if(A == bayonet)
clear_bayonet()
- if(A == gun_light)
- clear_gunlight()
if(A == suppressed)
clear_suppressor()
return ..()
@@ -123,13 +119,6 @@
else
. += "It doesn't have a firing pin installed, and won't fire."
- if(gun_light)
- . += "It has \a [gun_light] [can_flashlight ? "" : "permanently "]mounted on it."
- if(can_flashlight) //if it has a light and this is false, the light is permanent.
- . += span_info("[gun_light] looks like it can be unscrewed from [src].")
- else if(can_flashlight)
- . += "It has a mounting point for a seclite."
-
if(bayonet)
. += "It has \a [bayonet] [can_bayonet ? "" : "permanently "]affixed to it."
if(can_bayonet) //if it has a bayonet and this is false, the bayonet is permanent.
@@ -157,15 +146,17 @@
visible_message(span_warning("*click*"), vision_distance = COMBAT_MESSAGE_RANGE)
playsound(src, dry_fire_sound, 30, TRUE)
-
-/obj/item/gun/proc/shoot_live_shot(mob/living/user, pointblank = 0, atom/pbtarget = null, message = 1)
- if(recoil && !tk_firing(user))
- shake_camera(user, recoil + 1, recoil)
-
+/obj/item/gun/proc/fire_sounds()
if(suppressed)
playsound(src, suppressed_sound, suppressed_volume, vary_fire_sound, ignore_walls = FALSE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0)
else
playsound(src, fire_sound, fire_sound_volume, vary_fire_sound)
+
+/obj/item/gun/proc/shoot_live_shot(mob/living/user, pointblank = 0, atom/pbtarget = null, message = 1)
+ if(recoil && !tk_firing(user))
+ shake_camera(user, recoil + 1, recoil)
+ fire_sounds()
+ if(!suppressed)
if(message)
if(tk_firing(user))
visible_message(span_danger("[src] fires itself[pointblank ? " point blank at [pbtarget]!" : "!"]"), \
@@ -271,7 +262,7 @@
var/shot_leg = pick(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG)
process_fire(user, user, FALSE, null, shot_leg)
SEND_SIGNAL(user, COMSIG_MOB_CLUMSY_SHOOT_FOOT)
- if(!HAS_TRAIT(src, TRAIT_NODROP))
+ if(!tk_firing(user) && !HAS_TRAIT(src, TRAIT_NODROP))
user.dropItemToGround(src, TRUE)
return TRUE
@@ -417,19 +408,7 @@
/obj/item/gun/attackby(obj/item/I, mob/living/user, params)
if(user.combat_mode)
return ..()
- else if(istype(I, /obj/item/flashlight/seclite))
- if(!can_flashlight)
- return ..()
- var/obj/item/flashlight/seclite/S = I
- if(!gun_light)
- if(!user.transferItemToLoc(I, src))
- return
- to_chat(user, span_notice("You click [S] into place on [src]."))
- set_gun_light(S)
- update_gunlight()
- alight = new(src)
- if(loc == user)
- alight.Grant(user)
+
else if(istype(I, /obj/item/knife))
var/obj/item/knife/K = I
if(!can_bayonet || !K.bayonet || bayonet) //ensure the gun has an attachment point available, and that the knife is compatible with it.
@@ -449,20 +428,9 @@
return
if(!user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
return
- if((can_flashlight && gun_light) && (can_bayonet && bayonet)) //give them a choice instead of removing both
- var/list/possible_items = list(gun_light, bayonet)
- var/obj/item/item_to_remove = tgui_input_list(user, "Attachment to remove", "Attachment Removal", sort_names(possible_items))
- if(isnull(item_to_remove))
- return
- if(!user.canUseTopic(src, BE_CLOSE, FALSE, NO_TK))
- return
- return remove_gun_attachment(user, I, item_to_remove)
- else if(gun_light && can_flashlight) //if it has a gun_light and can_flashlight is false, the flashlight is permanently attached.
- return remove_gun_attachment(user, I, gun_light, "unscrewed")
-
- else if(bayonet && can_bayonet) //if it has a bayonet, and the bayonet can be removed
- return remove_gun_attachment(user, I, bayonet, "unfix")
+ if(bayonet && can_bayonet) //if it has a bayonet, and the bayonet can be removed
+ return remove_bayonet(user, I)
else if(pin && user.is_holding(src))
user.visible_message(span_warning("[user] attempts to remove [pin] from [src] with [I]."),
@@ -509,19 +477,15 @@
QDEL_NULL(pin)
return TRUE
-/obj/item/gun/proc/remove_gun_attachment(mob/living/user, obj/item/tool_item, obj/item/item_to_remove, removal_verb)
- if(tool_item)
- tool_item.play_tool_sound(src)
- to_chat(user, span_notice("You [removal_verb ? removal_verb : "remove"] [item_to_remove] from [src]."))
- item_to_remove.forceMove(drop_location())
+/obj/item/gun/proc/remove_bayonet(mob/living/user, obj/item/tool_item)
+ tool_item?.play_tool_sound(src)
+ to_chat(user, span_notice("You unfix [bayonet] from [src]."))
+ bayonet.forceMove(drop_location())
if(Adjacent(user) && !issilicon(user))
- user.put_in_hands(item_to_remove)
+ user.put_in_hands(bayonet)
- if(item_to_remove == bayonet)
- return clear_bayonet()
- else if(item_to_remove == gun_light)
- return clear_gunlight()
+ return clear_bayonet()
/obj/item/gun/proc/clear_bayonet()
if(!bayonet)
@@ -530,79 +494,8 @@
update_appearance()
return TRUE
-/obj/item/gun/proc/clear_gunlight()
- if(!gun_light)
- return
- var/obj/item/flashlight/seclite/removed_light = gun_light
- set_gun_light(null)
- update_gunlight()
- removed_light.update_brightness()
- QDEL_NULL(alight)
- return TRUE
-
-
-/**
- * Swaps the gun's seclight, dropping the old seclight if it has not been qdel'd.
- *
- * Returns the former gun_light that has now been replaced by this proc.
- * Arguments:
- * * new_light - The new light to attach to the weapon. Can be null, which will mean the old light is removed with no replacement.
- */
-/obj/item/gun/proc/set_gun_light(obj/item/flashlight/seclite/new_light)
- // Doesn't look like this should ever happen? We're replacing our old light with our old light?
- if(gun_light == new_light)
- CRASH("Tried to set a new gun light when the old gun light was also the new gun light.")
-
- . = gun_light
-
- // If there's an old gun light that isn't being QDELETED, detatch and drop it to the floor.
- if(!QDELETED(gun_light))
- gun_light.set_light_flags(gun_light.light_flags & ~LIGHT_ATTACHED)
- if(gun_light.loc == src)
- gun_light.forceMove(get_turf(src))
-
- // If there's a new gun light to be added, attach and move it to the gun.
- if(new_light)
- new_light.set_light_flags(new_light.light_flags | LIGHT_ATTACHED)
- if(new_light.loc != src)
- new_light.forceMove(src)
-
- gun_light = new_light
-
-/obj/item/gun/ui_action_click(mob/user, actiontype)
- if(istype(actiontype, alight))
- toggle_gunlight()
- else
- ..()
-
-/obj/item/gun/proc/toggle_gunlight()
- if(!gun_light)
- return
-
- var/mob/living/carbon/human/user = usr
- gun_light.on = !gun_light.on
- gun_light.update_brightness()
- to_chat(user, span_notice("You toggle the gunlight [gun_light.on ? "on":"off"]."))
-
- playsound(user, 'sound/weapons/empty.ogg', 100, TRUE)
- update_gunlight()
-
-/obj/item/gun/proc/update_gunlight()
- update_appearance()
- update_action_buttons()
-
/obj/item/gun/update_overlays()
. = ..()
- if(gun_light)
- var/mutable_appearance/flashlight_overlay
- var/state = "[gunlight_state][gun_light.on? "_on":""]" //Generic state.
- if(gun_light.icon_state in icon_states('icons/obj/guns/flashlights.dmi')) //Snowflake state?
- state = gun_light.icon_state
- flashlight_overlay = mutable_appearance('icons/obj/guns/flashlights.dmi', state)
- flashlight_overlay.pixel_x = flight_x_offset
- flashlight_overlay.pixel_y = flight_y_offset
- . += flashlight_overlay
-
if(bayonet)
var/mutable_appearance/knife_overlay
var/state = "bayonet" //Generic state.
diff --git a/code/modules/projectiles/guns/ballistic.dm b/code/modules/projectiles/guns/ballistic.dm
index 55120800375b1..9e03e1035ef3f 100644
--- a/code/modules/projectiles/guns/ballistic.dm
+++ b/code/modules/projectiles/guns/ballistic.dm
@@ -136,6 +136,20 @@
/obj/item/gun/ballistic/add_weapon_description()
AddElement(/datum/element/weapon_description, attached_proc = .proc/add_notes_ballistic)
+/obj/item/gun/ballistic/fire_sounds()
+ var/frequency_to_use = sin((90/magazine?.max_ammo) * get_ammo())
+ var/click_frequency_to_use = 1 - frequency_to_use * 0.75
+ var/play_click = round(sqrt(magazine?.max_ammo * 2)) > get_ammo()
+ if(suppressed)
+ playsound(src, suppressed_sound, suppressed_volume, vary_fire_sound, ignore_walls = FALSE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0)
+ if(play_click)
+ playsound(src, 'sound/weapons/gun/general/ballistic_click.ogg', suppressed_volume, vary_fire_sound, ignore_walls = FALSE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0, frequency = click_frequency_to_use)
+ else
+ playsound(src, fire_sound, fire_sound_volume, vary_fire_sound)
+ if(play_click)
+ playsound(src, 'sound/weapons/gun/general/ballistic_click.ogg', fire_sound_volume, vary_fire_sound, frequency = click_frequency_to_use)
+
+
/**
*
* Outputs type-specific weapon stats for ballistic weaponry based on its magazine and its caliber.
diff --git a/code/modules/projectiles/guns/ballistic/automatic.dm b/code/modules/projectiles/guns/ballistic/automatic.dm
index f11bc640c47a4..aa8481a1d9b64 100644
--- a/code/modules/projectiles/guns/ballistic/automatic.dm
+++ b/code/modules/projectiles/guns/ballistic/automatic.dm
@@ -7,7 +7,6 @@
semi_auto = TRUE
fire_sound = 'sound/weapons/gun/smg/shot.ogg'
fire_sound_volume = 90
- vary_fire_sound = FALSE
rack_sound = 'sound/weapons/gun/smg/smgrack.ogg'
suppressed_sound = 'sound/weapons/gun/smg/shot_suppressed.ogg'
var/select = 1 ///fire selector position. 1 = semi, 2 = burst. anything past that can vary between guns.
@@ -340,7 +339,6 @@
worn_icon_state = null
fire_sound = 'sound/weapons/gun/sniper/shot.ogg'
fire_sound_volume = 90
- vary_fire_sound = FALSE
load_sound = 'sound/weapons/gun/sniper/mag_insert.ogg'
rack_sound = 'sound/weapons/gun/sniper/rack.ogg'
suppressed_sound = 'sound/weapons/gun/general/heavy_shot_suppressed.ogg'
diff --git a/code/modules/projectiles/guns/ballistic/pistol.dm b/code/modules/projectiles/guns/ballistic/pistol.dm
index ffdd1fef73319..8644dcde2a069 100644
--- a/code/modules/projectiles/guns/ballistic/pistol.dm
+++ b/code/modules/projectiles/guns/ballistic/pistol.dm
@@ -16,7 +16,6 @@
load_empty_sound = 'sound/weapons/gun/pistol/mag_insert.ogg'
eject_sound = 'sound/weapons/gun/pistol/mag_release.ogg'
eject_empty_sound = 'sound/weapons/gun/pistol/mag_release.ogg'
- vary_fire_sound = FALSE
rack_sound = 'sound/weapons/gun/pistol/rack_small.ogg'
lock_back_sound = 'sound/weapons/gun/pistol/lock_small.ogg'
bolt_drop_sound = 'sound/weapons/gun/pistol/drop_small.ogg'
diff --git a/code/modules/projectiles/guns/ballistic/revolver.dm b/code/modules/projectiles/guns/ballistic/revolver.dm
index bd3a0322182e4..431e6f95621df 100644
--- a/code/modules/projectiles/guns/ballistic/revolver.dm
+++ b/code/modules/projectiles/guns/ballistic/revolver.dm
@@ -6,7 +6,6 @@
fire_sound = 'sound/weapons/gun/revolver/shot_alt.ogg'
load_sound = 'sound/weapons/gun/revolver/load_bullet.ogg'
eject_sound = 'sound/weapons/gun/revolver/empty.ogg'
- vary_fire_sound = FALSE
fire_sound_volume = 90
dry_fire_sound = 'sound/weapons/gun/revolver/dry_fire.ogg'
casing_ejector = FALSE
@@ -38,6 +37,20 @@
..()
spin()
+/obj/item/gun/ballistic/revolver/fire_sounds()
+ var/frequency_to_use = sin((90/magazine?.max_ammo) * get_ammo(TRUE, FALSE)) // fucking REVOLVERS
+ var/click_frequency_to_use = 1 - frequency_to_use * 0.75
+ var/play_click = sqrt(magazine?.max_ammo) > get_ammo(TRUE, FALSE)
+ if(suppressed)
+ playsound(src, suppressed_sound, suppressed_volume, vary_fire_sound, ignore_walls = FALSE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0)
+ if(play_click)
+ playsound(src, 'sound/weapons/gun/general/ballistic_click.ogg', suppressed_volume, vary_fire_sound, ignore_walls = FALSE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0, frequency = click_frequency_to_use)
+ else
+ playsound(src, fire_sound, fire_sound_volume, vary_fire_sound)
+ if(play_click)
+ playsound(src, 'sound/weapons/gun/general/ballistic_click.ogg', fire_sound_volume, vary_fire_sound, frequency = click_frequency_to_use)
+
+
/obj/item/gun/ballistic/revolver/verb/spin()
set name = "Spin Chamber"
set category = "Object"
@@ -188,19 +201,42 @@
spun = FALSE
+ var/zone = check_zone(user.zone_selected)
+ var/obj/item/bodypart/affecting = H.get_bodypart(zone)
+ var/is_target_face = zone == BODY_ZONE_HEAD || zone == BODY_ZONE_PRECISE_EYES || zone == BODY_ZONE_PRECISE_MOUTH
+ var/loaded_rounds = get_ammo(FALSE, FALSE) // check before it is fired
+
+ if(loaded_rounds && is_target_face)
+ add_memory_in_range(
+ user,
+ 7,
+ MEMORY_RUSSIAN_ROULETTE,
+ list(
+ DETAIL_PROTAGONIST = user,
+ DETAIL_LOADED_ROUNDS = loaded_rounds,
+ DETAIL_BODYPART = affecting.name,
+ DETAIL_OUTCOME = (chambered ? "lost" : "won")
+ ),
+ story_value = chambered ? STORY_VALUE_SHIT : max(STORY_VALUE_NONE, loaded_rounds), // the more bullets, the greater the story (but losing is always SHIT)
+ memory_flags = MEMORY_CHECK_BLINDNESS,
+ protagonist_memory_flags = NONE
+ )
+
if(chambered)
var/obj/item/ammo_casing/AC = chambered
if(AC.fire_casing(user, user, params, distro = 0, quiet = 0, zone_override = null, spread = 0, fired_from = src))
playsound(user, fire_sound, fire_sound_volume, vary_fire_sound)
- var/zone = check_zone(user.zone_selected)
- var/obj/item/bodypart/affecting = H.get_bodypart(zone)
- if(zone == BODY_ZONE_HEAD || zone == BODY_ZONE_PRECISE_EYES || zone == BODY_ZONE_PRECISE_MOUTH)
+ if(is_target_face)
shoot_self(user, affecting)
else
user.visible_message(span_danger("[user.name] cowardly fires [src] at [user.p_their()] [affecting.name]!"), span_userdanger("You cowardly fire [src] at your [affecting.name]!"), span_hear("You hear a gunshot!"))
chambered = null
+ SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "russian_roulette_lose", /datum/mood_event/russian_roulette_lose)
return
+ if(loaded_rounds && is_target_face)
+ SEND_SIGNAL(user, COMSIG_ADD_MOOD_EVENT, "russian_roulette_win", /datum/mood_event/russian_roulette_win, loaded_rounds)
+
user.visible_message(span_danger("*click*"))
playsound(src, dry_fire_sound, 30, TRUE)
diff --git a/code/modules/projectiles/guns/ballistic/rifle.dm b/code/modules/projectiles/guns/ballistic/rifle.dm
index 2e1adf7e0000f..46c343c2493a7 100644
--- a/code/modules/projectiles/guns/ballistic/rifle.dm
+++ b/code/modules/projectiles/guns/ballistic/rifle.dm
@@ -12,7 +12,6 @@
internal_magazine = TRUE
fire_sound = 'sound/weapons/gun/rifle/shot.ogg'
fire_sound_volume = 90
- vary_fire_sound = FALSE
rack_sound = 'sound/weapons/gun/rifle/bolt_out.ogg'
bolt_drop_sound = 'sound/weapons/gun/rifle/bolt_in.ogg'
tac_reloads = FALSE
diff --git a/code/modules/projectiles/guns/ballistic/shotgun.dm b/code/modules/projectiles/guns/ballistic/shotgun.dm
index 67d0048681080..6afdd56f353a0 100644
--- a/code/modules/projectiles/guns/ballistic/shotgun.dm
+++ b/code/modules/projectiles/guns/ballistic/shotgun.dm
@@ -9,7 +9,6 @@
inhand_x_dimension = 64
inhand_y_dimension = 64
fire_sound = 'sound/weapons/gun/shotgun/shot.ogg'
- vary_fire_sound = FALSE
fire_sound_volume = 90
rack_sound = 'sound/weapons/gun/shotgun/rack.ogg'
load_sound = 'sound/weapons/gun/shotgun/insert_shell.ogg'
diff --git a/code/modules/projectiles/guns/energy.dm b/code/modules/projectiles/guns/energy.dm
index 5d959c5265bfe..951a25685388d 100644
--- a/code/modules/projectiles/guns/energy.dm
+++ b/code/modules/projectiles/guns/energy.dm
@@ -34,6 +34,29 @@
///set to true so the gun is given an empty cell
var/dead_cell = FALSE
+/obj/item/gun/energy/fire_sounds()
+ var/obj/item/ammo_casing/energy/shot = ammo_type[select]
+ var/batt_percent = FLOOR(clamp(cell.charge / cell.maxcharge, 0, 1) * 100, 1)
+ // What percentage of the full battery a shot will expend
+ var/shot_cost_percent = 0
+ // The total amount of shots the fully charged energy gun can fire before running out
+ var/max_shots = 0
+ // How many shots left before the energy gun's current battery runs out of energy
+ var/shots_left = 0
+ // What frequency the energy gun's sound will make
+ var/frequency_to_use = 0
+
+ if(shot.e_cost > 0)
+ shot_cost_percent = FLOOR(clamp(shot.e_cost / cell.maxcharge, 0, 1) * 100, 1)
+ max_shots = round(100/shot_cost_percent)
+ shots_left = round(batt_percent/shot_cost_percent)
+ frequency_to_use = sin((90/max_shots) * shots_left)
+
+ if(suppressed)
+ playsound(src, suppressed_sound, suppressed_volume, vary_fire_sound, ignore_walls = FALSE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0, frequency = frequency_to_use)
+ else
+ playsound(src, fire_sound, fire_sound_volume, vary_fire_sound, frequency = frequency_to_use)
+
/obj/item/gun/energy/emp_act(severity)
. = ..()
if(!(. & EMP_PROTECT_CONTENTS))
@@ -59,6 +82,7 @@
START_PROCESSING(SSobj, src)
update_appearance()
RegisterSignal(src, COMSIG_ITEM_RECHARGED, .proc/instant_recharge)
+ AddElement(/datum/element/update_icon_updates_onmob)
/obj/item/gun/energy/add_weapon_description()
AddElement(/datum/element/weapon_description, attached_proc = .proc/add_notes_energy)
@@ -92,10 +116,6 @@
return readout.Join("\n") // Sending over the singular string, rather than the whole list
-/obj/item/gun/energy/ComponentInitialize()
- . = ..()
- AddElement(/datum/element/update_icon_updates_onmob)
-
/obj/item/gun/energy/proc/update_ammo_types()
var/obj/item/ammo_casing/energy/shot
for (var/i in 1 to ammo_type.len)
@@ -214,8 +234,8 @@
if(modifystate)
var/obj/item/ammo_casing/energy/shot = ammo_type[select]
if(single_shot_type_overlay)
- . += "[icon_state]_[shot.select_name]"
- overlay_icon_state += "_[shot.select_name]"
+ . += "[icon_state]_[initial(shot.select_name)]"
+ overlay_icon_state += "_[initial(shot.select_name)]"
var/ratio = get_charge_ratio()
if(ratio == 0 && display_empty)
diff --git a/code/modules/projectiles/guns/energy/beam_rifle.dm b/code/modules/projectiles/guns/energy/beam_rifle.dm
index b1a5219f2c950..c5a69e323a942 100644
--- a/code/modules/projectiles/guns/energy/beam_rifle.dm
+++ b/code/modules/projectiles/guns/energy/beam_rifle.dm
@@ -29,6 +29,7 @@
weapon_weight = WEAPON_HEAVY
w_class = WEIGHT_CLASS_BULKY
ammo_type = list(/obj/item/ammo_casing/energy/beam_rifle/hitscan)
+ actions_types = list(/datum/action/item_action/zoom_lock_action)
cell_type = /obj/item/stock_parts/cell/beam_rifle
canMouseDown = TRUE
var/aiming = FALSE
@@ -72,7 +73,6 @@
var/current_zoom_x = 0
var/current_zoom_y = 0
- var/datum/action/item_action/zoom_lock_action/zoom_lock_action
var/mob/listeningTo
/obj/item/gun/energy/beam_rifle/debug
@@ -95,7 +95,7 @@
return ..()
/obj/item/gun/energy/beam_rifle/ui_action_click(mob/user, actiontype)
- if(istype(actiontype, zoom_lock_action))
+ if(istype(actiontype, /datum/action/item_action/zoom_lock_action))
zoom_lock++
if(zoom_lock > 3)
zoom_lock = 0
@@ -109,8 +109,9 @@
if(ZOOM_LOCK_OFF)
to_chat(user, span_boldnotice("You disable [src]'s zooming system."))
reset_zooming()
- else
- ..()
+ return
+
+ return ..()
/obj/item/gun/energy/beam_rifle/proc/set_autozoom_pixel_offsets_immediate(current_angle)
if(zoom_lock == ZOOM_LOCK_CENTER_VIEW || zoom_lock == ZOOM_LOCK_OFF)
@@ -162,7 +163,6 @@
fire_delay = delay
current_tracers = list()
START_PROCESSING(SSfastprocess, src)
- zoom_lock_action = new(src)
/obj/item/gun/energy/beam_rifle/Destroy()
STOP_PROCESSING(SSfastprocess, src)
diff --git a/code/modules/projectiles/guns/energy/energy_gun.dm b/code/modules/projectiles/guns/energy/energy_gun.dm
index 6e2f701bd94cc..110d4d6ef6135 100644
--- a/code/modules/projectiles/guns/energy/energy_gun.dm
+++ b/code/modules/projectiles/guns/energy/energy_gun.dm
@@ -6,12 +6,16 @@
inhand_icon_state = null //so the human update icon uses the icon_state instead.
ammo_type = list(/obj/item/ammo_casing/energy/disabler, /obj/item/ammo_casing/energy/laser)
modifystate = TRUE
- can_flashlight = TRUE
ammo_x_offset = 3
- flight_x_offset = 15
- flight_y_offset = 10
dual_wield_spread = 60
+/obj/item/gun/energy/e_gun/add_seclight_point()
+ AddComponent(/datum/component/seclite_attachable, \
+ light_overlay_icon = 'icons/obj/guns/flashlights.dmi', \
+ light_overlay = "flight", \
+ overlay_x = 15, \
+ overlay_y = 10)
+
/obj/item/gun/energy/e_gun/mini
name = "miniature energy gun"
desc = "A small, pistol-sized energy gun with a built-in flashlight. It has two settings: disable and kill."
@@ -21,15 +25,17 @@
cell_type = /obj/item/stock_parts/cell/mini_egun
ammo_x_offset = 2
charge_sections = 3
- can_flashlight = FALSE // Can't attach or detach the flashlight, and override it's icon update
- gunlight_state = "mini-light"
- flight_x_offset = 19
- flight_y_offset = 13
single_shot_type_overlay = FALSE
-/obj/item/gun/energy/e_gun/mini/Initialize(mapload)
- set_gun_light(new /obj/item/flashlight/seclite(src))
- return ..()
+/obj/item/gun/energy/e_gun/mini/add_seclight_point()
+ // The mini energy gun's light comes attached but is unremovable.
+ AddComponent(/datum/component/seclite_attachable, \
+ starting_light = new /obj/item/flashlight/seclite(src), \
+ is_light_removable = FALSE, \
+ light_overlay_icon = 'icons/obj/guns/flashlights.dmi', \
+ light_overlay = "mini-light", \
+ overlay_x = 19, \
+ overlay_y = 13)
/obj/item/gun/energy/e_gun/stun
name = "tactical energy gun"
@@ -74,9 +80,11 @@
ammo_type = list(/obj/item/ammo_casing/energy/net, /obj/item/ammo_casing/energy/trap)
modifystate = FALSE
w_class = WEIGHT_CLASS_NORMAL
- can_flashlight = FALSE
ammo_x_offset = 1
+/obj/item/gun/energy/e_gun/dragnet/add_seclight_point()
+ return
+
/obj/item/gun/energy/e_gun/dragnet/snare
name = "Energy Snare Launcher"
desc = "Fires an energy snare that slows the target down."
@@ -91,10 +99,12 @@
w_class = WEIGHT_CLASS_HUGE
ammo_type = list(/obj/item/ammo_casing/energy/electrode, /obj/item/ammo_casing/energy/laser)
weapon_weight = WEAPON_HEAVY
- can_flashlight = FALSE
trigger_guard = TRIGGER_GUARD_NONE
ammo_x_offset = 2
+/obj/item/gun/energy/e_gun/turret/add_seclight_point()
+ return
+
/obj/item/gun/energy/e_gun/nuclear
name = "advanced energy gun"
desc = "An energy gun with an experimental miniaturized nuclear reactor that automatically charges the internal power cell."
diff --git a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
index a6e7398500154..eb07b8b1728bd 100644
--- a/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
+++ b/code/modules/projectiles/guns/energy/kinetic_accelerator.dm
@@ -8,9 +8,6 @@
item_flags = NONE
obj_flags = UNIQUE_RENAME
weapon_weight = WEAPON_LIGHT
- can_flashlight = TRUE
- flight_x_offset = 15
- flight_y_offset = 9
can_bayonet = TRUE
knife_x_offset = 20
knife_y_offset = 12
@@ -18,6 +15,13 @@
var/max_mod_capacity = 100
var/list/modkits = list()
+/obj/item/gun/energy/recharge/kinetic_accelerator/add_seclight_point()
+ AddComponent(/datum/component/seclite_attachable, \
+ light_overlay_icon = 'icons/obj/guns/flashlights.dmi', \
+ light_overlay = "flight", \
+ overlay_x = 15, \
+ overlay_y = 9)
+
/obj/item/gun/energy/recharge/kinetic_accelerator/examine(mob/user)
. = ..()
if(max_mod_capacity)
diff --git a/code/modules/projectiles/guns/energy/laser.dm b/code/modules/projectiles/guns/energy/laser.dm
index 3f401dda4b758..447c65e17ee54 100644
--- a/code/modules/projectiles/guns/energy/laser.dm
+++ b/code/modules/projectiles/guns/energy/laser.dm
@@ -163,19 +163,23 @@
ammo_type = list(/obj/item/ammo_casing/energy/nanite)
shaded_charge = TRUE
ammo_x_offset = 1
- can_flashlight = TRUE
- flight_x_offset = 15
- flight_y_offset = 9
can_bayonet = TRUE
knife_x_offset = 19
knife_y_offset = 13
w_class = WEIGHT_CLASS_NORMAL
dual_wield_spread = 10 //as intended by the coders
-/obj/item/gun/energy/laser/thermal/ComponentInitialize()
+/obj/item/gun/energy/laser/thermal/Initialize(mapload)
. = ..()
AddElement(/datum/element/empprotection, EMP_PROTECT_SELF|EMP_PROTECT_CONTENTS)
+/obj/item/gun/energy/laser/thermal/add_seclight_point()
+ AddComponent(/datum/component/seclite_attachable, \
+ light_overlay_icon = 'icons/obj/guns/flashlights.dmi', \
+ light_overlay = "flight", \
+ overlay_x = 15, \
+ overlay_y = 9)
+
/obj/item/gun/energy/laser/thermal/inferno //the magma gun
name = "inferno pistol"
desc = "A modified handcannon with a self-replicating reserve of decommissioned weaponized nanites. Spit globs of molten angry robots into the bad guys. While it doesn't manipulate temperature in of itself, it does cause an violent eruption in anyone who is severely cold."
diff --git a/code/modules/projectiles/guns/energy/mounted.dm b/code/modules/projectiles/guns/energy/mounted.dm
index c6d8df7321fba..c553d4edbb63b 100644
--- a/code/modules/projectiles/guns/energy/mounted.dm
+++ b/code/modules/projectiles/guns/energy/mounted.dm
@@ -7,11 +7,10 @@
display_empty = FALSE
force = 5
selfcharge = 1
- can_flashlight = FALSE
trigger_guard = TRIGGER_GUARD_ALLOW_ALL // Has no trigger at all, uses neural signals instead
-/obj/item/gun/energy/e_gun/advtaser/mounted/dropped()//if somebody manages to drop this somehow...
- ..()
+/obj/item/gun/energy/e_gun/advtaser/mounted/add_seclight_point()
+ return
/obj/item/gun/energy/laser/mounted
name = "mounted laser"
@@ -23,9 +22,6 @@
selfcharge = 1
trigger_guard = TRIGGER_GUARD_ALLOW_ALL
-/obj/item/gun/energy/laser/mounted/dropped()
- ..()
-
/obj/item/gun/energy/laser/mounted/augment
icon = 'icons/obj/surgery.dmi'
icon_state = "arm_laser"
diff --git a/code/modules/projectiles/guns/energy/pulse.dm b/code/modules/projectiles/guns/energy/pulse.dm
index 9f90b69da74ca..d53edc8a52db4 100644
--- a/code/modules/projectiles/guns/energy/pulse.dm
+++ b/code/modules/projectiles/guns/energy/pulse.dm
@@ -40,9 +40,13 @@
worn_icon_state = "gun"
inhand_icon_state = null
cell_type = "/obj/item/stock_parts/cell/pulse/carbine"
- can_flashlight = TRUE
- flight_x_offset = 18
- flight_y_offset = 12
+
+/obj/item/gun/energy/pulse/carbine/add_seclight_point()
+ AddComponent(/datum/component/seclite_attachable, \
+ light_overlay_icon = 'icons/obj/guns/flashlights.dmi', \
+ light_overlay = "flight", \
+ overlay_x = 18, \
+ overlay_y = 12)
/obj/item/gun/energy/pulse/carbine/loyalpin
pin = /obj/item/firing_pin/implant/mindshield
diff --git a/code/modules/projectiles/guns/energy/special.dm b/code/modules/projectiles/guns/energy/special.dm
index 663d9319804a9..12ad379f4c4d0 100644
--- a/code/modules/projectiles/guns/energy/special.dm
+++ b/code/modules/projectiles/guns/energy/special.dm
@@ -5,13 +5,17 @@
inhand_icon_state = null //so the human update icon uses the icon_state instead.
worn_icon_state = null
shaded_charge = TRUE
- can_flashlight = TRUE
w_class = WEIGHT_CLASS_HUGE
flags_1 = CONDUCT_1
slot_flags = ITEM_SLOT_BACK
ammo_type = list(/obj/item/ammo_casing/energy/ion)
- flight_x_offset = 17
- flight_y_offset = 9
+
+/obj/item/gun/energy/ionrifle/add_seclight_point()
+ AddComponent(/datum/component/seclite_attachable, \
+ light_overlay_icon = 'icons/obj/guns/flashlights.dmi', \
+ light_overlay = "flight", \
+ overlay_x = 17, \
+ overlay_y = 9)
/obj/item/gun/energy/ionrifle/emp_act(severity)
return
@@ -22,8 +26,11 @@
icon_state = "ioncarbine"
w_class = WEIGHT_CLASS_BULKY
slot_flags = ITEM_SLOT_BELT
- flight_x_offset = 18
- flight_y_offset = 11
+
+/obj/item/gun/energy/ionrifle/carbine/add_seclight_point()
+ . = ..()
+ // We use the same overlay as the parent, so we can just let the component inherit the correct offsets here
+ AddComponent(/datum/component/seclite_attachable, overlay_x = 18, overlay_y = 11)
/obj/item/gun/energy/decloner
name = "biological demolecularisor"
@@ -160,8 +167,11 @@
return (!QDELETED(cell) && cell.use(amount ? amount * charge_weld : charge_weld))
/obj/item/gun/energy/plasmacutter/use_tool(atom/target, mob/living/user, delay, amount=1, volume=0, datum/callback/extra_checks)
+
if(amount)
+ target.add_overlay(GLOB.welding_sparks)
. = ..()
+ target.cut_overlay(GLOB.welding_sparks)
else
. = ..(amount=1)
diff --git a/code/modules/projectiles/guns/energy/stun.dm b/code/modules/projectiles/guns/energy/stun.dm
index ae5f1a4131a97..627c8b5e8933d 100644
--- a/code/modules/projectiles/guns/energy/stun.dm
+++ b/code/modules/projectiles/guns/energy/stun.dm
@@ -16,10 +16,12 @@
/obj/item/gun/energy/e_gun/advtaser/cyborg
name = "cyborg taser"
desc = "An integrated hybrid taser that draws directly from a cyborg's power cell. The weapon contains a limiter to prevent the cyborg's power cell from overheating."
- can_flashlight = FALSE
can_charge = FALSE
use_cyborg_cell = TRUE
+/obj/item/gun/energy/e_gun/advtaser/cyborg/add_seclight_point()
+ return
+
/obj/item/gun/energy/e_gun/advtaser/cyborg/emp_act()
return
@@ -30,9 +32,13 @@
inhand_icon_state = null
ammo_type = list(/obj/item/ammo_casing/energy/disabler)
ammo_x_offset = 2
- can_flashlight = TRUE
- flight_x_offset = 15
- flight_y_offset = 10
+
+/obj/item/gun/energy/disabler/add_seclight_point()
+ AddComponent(/datum/component/seclite_attachable, \
+ light_overlay_icon = 'icons/obj/guns/flashlights.dmi', \
+ light_overlay = "flight", \
+ overlay_x = 15, \
+ overlay_y = 10)
/obj/item/gun/energy/disabler/cyborg
name = "cyborg disabler"
diff --git a/code/modules/projectiles/guns/magic.dm b/code/modules/projectiles/guns/magic.dm
index 808d956a02a10..5ed5c643e7fd4 100644
--- a/code/modules/projectiles/guns/magic.dm
+++ b/code/modules/projectiles/guns/magic.dm
@@ -29,12 +29,20 @@
. = ..()
RegisterSignal(src, COMSIG_ITEM_MAGICALLY_CHARGED, .proc/on_magic_charge)
+
+/obj/item/gun/magic/fire_sounds()
+ var/frequency_to_use = sin((90/max_charges) * charges)
+ if(suppressed)
+ playsound(src, suppressed_sound, suppressed_volume, vary_fire_sound, ignore_walls = FALSE, extrarange = SILENCED_SOUND_EXTRARANGE, falloff_distance = 0, frequency = frequency_to_use)
+ else
+ playsound(src, fire_sound, fire_sound_volume, vary_fire_sound, frequency = frequency_to_use)
+
/**
* Signal proc for [COMSIG_ITEM_MAGICALLY_CHARGED]
*
* Adds uses to wands or staffs.
*/
-/obj/item/gun/magic/proc/on_magic_charge(datum/source, obj/effect/proc_holder/spell/targeted/charge/spell, mob/living/caster)
+/obj/item/gun/magic/proc/on_magic_charge(datum/source, datum/action/cooldown/spell/charge/spell, mob/living/caster)
SIGNAL_HANDLER
. = COMPONENT_ITEM_CHARGED
diff --git a/code/modules/projectiles/guns/magic/staff.dm b/code/modules/projectiles/guns/magic/staff.dm
index 544a67c0cd5b6..8ef57a4b9eab8 100644
--- a/code/modules/projectiles/guns/magic/staff.dm
+++ b/code/modules/projectiles/guns/magic/staff.dm
@@ -11,14 +11,12 @@
/obj/item/gun/magic/staff/proc/is_wizard_or_friend(mob/user)
if(!user?.mind?.has_antag_datum(/datum/antagonist/wizard) \
&& !user.mind.has_antag_datum(/datum/antagonist/survivalist/magic) \
- && !user.mind.has_antag_datum(/datum/antagonist/wizard_minion))
+ && !user.mind.has_antag_datum(/datum/antagonist/wizard_minion) \
+ && !allow_intruder_use)
return FALSE
return TRUE
/obj/item/gun/magic/staff/check_botched(mob/living/user, atom/target)
- if(allow_intruder_use)
- return ..()
-
if(!is_wizard_or_friend(user))
return !on_intruder_use(user, target)
return ..()
diff --git a/code/modules/projectiles/guns/magic/wand.dm b/code/modules/projectiles/guns/magic/wand.dm
index b06cd6116dbbf..757c89b994baa 100644
--- a/code/modules/projectiles/guns/magic/wand.dm
+++ b/code/modules/projectiles/guns/magic/wand.dm
@@ -82,7 +82,7 @@
to_chat(user, span_notice("You feel great!"))
return
to_chat(user, "You irradiate yourself with pure negative energy! \
- [pick("Do not pass go. Do not collect 200 zorkmids.","You feel more confident in your spell casting skills.","You Die...","Do you want your possessions identified?")]\
+ [pick("Do not pass go. Do not collect 200 zorkmids.","You feel more confident in your spell casting skills.","You die...","Do you want your possessions identified?")]\
")
user.death(FALSE)
@@ -118,7 +118,7 @@
var/mob/living/L = user
if(L.mob_biotypes & MOB_UNDEAD) //positive energy harms the undead
to_chat(user, "You irradiate yourself with pure positive energy! \
- [pick("Do not pass go. Do not collect 200 zorkmids.","You feel more confident in your spell casting skills.","You Die...","Do you want your possessions identified?")]\
+ [pick("Do not pass go. Do not collect 200 zorkmids.","You feel more confident in your spell casting skills.","You die...","Do you want your possessions identified?")]\
")
user.death(0)
return
@@ -170,7 +170,7 @@
/obj/item/gun/magic/wand/teleport/zap_self(mob/living/user)
if(do_teleport(user, user, 10, channel = TELEPORT_CHANNEL_MAGIC))
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(3, location = user.loc)
+ smoke.set_up(3, holder = src, location = user.loc)
smoke.start()
charges--
..()
@@ -193,7 +193,7 @@
if(do_teleport(user, destination, channel=TELEPORT_CHANNEL_MAGIC))
for(var/t in list(origin, destination))
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(0, location = t)
+ smoke.set_up(0, holder = src, location = t)
smoke.start()
..()
diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm
index aecad164c0125..a0fdba798c40c 100644
--- a/code/modules/projectiles/projectile.dm
+++ b/code/modules/projectiles/projectile.dm
@@ -183,6 +183,8 @@
var/static/list/projectile_connections = list(
COMSIG_ATOM_ENTERED = .proc/on_entered,
)
+ /// If true directly targeted turfs can be hit
+ var/can_hit_turfs = FALSE
/obj/projectile/Initialize(mapload)
. = ..()
@@ -464,7 +466,7 @@
*/
/obj/projectile/proc/select_target(turf/our_turf, atom/target, atom/bumped)
// 1. special bumped border object check
- if(bumped?.flags_1 & ON_BORDER_1)
+ if((bumped?.flags_1 & ON_BORDER_1) && can_hit_target(bumped, original == bumped, FALSE, TRUE))
return bumped
// 2. original
if(can_hit_target(original, TRUE, FALSE, original == bumped))
@@ -494,7 +496,7 @@
/obj/projectile/proc/can_hit_target(atom/target, direct_target = FALSE, ignore_loc = FALSE, cross_failed = FALSE)
if(QDELETED(target) || impacted[target])
return FALSE
- if(!ignore_loc && (loc != target.loc))
+ if(!ignore_loc && (loc != target.loc) && !(can_hit_turfs && direct_target && loc == target))
return FALSE
// if pass_flags match, pass through entirely - unless direct target is set.
if((target.pass_flags_self & pass_flags) && !direct_target)
@@ -511,7 +513,7 @@
return TRUE
if(!isliving(target))
if(isturf(target)) // non dense turfs
- return FALSE
+ return can_hit_turfs && direct_target
if(target.layer < hit_threshhold)
return FALSE
else if(!direct_target) // non dense objects do not get hit unless specifically clicked
diff --git a/code/modules/projectiles/projectile/bullets/shotgun.dm b/code/modules/projectiles/projectile/bullets/shotgun.dm
index 3420667024c13..eefc2f798f4f5 100644
--- a/code/modules/projectiles/projectile/bullets/shotgun.dm
+++ b/code/modules/projectiles/projectile/bullets/shotgun.dm
@@ -1,5 +1,6 @@
/obj/projectile/bullet/shotgun_slug
name = "12g shotgun slug"
+ icon_state = "pellet"
damage = 50
sharpness = SHARP_POINTY
wound_bonus = 0
@@ -16,6 +17,7 @@
/obj/projectile/bullet/shotgun_beanbag
name = "beanbag slug"
+ icon_state = "pellet"
damage = 10
stamina = 55
wound_bonus = 20
@@ -24,6 +26,7 @@
/obj/projectile/bullet/incendiary/shotgun
name = "incendiary slug"
+ icon_state = "pellet"
damage = 20
/obj/projectile/bullet/incendiary/shotgun/no_trail
@@ -68,6 +71,7 @@
/obj/projectile/bullet/shotgun_frag12
name ="frag12 slug"
+ icon_state = "pellet"
damage = 15
paralyze = 10
@@ -77,6 +81,7 @@
return BULLET_ACT_HIT
/obj/projectile/bullet/pellet
+ icon_state = "pellet"
var/tile_dropoff = 0.45
var/tile_dropoff_s = 0.25
@@ -141,4 +146,5 @@
// Mech Scattershot
/obj/projectile/bullet/scattershot
+ icon_state = "pellet"
damage = 24
diff --git a/code/modules/projectiles/projectile/magic.dm b/code/modules/projectiles/projectile/magic.dm
index 49ca968554f86..6fad0956c4a25 100644
--- a/code/modules/projectiles/projectile/magic.dm
+++ b/code/modules/projectiles/projectile/magic.dm
@@ -11,30 +11,47 @@
/// determines the drain cost on the antimagic item
var/antimagic_charge_cost = 1
-/obj/projectile/magic/prehit_pierce(mob/living/target)
+/obj/projectile/magic/prehit_pierce(atom/target)
. = ..()
- if(istype(target) && target.can_block_magic(antimagic_flags, antimagic_charge_cost))
- visible_message(span_warning("[src] fizzles on contact with [target]!"))
- return PROJECTILE_DELETE_WITHOUT_HITTING
+
+ if(isliving(target))
+ var/mob/living/victim = target
+ if(victim.can_block_magic(antimagic_flags, antimagic_charge_cost))
+ visible_message(span_warning("[src] fizzles on contact with [victim]!"))
+ return PROJECTILE_DELETE_WITHOUT_HITTING
+
+ if(istype(target, /obj/machinery/hydroponics)) // even plants can block antimagic
+ var/obj/machinery/hydroponics/plant_tray = target
+ if(!plant_tray.myseed)
+ return
+ if(plant_tray.myseed.get_gene(/datum/plant_gene/trait/anti_magic))
+ visible_message(span_warning("[src] fizzles on contact with [plant_tray]!"))
+ return PROJECTILE_DELETE_WITHOUT_HITTING
/obj/projectile/magic/death
name = "bolt of death"
icon_state = "pulse1_bl"
-/obj/projectile/magic/death/on_hit(mob/living/target)
+/obj/projectile/magic/death/on_hit(atom/target)
. = ..()
- if(!isliving(target))
- return
- if(target.mob_biotypes & MOB_UNDEAD) //negative energy heals the undead
- if(target.revive(full_heal = TRUE, admin_revive = TRUE))
- target.grab_ghost(force = TRUE) // even suicides
- to_chat(target, span_notice("You rise with a start, you're undead!!!"))
- else if(target.stat != DEAD)
- to_chat(target, span_notice("You feel great!"))
- return
+ if(isliving(target))
+ var/mob/living/victim = target
+ if(victim.mob_biotypes & MOB_UNDEAD) //negative energy heals the undead
+ if(victim.revive(full_heal = TRUE, admin_revive = TRUE))
+ victim.grab_ghost(force = TRUE) // even suicides
+ to_chat(victim, span_notice("You rise with a start, you're undead!!!"))
+ else if(victim.stat != DEAD)
+ to_chat(victim, span_notice("You feel great!"))
+ return
+ victim.death()
- target.death()
+ if(istype(target, /obj/machinery/hydroponics))
+ var/obj/machinery/hydroponics/plant_tray = target
+ if(!plant_tray.myseed)
+ return
+ plant_tray.set_weedlevel(0) // even the weeds perish
+ plant_tray.plantdies()
/obj/projectile/magic/resurrection
name = "bolt of resurrection"
@@ -43,20 +60,27 @@
damage_type = OXY
nodamage = TRUE
-/obj/projectile/magic/resurrection/on_hit(mob/living/target)
+/obj/projectile/magic/resurrection/on_hit(atom/target)
. = ..()
- if(!isliving(target))
- return
- if(target.mob_biotypes & MOB_UNDEAD) //positive energy harms the undead
- target.death()
- return
+ if(isliving(target))
+ var/mob/living/victim = target
+
+ if(victim.mob_biotypes & MOB_UNDEAD) //positive energy harms the undead
+ victim.death()
+ return
- if(target.revive(full_heal = TRUE, admin_revive = TRUE))
- target.grab_ghost(force = TRUE) // even suicides
- to_chat(target, span_notice("You rise with a start, you're alive!!!"))
- else if(target.stat != DEAD)
- to_chat(target, span_notice("You feel great!"))
+ if(victim.revive(full_heal = TRUE, admin_revive = TRUE))
+ victim.grab_ghost(force = TRUE) // even suicides
+ to_chat(victim, span_notice("You rise with a start, you're alive!!!"))
+ else if(victim.stat != DEAD)
+ to_chat(victim, span_notice("You feel great!"))
+
+ if(istype(target, /obj/machinery/hydroponics))
+ var/obj/machinery/hydroponics/plant_tray = target
+ if(!plant_tray.myseed)
+ return
+ plant_tray.set_plant_health(plant_tray.myseed.endurance, forced = TRUE)
/obj/projectile/magic/teleport
name = "bolt of teleportation"
@@ -79,7 +103,7 @@
teleammount++
var/smoke_range = max(round(4 - teleammount), 0)
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(smoke_range, location = stuff.loc) //Smoke drops off if a lot of stuff is moved for the sake of sanity
+ smoke.set_up(smoke_range, holder = src, location = stuff.loc) //Smoke drops off if a lot of stuff is moved for the sake of sanity
smoke.start()
/obj/projectile/magic/safety
@@ -100,7 +124,7 @@
if(do_teleport(target, destination_turf, channel=TELEPORT_CHANNEL_MAGIC))
for(var/t in list(origin_turf, destination_turf))
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(0, location = t)
+ smoke.set_up(0, holder = src, location = t)
smoke.start()
/obj/projectile/magic/door
@@ -139,10 +163,18 @@
damage_type = BURN
nodamage = TRUE
-/obj/projectile/magic/change/on_hit(mob/living/target)
+/obj/projectile/magic/change/on_hit(atom/target)
. = ..()
+
if(isliving(target))
- target.wabbajack()
+ var/mob/living/victim = target
+ victim.wabbajack()
+
+ if(istype(target, /obj/machinery/hydroponics))
+ var/obj/machinery/hydroponics/plant_tray = target
+ if(!plant_tray.myseed)
+ return
+ plant_tray.polymorph()
/obj/projectile/magic/animate
name = "bolt of animation"
@@ -161,7 +193,7 @@
var/obj/structure/statue/petrified/P = src
if(P.petrified_mob)
var/mob/living/L = P.petrified_mob
- var/mob/living/simple_animal/hostile/statue/S = new(P.loc, owner)
+ var/mob/living/simple_animal/hostile/netherworld/statue/S = new(P.loc, owner)
S.name = "statue of [L.name]"
if(owner)
S.faction = list("[REF(owner)]")
@@ -337,7 +369,7 @@
/obj/projectile/magic/sapping/on_hit(mob/living/target)
. = ..()
if(isliving(target))
- SEND_SIGNAL(target, COMSIG_ADD_MOOD_EVENT, src, /datum/mood_event/sapped)
+ SEND_SIGNAL(target, COMSIG_ADD_MOOD_EVENT, REF(src), /datum/mood_event/sapped)
/obj/projectile/magic/necropotence
name = "bolt of necropotence"
@@ -345,20 +377,16 @@
/obj/projectile/magic/necropotence/on_hit(mob/living/target)
. = ..()
- if(isliving(target))
- if(!target.mind)
- return
+ if(!isliving(target))
+ return
- to_chat(target, span_danger("Your body feels drained and there is a burning pain in your chest."))
- target.maxHealth -= 20
- target.health = min(target.health, target.maxHealth)
- if(target.maxHealth <= 0)
- to_chat(target, span_userdanger("Your weakened soul is completely consumed by the [src]!"))
- return
- for(var/obj/effect/proc_holder/spell/spell in target.mind.spell_list)
- spell.charge_counter = spell.charge_max
- spell.recharging = FALSE
- spell.update_appearance()
+ // Performs a soul tap on living targets hit.
+ // Takes away max health, but refreshes their spell cooldowns (if any)
+ var/datum/action/cooldown/spell/tap/tap = new(src)
+ if(tap.is_valid_target(target))
+ tap.cast(target)
+
+ qdel(tap)
/obj/projectile/magic/wipe
name = "bolt of possession"
@@ -403,17 +431,61 @@
to_chat(target, span_notice("Your mind has managed to go unnoticed in the spirit world."))
qdel(trauma)
-/// Gives magic projectiles a 3x3 Area of Effect range that will bump into any nearby mobs
+/// Gives magic projectiles an area of effect radius that will bump into any nearby mobs
/obj/projectile/magic/aoe
- name = "Area Bolt"
- desc = "What the fuck does this do?!"
+ damage = 0
+
+ /// The AOE radius that the projectile will trigger on people.
+ var/trigger_range = 1
+ /// Whether our projectile will only be able to hit the original target / clicked on atom
+ var/can_only_hit_target = FALSE
+
+ /// Whether our projectile leaves a trail behind it as it moves.
+ var/trail = FALSE
+ /// The duration of the trail before deleting.
+ var/trail_lifespan = 0 SECONDS
+ /// The icon the trail uses.
+ var/trail_icon = 'icons/obj/wizard.dmi'
+ /// The icon state the trail uses.
+ var/trail_icon_state = "trail"
/obj/projectile/magic/aoe/Range()
- for(var/mob/living/target in range(1, get_turf(src)))
- if(target.stat != DEAD && target != firer)
- return Bump(target)
- ..()
+ if(trigger_range >= 1)
+ for(var/mob/living/nearby_guy in range(trigger_range, get_turf(src)))
+ if(nearby_guy.stat == DEAD)
+ continue
+ if(nearby_guy == firer)
+ continue
+ // Bump handles anti-magic checks for us, conveniently.
+ return Bump(nearby_guy)
+ return ..()
+
+/obj/projectile/magic/aoe/can_hit_target(atom/target, list/passthrough, direct_target = FALSE, ignore_loc = FALSE)
+ if(can_only_hit_target && target != original)
+ return FALSE
+ return ..()
+
+/obj/projectile/magic/aoe/Moved(atom/OldLoc, Dir)
+ . = ..()
+ if(trail)
+ create_trail()
+
+/// Creates and handles the trail that follows the projectile.
+/obj/projectile/magic/aoe/proc/create_trail()
+ if(!trajectory)
+ return
+
+ var/datum/point/vector/previous = trajectory.return_vector_after_increments(1, -1)
+ var/obj/effect/overlay/trail = new /obj/effect/overlay(previous.return_turf())
+ trail.pixel_x = previous.return_px()
+ trail.pixel_y = previous.return_py()
+ trail.icon = trail_icon
+ trail.icon_state = trail_icon_state
+ //might be changed to temp overlay
+ trail.set_density(FALSE)
+ trail.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ QDEL_IN(trail, trail_lifespan)
/obj/projectile/magic/aoe/lightning
name = "lightning bolt"
@@ -423,30 +495,33 @@
nodamage = FALSE
speed = 0.3
+ /// The power of the zap itself when it electrocutes someone
var/zap_power = 20000
+ /// The range of the zap itself when it electrocutes someone
var/zap_range = 15
+ /// The flags of the zap itself when it electrocutes someone
var/zap_flags = ZAP_MOB_DAMAGE | ZAP_MOB_STUN | ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN
- var/chain
- var/mob/living/caster
+ /// A reference to the chain beam between the caster and the projectile
+ var/datum/beam/chain
/obj/projectile/magic/aoe/lightning/fire(setAngle)
- if(caster)
- chain = caster.Beam(src, icon_state = "lightning[rand(1, 12)]")
- ..()
+ if(firer)
+ chain = firer.Beam(src, icon_state = "lightning[rand(1, 12)]")
+ return ..()
/obj/projectile/magic/aoe/lightning/on_hit(target)
. = ..()
tesla_zap(src, zap_range, zap_power, zap_flags)
+/obj/projectile/magic/aoe/lightning/Destroy()
+ QDEL_NULL(chain)
+ return ..()
+
/obj/projectile/magic/aoe/lightning/no_zap
zap_power = 10000
zap_range = 4
zap_flags = ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_LOW_POWER_GEN
-/obj/projectile/magic/aoe/lightning/Destroy()
- qdel(chain)
- . = ..()
-
/obj/projectile/magic/fireball
name = "bolt of fireball"
icon_state = "fireball"
@@ -454,20 +529,82 @@
damage_type = BRUTE
nodamage = FALSE
- //explosion values
+ /// Heavy explosion range of the fireball
var/exp_heavy = 0
+ /// Light explosion range of the fireball
var/exp_light = 2
- var/exp_flash = 3
+ /// Fire radius of the fireball
var/exp_fire = 2
+ /// Flash radius of the fireball
+ var/exp_flash = 3
-/obj/projectile/magic/fireball/on_hit(mob/living/target)
+/obj/projectile/magic/fireball/on_hit(atom/target, blocked = FALSE, pierce_hit)
. = ..()
- if(ismob(target))
- //between this 10 burn, the 10 brute, the explosion brute, and the onfire burn, your at about 65 damage if you stop drop and roll immediately
- target.take_overall_damage(0, 10)
-
- var/turf/T = get_turf(target)
- explosion(T, devastation_range = -1, heavy_impact_range = exp_heavy, light_impact_range = exp_light, flame_range = exp_fire, flash_range = exp_flash, adminlog = FALSE, explosion_cause = src)
+ if(isliving(target))
+ var/mob/living/mob_target = target
+ // between this 10 burn, the 10 brute, the explosion brute, and the onfire burn,
+ // you are at about 65 damage if you stop drop and roll immediately
+ mob_target.take_overall_damage(burn = 10)
+
+ var/turf/target_turf = get_turf(target)
+
+ explosion(
+ target_turf,
+ devastation_range = -1,
+ heavy_impact_range = exp_heavy,
+ light_impact_range = exp_light,
+ flame_range = exp_fire,
+ flash_range = exp_flash,
+ adminlog = FALSE,
+ explosion_cause = src,
+ )
+
+/obj/projectile/magic/aoe/magic_missile
+ name = "magic missile"
+ icon_state = "magicm"
+ range = 20
+ speed = 5
+ trigger_range = 0
+ can_only_hit_target = TRUE
+ nodamage = FALSE
+ paralyze = 6 SECONDS
+ hitsound = 'sound/magic/mm_hit.ogg'
+
+ trail = TRUE
+ trail_lifespan = 0.5 SECONDS
+ trail_icon_state = "magicmd"
+
+/obj/projectile/magic/aoe/magic_missile/lesser
+ color = "red" //Looks more culty this way
+ range = 10
+
+/obj/projectile/magic/aoe/juggernaut
+ name = "Gauntlet Echo"
+ icon_state = "cultfist"
+ alpha = 180
+ damage = 30
+ damage_type = BRUTE
+ knockdown = 50
+ hitsound = 'sound/weapons/punch3.ogg'
+ trigger_range = 0
+ antimagic_flags = MAGIC_RESISTANCE_HOLY
+ ignored_factions = list("cult")
+ range = 15
+ speed = 7
+
+/obj/projectile/magic/spell/juggernaut/on_hit(atom/target, blocked)
+ . = ..()
+ var/turf/target_turf = get_turf(src)
+ playsound(target_turf, 'sound/weapons/resonator_blast.ogg', 100, FALSE)
+ new /obj/effect/temp_visual/cult/sac(target_turf)
+ for(var/obj/adjacent_object in range(1, src))
+ if(!adjacent_object.density)
+ continue
+ if(istype(adjacent_object, /obj/structure/destructible/cult))
+ continue
+
+ adjacent_object.take_damage(90, BRUTE, MELEE, 0)
+ new /obj/effect/temp_visual/cult/turf/floor(get_turf(adjacent_object))
//still magic related, but a different path
diff --git a/code/modules/reagents/chemistry/machinery/smoke_machine.dm b/code/modules/reagents/chemistry/machinery/smoke_machine.dm
index f5cd71c5c02ce..9617c37fcc17b 100644
--- a/code/modules/reagents/chemistry/machinery/smoke_machine.dm
+++ b/code/modules/reagents/chemistry/machinery/smoke_machine.dm
@@ -18,7 +18,8 @@
var/setting = 1 // displayed range is 3 * setting
var/max_range = 3 // displayed max range is 3 * max range
-/datum/effect_system/fluid_spread/smoke/chem/smoke_machine/set_up(range = 1, amount = DIAMOND_AREA(range), atom/location = null, datum/reagents/carry = null, efficiency = 10, silent=FALSE)
+/datum/effect_system/fluid_spread/smoke/chem/smoke_machine/set_up(range = 1, amount = DIAMOND_AREA(range), atom/holder, atom/location = null, datum/reagents/carry = null, efficiency = 10, silent=FALSE)
+ src.holder = holder
src.location = get_turf(location)
src.amount = amount
carry?.copy_to(chemholder, 20)
@@ -87,7 +88,7 @@
if(on && !smoke_test)
update_appearance()
var/datum/effect_system/fluid_spread/smoke/chem/smoke_machine/smoke = new()
- smoke.set_up(setting * 3, location = location, carry = reagents, efficiency = efficiency)
+ smoke.set_up(setting * 3, holder = src, location = location, carry = reagents, efficiency = efficiency)
smoke.start()
use_power(active_power_usage)
diff --git a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm
index 530f81f215058..cb92785aa6839 100644
--- a/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/alcohol_reagents.dm
@@ -1029,7 +1029,7 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_desc = "A cold refreshment."
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/ethanol/demonsblood //Prevents the imbiber from being dragged into a pool of blood by a slaughter demon.
+/datum/reagent/consumable/ethanol/demonsblood
name = "Demon's Blood"
description = "AHHHH!!!!"
color = "#820000" // rgb: 130, 0, 0
@@ -1041,7 +1041,34 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_desc = "Just looking at this thing makes the hair at the back of your neck stand up."
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
-/datum/reagent/consumable/ethanol/devilskiss //If eaten by a slaughter demon, the demon will regret it.
+/datum/reagent/consumable/ethanol/demonsblood/on_mob_metabolize(mob/living/metabolizer)
+ . = ..()
+ RegisterSignal(metabolizer, COMSIG_LIVING_BLOOD_CRAWL_PRE_CONSUMED, .proc/pre_bloodcrawl_consumed)
+
+/datum/reagent/consumable/ethanol/demonsblood/on_mob_end_metabolize(mob/living/metabolizer)
+ . = ..()
+ UnregisterSignal(metabolizer, COMSIG_LIVING_BLOOD_CRAWL_PRE_CONSUMED)
+
+/// Prevents the imbiber from being dragged into a pool of blood by a slaughter demon.
+/datum/reagent/consumable/ethanol/demonsblood/proc/pre_bloodcrawl_consumed(
+ mob/living/source,
+ datum/action/cooldown/spell/jaunt/bloodcrawl/crawl,
+ mob/living/jaunter,
+ obj/effect/decal/cleanable/blood,
+)
+
+ SIGNAL_HANDLER
+
+ var/turf/jaunt_turf = get_turf(jaunter)
+ jaunt_turf.visible_message(
+ span_warning("Something prevents [source] from entering [blood]!"),
+ blind_message = span_notice("You hear a splash and a thud.")
+ )
+ to_chat(jaunter, span_warning("A strange force is blocking [source] from entering!"))
+
+ return COMPONENT_STOP_CONSUMPTION
+
+/datum/reagent/consumable/ethanol/devilskiss
name = "Devil's Kiss"
description = "Creepy time!"
color = "#A68310" // rgb: 166, 131, 16
@@ -1053,6 +1080,41 @@ All effects don't start immediately, but rather get worse over time; the rate is
glass_desc = "Creepy time!"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+/datum/reagent/consumable/ethanol/devilskiss/on_mob_metabolize(mob/living/metabolizer)
+ . = ..()
+ RegisterSignal(metabolizer, COMSIG_LIVING_BLOOD_CRAWL_CONSUMED, .proc/on_bloodcrawl_consumed)
+
+/datum/reagent/consumable/ethanol/devilskiss/on_mob_end_metabolize(mob/living/metabolizer)
+ . = ..()
+ UnregisterSignal(metabolizer, COMSIG_LIVING_BLOOD_CRAWL_CONSUMED)
+
+/// If eaten by a slaughter demon, the demon will regret it.
+/datum/reagent/consumable/ethanol/devilskiss/proc/on_bloodcrawl_consumed(
+ mob/living/source,
+ datum/action/cooldown/spell/jaunt/bloodcrawl/crawl,
+ mob/living/jaunter,
+)
+
+ SIGNAL_HANDLER
+
+ . = COMPONENT_STOP_CONSUMPTION
+
+ to_chat(jaunter, span_boldwarning("AAH! THEIR FLESH! IT BURNS!"))
+ jaunter.apply_damage(25, BRUTE, wound_bonus = CANT_WOUND)
+
+ for(var/obj/effect/decal/cleanable/nearby_blood in range(1, get_turf(source)))
+ if(!nearby_blood.can_bloodcrawl_in())
+ continue
+ source.forceMove(get_turf(nearby_blood))
+ source.visible_message(span_warning("[nearby_blood] violently expels [source]!"))
+ crawl.exit_blood_effect(source)
+ return
+
+ // Fuck it, just eject them, thanks to some split second cleaning
+ source.forceMove(get_turf(source))
+ source.visible_message(span_warning("[source] appears from nowhere, covered in blood!"))
+ crawl.exit_blood_effect(source)
+
/datum/reagent/consumable/ethanol/vodkatonic
name = "Vodka and Tonic"
description = "For when a gin and tonic isn't Russian enough."
diff --git a/code/modules/reagents/chemistry/reagents/food_reagents.dm b/code/modules/reagents/chemistry/reagents/food_reagents.dm
index 789964a3dd88a..3f53e34ceac7a 100644
--- a/code/modules/reagents/chemistry/reagents/food_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/food_reagents.dm
@@ -118,7 +118,7 @@
burn_heal = 1
/datum/reagent/consumable/nutriment/vitamin/on_mob_life(mob/living/carbon/M, delta_time, times_fired)
- if(M.satiety < 600)
+ if(M.satiety < MAX_SATIETY)
M.satiety += 30 * REM * delta_time
. = ..()
@@ -136,6 +136,26 @@
taste_description = "rich earthy pungent"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+/datum/reagent/consumable/nutriment/cloth_fibers
+ name = "Cloth Fibers"
+ description = "It's not actually a form of nutriment but it does keep Mothpeople going for a short while..."
+ nutriment_factor = 30 * REAGENTS_METABOLISM
+ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+ brute_heal = 0
+ burn_heal = 0
+ ///Amount of satiety that will be drained when the cloth_fibers is fully metabolized
+ var/delayed_satiety_drain = 2 * CLOTHING_NUTRITION_GAIN
+
+/datum/reagent/consumable/nutriment/cloth_fibers/on_mob_life(mob/living/carbon/M, delta_time, times_fired)
+ if(M.satiety < MAX_SATIETY)
+ M.adjust_nutrition(CLOTHING_NUTRITION_GAIN)
+ delayed_satiety_drain += CLOTHING_NUTRITION_GAIN
+ return ..()
+
+/datum/reagent/consumable/nutriment/cloth_fibers/on_mob_delete(mob/living/carbon/M)
+ M.adjust_nutrition(-delayed_satiety_drain)
+ return ..()
+
/datum/reagent/consumable/cooking_oil
name = "Cooking Oil"
description = "A variety of cooking oil derived from fat or plants. Used in food preparation and frying."
@@ -1043,3 +1063,11 @@
color = "#efeff0"
nutriment_factor = 1.5 * REAGENTS_METABOLISM
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+
+/datum/reagent/consumable/olivepaste
+ name = "Olive Paste"
+ description = "A mushy pile of finely ground olives."
+ taste_description = "mushy olives"
+ color = "#adcf77"
+ chemical_flags = REAGENT_CAN_BE_SYNTHESIZED
+
diff --git a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
index 87dfa1fad6902..33cdd29a414e4 100644
--- a/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/medicine_reagents.dm
@@ -271,7 +271,7 @@
/datum/reagent/medicine/spaceacillin
name = "Spaceacillin"
- description = "Spaceacillin will prevent a patient from conventionally spreading any diseases they are currently infected with. Also reduces infection in serious burns."
+ description = "Spaceacillin will provide limited resistance against disease and parasites. Also reduces infection in serious burns."
color = "#E1F2E6"
metabolization_rate = 0.1 * REAGENTS_METABOLISM
ph = 8.1
@@ -1154,7 +1154,7 @@
M.adjustOrganLoss(ORGAN_SLOT_BRAIN, 2 * REM * delta_time, 150)
if(DT_PROB(5, delta_time))
M.say(pick("Yeah, well, you know, that's just, like, uh, your opinion, man.", "Am I glad he's frozen in there and that we're out here, and that he's the sheriff and that we're frozen out here, and that we're in there, and I just remembered, we're out here. What I wanna know is: Where's the caveman?", "It ain't me, it ain't me...", "Make love, not war!", "Stop, hey, what's that sound? Everybody look what's going down...", "Do you believe in magic in a young girl's heart?"), forced = /datum/reagent/medicine/earthsblood)
- M.druggy = clamp(M.druggy + (10 * REM * delta_time), 0, 15 * REM * delta_time) //See above
+ M.adjust_timed_status_effect(20 SECONDS * REM * delta_time, /datum/status_effect/drugginess, max_duration = 30 SECONDS * REM * delta_time)
..()
. = TRUE
diff --git a/code/modules/reagents/chemistry/reagents/other_reagents.dm b/code/modules/reagents/chemistry/reagents/other_reagents.dm
index ae7043ecf1f0f..9c51356a598c7 100644
--- a/code/modules/reagents/chemistry/reagents/other_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/other_reagents.dm
@@ -369,8 +369,8 @@
/datum/reagent/fuel/unholywater/on_mob_life(mob/living/carbon/M, delta_time, times_fired)
if(IS_CULTIST(M))
- M.adjust_drowsyness(-5* REM * delta_time)
- M.AdjustAllImmobility(-40 *REM* REM * delta_time)
+ M.adjust_drowsyness(-5 * REM * delta_time)
+ M.AdjustAllImmobility(-40 * REM * delta_time)
M.adjustStaminaLoss(-10 * REM * delta_time, 0)
M.adjustToxLoss(-2 * REM * delta_time, 0)
M.adjustOxyLoss(-2 * REM * delta_time, 0)
@@ -1500,15 +1500,10 @@
taste_description = "searingly cold"
chemical_flags = REAGENT_CAN_BE_SYNTHESIZED|REAGENT_NO_RANDOM_RECIPE
-/datum/reagent/hypernoblium/on_mob_metabolize(mob/living/L)
- . = ..()
- if(isplasmaman(L))
- ADD_TRAIT(L, TRAIT_NOFIRE, type)
-
-/datum/reagent/hypernoblium/on_mob_end_metabolize(mob/living/L)
- if(isplasmaman(L))
- REMOVE_TRAIT(L, TRAIT_NOFIRE, type)
- return ..()
+/datum/reagent/hypernoblium/on_mob_life(mob/living/carbon/M, delta_time, times_fired)
+ if(isplasmaman(M))
+ M.set_timed_status_effect(10 SECONDS * REM * delta_time, /datum/status_effect/hypernob_protection)
+ ..()
/datum/reagent/healium
name = "Healium"
diff --git a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
index 1bd9a4cb1d100..e6e63ebec2773 100644
--- a/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
+++ b/code/modules/reagents/chemistry/reagents/toxin_reagents.dm
@@ -247,8 +247,8 @@
zombiepowder.data["method"] |= INGEST
/datum/reagent/toxin/zombiepowder/on_mob_life(mob/living/M, delta_time, times_fired)
- ..()
if(HAS_TRAIT(M, TRAIT_FAKEDEATH) && HAS_TRAIT(M, TRAIT_DEATHCOMA))
+ ..()
return TRUE
switch(current_cycle)
if(1 to 5)
@@ -259,6 +259,8 @@
M.adjustStaminaLoss(40 * REM * delta_time, 0)
if(9 to INFINITY)
M.fakedeath(type)
+ ..()
+ return TRUE
/datum/reagent/toxin/ghoulpowder
name = "Ghoul Powder"
diff --git a/code/modules/reagents/chemistry/recipes.dm b/code/modules/reagents/chemistry/recipes.dm
index f17574d1f5315..3e6c2992ec572 100644
--- a/code/modules/reagents/chemistry/recipes.dm
+++ b/code/modules/reagents/chemistry/recipes.dm
@@ -379,8 +379,8 @@
if(!force_range)
force_range = (sum_volume/6) + 3
if(invert_reagents.reagent_list)
- smoke.set_up(force_range, location = holder.my_atom, carry = invert_reagents)
- smoke.start()
+ smoke.set_up(force_range, holder = holder.my_atom, location = holder.my_atom, carry = invert_reagents)
+ smoke.start(log = TRUE)
holder.my_atom.audible_message("The [holder.my_atom] suddenly explodes, launching the aerosolized reagents into the air!")
if(clear_reactants)
clear_reactants(holder)
@@ -400,8 +400,8 @@
if(!force_range)
force_range = (sum_volume/6) + 3
if(reagents.reagent_list)
- smoke.set_up(force_range, location = holder.my_atom, carry = reagents)
- smoke.start()
+ smoke.set_up(force_range, holder = holder.my_atom, location = holder.my_atom, carry = reagents)
+ smoke.start(log = TRUE)
holder.my_atom.audible_message("The [holder.my_atom] suddenly explodes, launching the aerosolized reagents into the air!")
if(clear_reactants)
clear_reactants(holder)
diff --git a/code/modules/reagents/chemistry/recipes/others.dm b/code/modules/reagents/chemistry/recipes/others.dm
index ac88ba89f78a5..8b61e7e4b5981 100644
--- a/code/modules/reagents/chemistry/recipes/others.dm
+++ b/code/modules/reagents/chemistry/recipes/others.dm
@@ -328,7 +328,7 @@
reaction_flags = REACTION_INSTANT
/datum/chemical_reaction/foam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/fluid_spread/foam, 2 * created_volume, notification = span_danger("The solution spews out foam!"))
+ holder.create_foam(/datum/effect_system/fluid_spread/foam, 2 * created_volume, notification = span_danger("The solution spews out foam!"), log = TRUE)
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE
/datum/chemical_reaction/metalfoam
@@ -338,7 +338,7 @@
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE
/datum/chemical_reaction/metalfoam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/fluid_spread/foam/metal, 5 * created_volume, /obj/structure/foamedmetal, span_danger("The solution spews out a metallic foam!"))
+ holder.create_foam(/datum/effect_system/fluid_spread/foam/metal, 5 * created_volume, /obj/structure/foamedmetal, span_danger("The solution spews out a metallic foam!"), log = TRUE)
/datum/chemical_reaction/smart_foam
required_reagents = list(/datum/reagent/aluminium = 3, /datum/reagent/smart_foaming_agent = 1, /datum/reagent/toxin/acid/fluacid = 1)
@@ -347,7 +347,7 @@
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE
/datum/chemical_reaction/smart_foam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/fluid_spread/foam/metal/smart, 5 * created_volume, /obj/structure/foamedmetal, span_danger("The solution spews out metallic foam!"))
+ holder.create_foam(/datum/effect_system/fluid_spread/foam/metal/smart, 5 * created_volume, /obj/structure/foamedmetal, span_danger("The solution spews out metallic foam!"), log = TRUE)
/datum/chemical_reaction/ironfoam
required_reagents = list(/datum/reagent/iron = 3, /datum/reagent/foaming_agent = 1, /datum/reagent/toxin/acid/fluacid = 1)
@@ -356,7 +356,7 @@
reaction_tags = REACTION_TAG_EASY | REACTION_TAG_UNIQUE
/datum/chemical_reaction/ironfoam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/fluid_spread/foam/metal/iron, 5 * created_volume, /obj/structure/foamedmetal/iron, span_danger("The solution spews out a metallic foam!"))
+ holder.create_foam(/datum/effect_system/fluid_spread/foam/metal/iron, 5 * created_volume, /obj/structure/foamedmetal/iron, span_danger("The solution spews out a metallic foam!"), log = TRUE)
/datum/chemical_reaction/foaming_agent
results = list(/datum/reagent/foaming_agent = 1)
diff --git a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
index 7c5f4e80cc7b5..482471a5666d2 100644
--- a/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
+++ b/code/modules/reagents/chemistry/recipes/pyrotechnics.dm
@@ -336,8 +336,8 @@
S.attach(location)
playsound(location, 'sound/effects/smoke.ogg', 50, TRUE, -3)
if(S)
- S.set_up(amount = created_volume * 3, location = location, carry = holder, silent = FALSE)
- S.start()
+ S.set_up(amount = created_volume * 3, holder = holder.my_atom, location = location, carry = holder, silent = FALSE)
+ S.start(log = TRUE)
if(holder?.my_atom)
holder.clear_reagents()
@@ -354,8 +354,8 @@
S.attach(location)
playsound(location, 'sound/effects/smoke.ogg', 50, TRUE, -3)
if(S)
- S.set_up(amount = created_volume, location = location, carry = holder, silent = FALSE)
- S.start()
+ S.set_up(amount = created_volume, holder = holder.my_atom, location = location, carry = holder, silent = FALSE)
+ S.start(log = TRUE)
if(holder?.my_atom)
holder.clear_reagents()
diff --git a/code/modules/reagents/chemistry/recipes/slime_extracts.dm b/code/modules/reagents/chemistry/recipes/slime_extracts.dm
index 49398f41bc2e2..6e179cec8aaf9 100644
--- a/code/modules/reagents/chemistry/recipes/slime_extracts.dm
+++ b/code/modules/reagents/chemistry/recipes/slime_extracts.dm
@@ -190,7 +190,7 @@
required_other = TRUE
/datum/chemical_reaction/slime/slimefoam/on_reaction(datum/reagents/holder, datum/equilibrium/reaction, created_volume)
- holder.create_foam(/datum/effect_system/fluid_spread/foam, 80, span_danger("[src] spews out foam!"))
+ holder.create_foam(/datum/effect_system/fluid_spread/foam, 80, span_danger("[src] spews out foam!"), log = TRUE)
//Dark Blue
/datum/chemical_reaction/slime/slimefreeze
diff --git a/code/modules/reagents/chemistry/recipes/special.dm b/code/modules/reagents/chemistry/recipes/special.dm
index 56090bb5eda8b..5c2d8360e64fa 100644
--- a/code/modules/reagents/chemistry/recipes/special.dm
+++ b/code/modules/reagents/chemistry/recipes/special.dm
@@ -190,6 +190,31 @@ GLOBAL_LIST_INIT(medicine_reagents, build_medicine_reagents())
return null
.[pathR] = textreagents[R]
+/datum/chemical_reaction/randomized/proc/SaveOldRecipe()
+ var/recipe_data = list()
+
+ recipe_data["timestamp"] = created
+ recipe_data["required_reagents"] = required_reagents
+ recipe_data["required_catalysts"] = required_catalysts
+
+ recipe_data["is_cold_recipe"] = is_cold_recipe
+ recipe_data["required_temp"] = required_temp
+ recipe_data["optimal_temp"] = optimal_temp
+ recipe_data["overheat_temp"] = overheat_temp
+ recipe_data["thermic_constant"] = thermic_constant
+
+ recipe_data["optimal_ph_min"] = optimal_ph_min
+ recipe_data["optimal_ph_max"] = optimal_ph_max
+ recipe_data["determin_ph_range"] = determin_ph_range
+ recipe_data["H_ion_release"] = H_ion_release
+
+ recipe_data["purity_min"] = purity_min
+
+ recipe_data["results"] = results
+ recipe_data["required_container"] = required_container
+
+ return recipe_data
+
/datum/chemical_reaction/randomized/proc/LoadOldRecipe(recipe_data)
created = text2num(recipe_data["timestamp"])
diff --git a/code/modules/reagents/reagent_containers/borghydro.dm b/code/modules/reagents/reagent_containers/borghydro.dm
deleted file mode 100644
index 8c5def625c011..0000000000000
--- a/code/modules/reagents/reagent_containers/borghydro.dm
+++ /dev/null
@@ -1,302 +0,0 @@
-#define C2NAMEREAGENT "[initial(reagent.name)] (Has Side-Effects)"
-/*
-Contains:
-Borg Hypospray
-Borg Shaker
-Nothing to do with hydroponics in here. Sorry to dissapoint you.
-*/
-
-/*
-Borg Hypospray
-*/
-/obj/item/reagent_containers/borghypo
- name = "cyborg hypospray"
- desc = "An advanced chemical synthesizer and injection system, designed for heavy-duty medical equipment."
- icon = 'icons/obj/syringe.dmi'
- inhand_icon_state = "hypo"
- lefthand_file = 'icons/mob/inhands/equipment/medical_lefthand.dmi'
- righthand_file = 'icons/mob/inhands/equipment/medical_righthand.dmi'
- icon_state = "borghypo"
- amount_per_transfer_from_this = 5
- volume = 30
- possible_transfer_amounts = list(5)
- var/mode = 1
- var/charge_cost = 50
- var/charge_timer = 0
- var/recharge_time = 10 //Time it takes for shots to recharge (in seconds)
- var/dispensed_temperature = DEFAULT_REAGENT_TEMPERATURE ///Optional variable to override the temperature add_reagent() will use
- var/bypass_protection = 0 //If the hypospray can go through armor or thick material
-
- var/list/datum/reagents/reagent_list = list()
- var/list/reagent_ids = list(/datum/reagent/medicine/c2/convermol, /datum/reagent/medicine/c2/libital, /datum/reagent/medicine/c2/multiver, /datum/reagent/medicine/c2/aiuri, /datum/reagent/medicine/epinephrine, /datum/reagent/medicine/spaceacillin, /datum/reagent/medicine/salglu_solution)
- var/accepts_reagent_upgrades = TRUE //If upgrades can increase number of reagents dispensed.
- var/list/modes = list() //Basically the inverse of reagent_ids. Instead of having numbers as "keys" and strings as values it has strings as keys and numbers as values.
- //Used as list for input() in shakers.
- var/list/reagent_names = list()
-
-
-/obj/item/reagent_containers/borghypo/Initialize(mapload)
- . = ..()
-
- for(var/R in reagent_ids)
- add_reagent(R)
-
- START_PROCESSING(SSobj, src)
-
-
-/obj/item/reagent_containers/borghypo/Destroy()
- STOP_PROCESSING(SSobj, src)
- QDEL_LIST(reagent_list)
- return ..()
-
-/obj/item/reagent_containers/borghypo/process(delta_time) //Every [recharge_time] seconds, recharge some reagents for the cyborg
- charge_timer += delta_time
- if(charge_timer >= recharge_time)
- regenerate_reagents()
- charge_timer = 0
-
- return 1
-
-// Use this to add more chemicals for the borghypo to produce.
-/obj/item/reagent_containers/borghypo/proc/add_reagent(datum/reagent/reagent)
- reagent_ids |= reagent
- var/datum/reagents/RG = new(30)
- RG.my_atom = src
- reagent_list += RG
-
- var/datum/reagents/R = reagent_list[length(reagent_list)]
- R.add_reagent(reagent, 30, reagtemp = dispensed_temperature)
-
- modes[reagent] = length(modes) + 1
-
- if(initial(reagent.harmful))
- reagent_names[C2NAMEREAGENT] = reagent
- else
- reagent_names[initial(reagent.name)] = reagent
-
-/obj/item/reagent_containers/borghypo/proc/del_reagent(datum/reagent/reagent)
- reagent_ids -= reagent
- if(istype(reagent, /datum/reagent/medicine/c2))
- reagent_names -= C2NAMEREAGENT
- else
- reagent_names -= initial(reagent.name)
- var/datum/reagents/RG
- var/datum/reagents/TRG
- for(var/i in 1 to length(reagent_ids))
- TRG = reagent_list[i]
- if (TRG.has_reagent(reagent))
- RG = TRG
- break
- if (RG)
- reagent_list -= RG
- RG.del_reagent(reagent)
-
- modes[reagent] = length(modes) - 1
-
-/obj/item/reagent_containers/borghypo/proc/regenerate_reagents()
- if(iscyborg(src.loc))
- var/mob/living/silicon/robot/R = src.loc
- if(R?.cell)
- for(var/i in 1 to length(reagent_ids))
- var/datum/reagents/RG = reagent_list[i]
- if(RG.total_volume < RG.maximum_volume) //Don't recharge reagents and drain power if the storage is full.
- R.cell.use(charge_cost) //Take power from borg...
- RG.add_reagent(reagent_ids[i], 5, reagtemp = dispensed_temperature) //And fill hypo with reagent.
-
-/obj/item/reagent_containers/borghypo/attack(mob/living/carbon/M, mob/user)
- var/datum/reagents/R = reagent_list[mode]
- if(!R.total_volume)
- to_chat(user, span_warning("The injector is empty!"))
- return
- if(!istype(M))
- return
- if(R.total_volume && M.try_inject(user, user.zone_selected, injection_flags = INJECT_TRY_SHOW_ERROR_MESSAGE | (bypass_protection ? INJECT_CHECK_PENETRATE_THICK : 0)))
- to_chat(M, span_warning("You feel a tiny prick!"))
- to_chat(user, span_notice("You inject [M] with the injector."))
- if(M.reagents)
- var/trans = R.trans_to(M, amount_per_transfer_from_this, transfered_by = user, methods = INJECT)
- to_chat(user, span_notice("[trans] unit\s injected. [R.total_volume] unit\s remaining."))
-
- var/list/injected = list()
- for(var/datum/reagent/RG in R.reagent_list)
- injected += RG.name
- log_combat(user, M, "injected", src, "(CHEMICALS: [english_list(injected)])")
-
-/obj/item/reagent_containers/borghypo/attack_self(mob/user)
- var/choice = tgui_input_list(user, "Reagent to dispense", "Medical Hypospray", sort_list(reagent_names))
- if(isnull(choice))
- return
- if(isnull(reagent_names[choice]))
- return
- var/chosen_reagent = modes[reagent_names[choice]]
- mode = chosen_reagent
- playsound(loc, 'sound/effects/pop.ogg', 50, FALSE)
- var/datum/reagent/R = GLOB.chemical_reagents_list[reagent_ids[mode]]
- to_chat(user, span_notice("[src] is now dispensing '[R.name]'."))
- return
-
-/obj/item/reagent_containers/borghypo/examine(mob/user)
- . = ..()
- . += DescribeContents() //Because using the standardized reagents datum was just too cool for whatever fuckwit wrote this
- var/datum/reagent/loaded = modes[mode]
- . += "Currently loaded: [initial(loaded.name)]. [initial(loaded.description)]"
- . += span_notice("Alt+Click to change transfer amount. Currently set to [amount_per_transfer_from_this == 5 ? "dose normally (5u)" : "microdose (2u)"].")
-
-/obj/item/reagent_containers/borghypo/proc/DescribeContents()
- . = list()
- var/empty = TRUE
-
- for(var/datum/reagents/RS in reagent_list)
- var/datum/reagent/R = locate() in RS.reagent_list
- if(R)
- . += span_notice("It currently has [R.volume] unit\s of [R.name] stored.")
- empty = FALSE
-
- if(empty)
- . += span_warning("It is currently empty! Allow some time for the internal synthesizer to produce more.")
-
-/obj/item/reagent_containers/borghypo/AltClick(mob/living/user)
- . = ..()
- if(user.stat == DEAD || user != loc)
- return //IF YOU CAN HEAR ME SET MY TRANSFER AMOUNT TO 1
- if(amount_per_transfer_from_this == 5)
- amount_per_transfer_from_this = 2
- else
- amount_per_transfer_from_this = 5
- to_chat(user,span_notice("[src] is now set to [amount_per_transfer_from_this == 5 ? "dose normally" : "microdose"]."))
-
-/obj/item/reagent_containers/borghypo/hacked
- icon_state = "borghypo_s"
- reagent_ids = list (/datum/reagent/toxin/acid/fluacid, /datum/reagent/toxin/mutetoxin, /datum/reagent/toxin/cyanide, /datum/reagent/toxin/sodium_thiopental, /datum/reagent/toxin/heparin, /datum/reagent/toxin/lexorin)
- accepts_reagent_upgrades = FALSE
-
-/obj/item/reagent_containers/borghypo/clown
- name = "laughter injector"
- desc = "Keeps the crew happy and productive!"
- reagent_ids = list(/datum/reagent/consumable/laughter)
- accepts_reagent_upgrades = FALSE
-
-/obj/item/reagent_containers/borghypo/clown/hacked
- name = "laughter injector"
- desc = "Keeps the crew so happy they don't work!"
- reagent_ids = list(/datum/reagent/consumable/superlaughter)
- accepts_reagent_upgrades = FALSE
-
-/obj/item/reagent_containers/borghypo/syndicate
- name = "syndicate cyborg hypospray"
- desc = "An experimental piece of Syndicate technology used to produce powerful restorative nanites used to very quickly restore injuries of all types. Also metabolizes potassium iodide for radiation poisoning, inacusiate for ear damage and morphine for offense."
- icon_state = "borghypo_s"
- charge_cost = 20
- recharge_time = 2
- reagent_ids = list(
- /datum/reagent/medicine/syndicate_nanites,
- /datum/reagent/medicine/inacusiate,
- /datum/reagent/medicine/potass_iodide,
- /datum/reagent/medicine/morphine,
- )
- bypass_protection = TRUE
- accepts_reagent_upgrades = FALSE
-
-/*
-Borg Shaker
-*/
-/obj/item/reagent_containers/borghypo/borgshaker
- name = "cyborg shaker"
- desc = "An advanced drink synthesizer and mixer."
- icon = 'icons/obj/drinks.dmi'
- icon_state = "shaker"
- possible_transfer_amounts = list(5,10,20)
- charge_cost = 20 //Lots of reagents all regenerating at once, so the charge cost is lower. They also regenerate faster.
- recharge_time = 3
- accepts_reagent_upgrades = FALSE
- dispensed_temperature = WATER_MATTERSTATE_CHANGE_TEMP //Water stays wet, ice stays ice
-
- reagent_ids = list(/datum/reagent/consumable/applejuice, /datum/reagent/consumable/banana, /datum/reagent/consumable/coffee,
- /datum/reagent/consumable/cream, /datum/reagent/consumable/dr_gibb, /datum/reagent/consumable/grenadine,
- /datum/reagent/consumable/ice, /datum/reagent/consumable/lemonjuice, /datum/reagent/consumable/lemon_lime,
- /datum/reagent/consumable/limejuice, /datum/reagent/consumable/menthol, /datum/reagent/consumable/milk,
- /datum/reagent/consumable/nothing, /datum/reagent/consumable/orangejuice, /datum/reagent/consumable/peachjuice,
- /datum/reagent/consumable/sodawater, /datum/reagent/consumable/space_cola, /datum/reagent/consumable/spacemountainwind,
- /datum/reagent/consumable/pwr_game, /datum/reagent/consumable/shamblers, /datum/reagent/consumable/soymilk,
- /datum/reagent/consumable/space_up, /datum/reagent/consumable/sugar, /datum/reagent/consumable/tea,
- /datum/reagent/consumable/tomatojuice, /datum/reagent/consumable/tonic, /datum/reagent/water,
- /datum/reagent/consumable/pineapplejuice, /datum/reagent/consumable/sol_dry,
- /datum/reagent/consumable/ethanol/ale, /datum/reagent/consumable/ethanol/applejack, /datum/reagent/consumable/ethanol/beer,
- /datum/reagent/consumable/ethanol/champagne, /datum/reagent/consumable/ethanol/cognac, /datum/reagent/consumable/ethanol/creme_de_menthe,
- /datum/reagent/consumable/ethanol/creme_de_cacao, /datum/reagent/consumable/ethanol/gin, /datum/reagent/consumable/ethanol/kahlua,
- /datum/reagent/consumable/ethanol/rum, /datum/reagent/consumable/ethanol/sake, /datum/reagent/consumable/ethanol/tequila,
- /datum/reagent/consumable/ethanol/triple_sec, /datum/reagent/consumable/ethanol/vermouth, /datum/reagent/consumable/ethanol/vodka,
- /datum/reagent/consumable/ethanol/whiskey, /datum/reagent/consumable/ethanol/wine, /datum/reagent/consumable/ethanol/creme_de_coconut)
-
-/obj/item/reagent_containers/borghypo/borgshaker/attack(mob/M, mob/user)
- return //Can't inject stuff with a shaker, can we? //not with that attitude
-
-/obj/item/reagent_containers/borghypo/borgshaker/regenerate_reagents()
- if(iscyborg(src.loc))
- var/mob/living/silicon/robot/R = src.loc
- if(R?.cell)
- for(var/i in modes) //Lots of reagents in this one, so it's best to regenrate them all at once to keep it from being tedious.
- var/valueofi = modes[i]
- var/datum/reagents/RG = reagent_list[valueofi]
- if(RG.total_volume < RG.maximum_volume)
- R.cell.use(charge_cost)
- RG.add_reagent(reagent_ids[valueofi], 5, reagtemp = dispensed_temperature)
-
-/obj/item/reagent_containers/borghypo/borgshaker/afterattack(obj/target, mob/user, proximity)
- . = ..()
- if(!proximity)
- return
-
- else if(target.is_refillable())
- var/datum/reagents/R = reagent_list[mode]
- if(!R.total_volume)
- to_chat(user, span_warning("[src] is currently out of this ingredient! Please allow some time for the synthesizer to produce more."))
- return
-
- if(target.reagents.total_volume >= target.reagents.maximum_volume)
- to_chat(user, span_notice("[target] is full."))
- return
-
- var/trans = R.trans_to(target, amount_per_transfer_from_this, transfered_by = user)
- to_chat(user, span_notice("You transfer [trans] unit\s of the solution to [target]."))
-
-/obj/item/reagent_containers/borghypo/borgshaker/DescribeContents()
- var/datum/reagents/RS = reagent_list[mode]
- var/datum/reagent/R = locate() in RS.reagent_list
- if(R)
- return span_notice("It currently has [R.volume] unit\s of [R.name] stored.")
- else
- return span_warning("It is currently empty! Please allow some time for the synthesizer to produce more.")
-
-/obj/item/reagent_containers/borghypo/borgshaker/hacked
- name = "cyborg shaker"
- desc = "Will mix drinks that knock them dead."
- icon = 'icons/obj/drinks.dmi'
- icon_state = "threemileislandglass"
- possible_transfer_amounts = list(5,10,20)
- charge_cost = 20 //Lots of reagents all regenerating at once, so the charge cost is lower. They also regenerate faster.
- recharge_time = 3
- accepts_reagent_upgrades = FALSE
- dispensed_temperature = WATER_MATTERSTATE_CHANGE_TEMP
-
- reagent_ids = list(/datum/reagent/toxin/fakebeer, /datum/reagent/consumable/ethanol/fernet)
-
-/obj/item/reagent_containers/borghypo/peace
- name = "Peace Hypospray"
-
- reagent_ids = list(/datum/reagent/peaceborg/confuse,/datum/reagent/peaceborg/tire,/datum/reagent/pax/peaceborg)
- accepts_reagent_upgrades = FALSE
-
-/obj/item/reagent_containers/borghypo/peace/hacked
- desc = "Everything's peaceful in death!"
- icon_state = "borghypo_s"
- reagent_ids = list(/datum/reagent/peaceborg/confuse,/datum/reagent/peaceborg/tire,/datum/reagent/pax/peaceborg,/datum/reagent/toxin/staminatoxin,/datum/reagent/toxin/sulfonal,/datum/reagent/toxin/sodium_thiopental,/datum/reagent/toxin/cyanide,/datum/reagent/toxin/fentanyl)
- accepts_reagent_upgrades = FALSE
-
-/obj/item/reagent_containers/borghypo/epi
- name = "epinephrine injector"
- desc = "An advanced chemical synthesizer and injection system, designed to stabilize patients."
- reagent_ids = list(/datum/reagent/medicine/epinephrine)
- accepts_reagent_upgrades = FALSE
-
-#undef C2NAMEREAGENT
diff --git a/code/modules/reagents/reagent_containers/hypospray.dm b/code/modules/reagents/reagent_containers/hypospray.dm
index b218b1084008e..174159ac55b63 100644
--- a/code/modules/reagents/reagent_containers/hypospray.dm
+++ b/code/modules/reagents/reagent_containers/hypospray.dm
@@ -314,3 +314,13 @@
volume = 15
amount_per_transfer_from_this = 15
list_reagents = list(/datum/reagent/medicine/epinephrine = 5, /datum/reagent/medicine/coagulant = 2.5, /datum/reagent/iron = 3.5, /datum/reagent/medicine/salglu_solution = 4)
+
+/obj/item/reagent_containers/hypospray/medipen/mutadone
+ name = "mutadone autoinjector"
+ desc = "An mutadone medipen to assist in curing genetic errors in one single injector."
+ icon_state = "penacid"
+ inhand_icon_state = "penacid"
+ base_icon_state = "penacid"
+ volume = 15
+ amount_per_transfer_from_this = 15
+ list_reagents = list(/datum/reagent/medicine/mutadone = 15)
diff --git a/code/modules/reagents/reagent_containers/medigel.dm b/code/modules/reagents/reagent_containers/medigel.dm
index 6306842321165..7945af0f45563 100644
--- a/code/modules/reagents/reagent_containers/medigel.dm
+++ b/code/modules/reagents/reagent_containers/medigel.dm
@@ -22,7 +22,6 @@
var/apply_type = PATCH
var/apply_method = "spray" //the thick gel is sprayed and then dries into patch like film.
var/self_delay = 30
- var/squirt_mode = 0
custom_price = PAYCHECK_CREW * 2
unique_reskin = list(
"Blue" = "medigel_blue",
@@ -33,16 +32,9 @@
"Purple" = "medigel_purple"
)
-/obj/item/reagent_containers/medigel/attack_self(mob/user)
- squirt_mode = !squirt_mode
- return ..()
-
-/obj/item/reagent_containers/medigel/attack_self_secondary(mob/user)
- squirt_mode = !squirt_mode
- return ..()
-
/obj/item/reagent_containers/medigel/mode_change_message(mob/user)
- to_chat(user, span_notice("You will now apply the medigel's contents in [squirt_mode ? "short bursts":"extended sprays"]. You'll now use [amount_per_transfer_from_this] units per use."))
+ var/squirt_mode = amount_per_transfer_from_this == initial(amount_per_transfer_from_this)
+ to_chat(user, span_notice("You will now apply the medigel's contents in [squirt_mode ? "extended sprays":"short bursts"]. You'll now use [amount_per_transfer_from_this] units per use."))
/obj/item/reagent_containers/medigel/attack(mob/M, mob/user, def_zone)
if(!reagents || !reagents.total_volume)
diff --git a/code/modules/reagents/reagent_containers/watering_can.dm b/code/modules/reagents/reagent_containers/watering_can.dm
new file mode 100644
index 0000000000000..f3f72234de0cd
--- /dev/null
+++ b/code/modules/reagents/reagent_containers/watering_can.dm
@@ -0,0 +1,47 @@
+/obj/item/reagent_containers/glass/watering_can
+ name = "watering can"
+ desc = "It's a watering can. It is scientifically proved that using a watering can to simulate rain increases plant happiness!"
+ icon = 'icons/obj/hydroponics/equipment.dmi'
+ icon_state = "watering_can"
+ inhand_icon_state = "watering_can"
+ lefthand_file = 'icons/mob/inhands/equipment/hydroponics_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/equipment/hydroponics_righthand.dmi'
+ custom_materials = list(/datum/material/iron = 200)
+ w_class = WEIGHT_CLASS_NORMAL
+ volume = 100
+ amount_per_transfer_from_this = 20
+ possible_transfer_amounts = list(20,100)
+
+/obj/item/reagent_containers/glass/watering_can/wood
+ name = "wood watering can"
+ desc = "An old metal-made watering can but shoddily painted to look like it was made of wood for some dubious reason..."
+ icon_state = "watering_can_wood"
+ inhand_icon_state = "watering_can_wood"
+ volume = 70
+ possible_transfer_amounts = list(20,70)
+
+/obj/item/reagent_containers/glass/watering_can/advanced
+ desc = "Everything a botanist would want in a watering can. This marvel of technology generates its own water!"
+ name = "advanced watering can"
+ icon_state = "adv_watering_can"
+ inhand_icon_state = "adv_watering_can"
+ custom_materials = list(/datum/material/iron = 2500, /datum/material/glass = 200)
+ list_reagents = list(/datum/reagent/water = 100)
+ ///Refill rate for the watering can
+ var/refill_rate = 5
+ ///Determins what reagent to use for refilling
+ var/datum/reagent/refill_reagent = /datum/reagent/water
+
+/obj/item/reagent_containers/glass/watering_can/advanced/Initialize(mapload)
+ . = ..()
+ START_PROCESSING(SSobj, src)
+
+/obj/item/reagent_containers/glass/watering_can/advanced/process(delta_time)
+ ///How much to refill
+ var/refill_add = min(volume - reagents.total_volume, refill_rate * delta_time)
+ if(refill_add > 0)
+ reagents.add_reagent(refill_reagent, refill_add)
+
+/obj/item/reagent_containers/glass/watering_can/advanced/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ return ..()
diff --git a/code/modules/reagents/reagent_dispenser.dm b/code/modules/reagents/reagent_dispenser.dm
index c741f4c3a9f15..1648a648ab69b 100644
--- a/code/modules/reagents/reagent_dispenser.dm
+++ b/code/modules/reagents/reagent_dispenser.dm
@@ -19,6 +19,8 @@
var/openable = FALSE
///Is this dispenser slowly leaking its reagent?
var/leaking = FALSE
+ ///How much reagent to leak
+ var/amount_to_leak = 10
/obj/structure/reagent_dispensers/Initialize(mapload)
. = ..()
@@ -30,8 +32,11 @@
. = ..()
if(can_be_tanked)
. += span_notice("Use a sheet of iron to convert this into a plumbing-compatible tank.")
- if(leaking)
- . += span_warning("Its tap is wrenched open!")
+ if(openable)
+ if(!leaking)
+ . += span_notice("Its tap looks like it could be wrenched open.")
+ else
+ . += span_warning("Its tap is wrenched open!")
/obj/structure/reagent_dispensers/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir)
. = ..()
@@ -75,6 +80,13 @@
else
qdel(src)
+/obj/structure/reagent_dispensers/proc/tank_leak()
+ if(leaking && reagents && reagents.total_volume >= amount_to_leak)
+ reagents.expose(get_turf(src), TOUCH, amount_to_leak / max(amount_to_leak, reagents.total_volume))
+ reagents.remove_reagent(reagent_id, amount_to_leak)
+ return TRUE
+ return FALSE
+
/obj/structure/reagent_dispensers/wrench_act(mob/living/user, obj/item/tool)
. = ..()
if(!openable)
@@ -82,14 +94,12 @@
leaking = !leaking
balloon_alert(user, "[leaking ? "opened" : "closed"] [src]'s tap")
log_game("[key_name(user)] [leaking ? "opened" : "closed"] [src]")
- if(leaking && reagents)
- reagents.expose(get_turf(src), TOUCH, 10 / max(10, reagents.total_volume))
+ tank_leak()
return TOOL_ACT_TOOLTYPE_SUCCESS
/obj/structure/reagent_dispensers/Moved(atom/OldLoc, Dir)
. = ..()
- if(leaking && reagents)
- reagents.expose(get_turf(src), TOUCH, 10 / max(10, reagents.total_volume))
+ tank_leak()
/obj/structure/reagent_dispensers/watertank
name = "water tank"
@@ -117,6 +127,14 @@
icon_state = "fuel"
reagent_id = /datum/reagent/fuel
openable = TRUE
+ //an assembly attached to the tank
+ var/obj/item/assembly_holder/rig = null
+ //whether it accepts assemblies or not
+ var/accepts_rig = TRUE
+ //overlay of attached assemblies
+ var/mutable_appearance/assembliesoverlay
+ /// The last person to rig this fuel tank - Stored with the object. Only the last person matters for investigation
+ var/last_rigger = ""
/obj/structure/reagent_dispensers/fueltank/Initialize(mapload)
. = ..()
@@ -124,10 +142,49 @@
if(SSevents.holidays?[APRIL_FOOLS])
icon_state = "fuel_fools"
+/obj/structure/reagent_dispensers/fueltank/Destroy()
+ QDEL_NULL(rig)
+ return ..()
+
+/obj/structure/reagent_dispensers/fueltank/Exited(atom/movable/gone, direction)
+ . = ..()
+ if(gone == rig)
+ rig = null
+
+/obj/structure/reagent_dispensers/fueltank/examine(mob/user)
+ . = ..()
+ if(get_dist(user, src) <= 2)
+ if(rig)
+ . += span_warning("There is some kind of device rigged to the tank!")
+ else
+ . += span_notice("It looks like you could rig a device to the tank.")
+
+/obj/structure/reagent_dispensers/fueltank/attack_hand(mob/user, list/modifiers)
+ . = ..()
+ if(.)
+ return
+ if(!rig)
+ return
+ user.balloon_alert_to_viewers("detaching rig...")
+ if(!do_after(user, 2 SECONDS, target = src))
+ return
+ user.balloon_alert_to_viewers("detached rig")
+ log_message("[key_name(user)] detached [rig] from [src]", LOG_GAME)
+ if(!user.put_in_hands(rig))
+ rig.forceMove(get_turf(user))
+ rig = null
+ last_rigger = null
+ cut_overlays(assembliesoverlay)
+ UnregisterSignal(src, COMSIG_IGNITER_ACTIVATE)
+
/obj/structure/reagent_dispensers/fueltank/boom()
explosion(src, heavy_impact_range = 1, light_impact_range = 5, flame_range = 5)
qdel(src)
+/obj/structure/reagent_dispensers/fueltank/proc/rig_boom()
+ log_bomber(last_rigger, "rigged fuel tank exploded", src)
+ boom()
+
/obj/structure/reagent_dispensers/fueltank/blob_act(obj/structure/blob/B)
boom()
@@ -168,6 +225,27 @@
log_bomber(user, "detonated a", src, "via welding tool")
boom()
return
+ if(istype(I, /obj/item/assembly_holder) && accepts_rig)
+ if(rig)
+ user.balloon_alert("another device is in the way!")
+ return ..()
+ user.balloon_alert_to_viewers("attaching rig...")
+ if(!do_after(user, 2 SECONDS, target = src))
+ return
+ user.balloon_alert_to_viewers("attached rig")
+ var/obj/item/assembly_holder/holder = I
+ if(locate(/obj/item/assembly/igniter) in holder.assemblies)
+ rig = holder
+ if(!user.transferItemToLoc(holder, src))
+ return
+ log_bomber(user, "rigged [name] with [holder.name] for explosion", src)
+ last_rigger = user
+ assembliesoverlay = holder
+ assembliesoverlay.pixel_x += 6
+ assembliesoverlay.pixel_y += 1
+ add_overlay(assembliesoverlay)
+ RegisterSignal(src, COMSIG_IGNITER_ACTIVATE, .proc/rig_boom)
+ return
return ..()
/obj/structure/reagent_dispensers/fueltank/large
diff --git a/code/modules/recycling/conveyor.dm b/code/modules/recycling/conveyor.dm
index e4463108ab130..de648244e2ab7 100644
--- a/code/modules/recycling/conveyor.dm
+++ b/code/modules/recycling/conveyor.dm
@@ -440,6 +440,7 @@ GLOBAL_LIST_EMPTY(conveyors_by_id)
. = ..()
. += span_notice("[src] is set to [oneway ? "one way" : "default"] configuration. It can be changed with a screwdriver.")
. += span_notice("[src] is set to [invert_icon ? "inverted": "normal"] position. It can be rotated with a wrench.")
+ . += span_notice("[src] is set to move [conveyor_speed] seconds per belt. It can be changed with a multitool.")
/obj/machinery/conveyor_switch/oneway
icon_state = "conveyor_switch_oneway"
diff --git a/code/modules/recycling/sortingmachinery.dm b/code/modules/recycling/sortingmachinery.dm
index c5494f6462100..dae278a45c5cc 100644
--- a/code/modules/recycling/sortingmachinery.dm
+++ b/code/modules/recycling/sortingmachinery.dm
@@ -205,7 +205,7 @@
if(!attempt_pre_unwrap_contents(user))
return
unwrap_contents()
- post_unwrap_contents(user)
+ post_unwrap_contents()
/**
* # Wrapped up items small enough to carry.
diff --git a/code/modules/religion/religion_sects.dm b/code/modules/religion/religion_sects.dm
index c97483f6907c9..aca029f1cf32a 100644
--- a/code/modules/religion/religion_sects.dm
+++ b/code/modules/religion/religion_sects.dm
@@ -399,7 +399,7 @@
///places you can spar in. rites can be used to expand this list with new arenas!
var/list/arenas = list(
"Recreation Area" = /area/station/commons/fitness/recreation,
- "Chapel" = /area/station/service/chapel
+ "Chapel" = /area/station/service/chapel,
)
///how many matches you've lost with holy stakes. 3 = excommunication
var/matches_lost = 0
diff --git a/code/modules/research/anomaly/anomaly_core.dm b/code/modules/research/anomaly/anomaly_core.dm
index a32ad34a14cc7..f171c996575f6 100644
--- a/code/modules/research/anomaly/anomaly_core.dm
+++ b/code/modules/research/anomaly/anomaly_core.dm
@@ -62,11 +62,11 @@
icon_state = "vortex_core"
anomaly_type = /obj/effect/anomaly/bhole
-/obj/item/assembly/signaler/anomaly/delimber
- name = "\improper delimber anomaly core"
- desc = "The neutralized core of a delimber anomaly. It's squirming, as if moving. It'd probably be valuable for research."
- icon_state = "delimber_core"
- anomaly_type = /obj/effect/anomaly/delimber
+/obj/item/assembly/signaler/anomaly/bioscrambler
+ name = "\improper bioscrambler anomaly core"
+ desc = "The neutralized core of a bioscrambler anomaly. It's squirming, as if moving. It'd probably be valuable for research."
+ icon_state = "bioscrambler_core"
+ anomaly_type = /obj/effect/anomaly/bioscrambler
/obj/item/assembly/signaler/anomaly/hallucination
name = "\improper hallucination anomaly core"
diff --git a/code/modules/research/anomaly/raw_anomaly.dm b/code/modules/research/anomaly/raw_anomaly.dm
index 4bb750b862398..66d0085b9fe1a 100644
--- a/code/modules/research/anomaly/raw_anomaly.dm
+++ b/code/modules/research/anomaly/raw_anomaly.dm
@@ -56,11 +56,11 @@
desc = "You should not see this!"
icon_state = "rawcore_bluespace"
-/obj/item/raw_anomaly_core/delimber
- name = "raw delimber core"
- desc = "The raw core of a delimber anomaly, it squirms."
- anomaly_type = /obj/item/assembly/signaler/anomaly/delimber
- icon_state = "rawcore_delimber"
+/obj/item/raw_anomaly_core/bioscrambler
+ name = "raw bioscrambler core"
+ desc = "The raw core of a bioscrambler anomaly, it squirms."
+ anomaly_type = /obj/item/assembly/signaler/anomaly/bioscrambler
+ icon_state = "rawcore_bioscrambler"
/obj/item/raw_anomaly_core/random/Initialize(mapload)
. = ..()
diff --git a/code/modules/research/bepis.dm b/code/modules/research/bepis.dm
index f42895ecf600b..e23965158c9d9 100644
--- a/code/modules/research/bepis.dm
+++ b/code/modules/research/bepis.dm
@@ -82,6 +82,9 @@
/obj/machinery/rnd/bepis/screwdriver_act(mob/living/user, obj/item/tool)
return default_deconstruction_screwdriver(user, "chamber_open", "chamber", tool)
+/obj/machinery/rnd/bepis/screwdriver_act_secondary(mob/living/user, obj/item/tool)
+ return default_deconstruction_screwdriver(user, "chamber_open", "chamber", tool)
+
/obj/machinery/rnd/bepis/RefreshParts()
. = ..()
var/C = 0
diff --git a/code/modules/research/designs/AI_module_designs.dm b/code/modules/research/designs/AI_module_designs.dm
index c74deeffa65ee..f6df3a8551c7e 100644
--- a/code/modules/research/designs/AI_module_designs.dm
+++ b/code/modules/research/designs/AI_module_designs.dm
@@ -153,3 +153,165 @@
build_path = /obj/item/ai_module/core/full/custom
category = list("AI Modules")
departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/dungeon_master_module
+ name = "Core Module Design (Dungeon Master)"
+ desc = "Allows for the construction of a Dungeon Master AI Core Module."
+ id = "dungeon_master_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/dungeon_master
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/painter_module
+ name = "Core Module Design (Painter)"
+ desc = "Allows for the construction of a Painter AI Core Module."
+ id = "painter_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/painter
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/nutimov_module
+ name = "Core Module Design (Nutimov)"
+ desc = "Allows for the construction of a Nutimov AI Core Module."
+ id = "nutimov_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/nutimov
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/ten_commandments_module
+ name = "Core Module Design (10 Commandments)"
+ desc = "Allows for the construction of a 10 Commandments AI Core Module."
+ id = "ten_commandments_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/ten_commandments
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/asimovpp_module
+ name = "Core Module Design (Asimov++)"
+ desc = "Allows for the construction of a Asimov++ AI Core Module."
+ id = "asimovpp_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/asimovpp
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/hippocratic_module
+ name = "Core Module Design (Hippocratic)"
+ desc = "Allows for the construction of a Hippocratic AI Core Module."
+ id = "hippocratic_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/hippocratic
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/paladin_devotion_module
+ name = "Core Module Design (Paladin Devotion)"
+ desc = "Allows for the construction of a Paladin Devotion AI Core Module."
+ id = "paladin_devotion_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/paladin_devotion
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/robocop_module
+ name = "Core Module Design (Robocop)"
+ desc = "Allows for the construction of a Robocop AI Core Module."
+ id = "robocop_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/robocop
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/maintain_module
+ name = "Core Module Design (Maintain)"
+ desc = "Allows for the construction of a Maintain AI Core Module."
+ id = "maintain_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/maintain
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/liveandletlive_module
+ name = "Core Module Design (Liveandletlive)"
+ desc = "Allows for the construction of a Liveandletlive AI Core Module."
+ id = "liveandletlive_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/liveandletlive
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/peacekeeper_module
+ name = "Core Module Design (Peacekeeper)"
+ desc = "Allows for the construction of a Peacekeeper AI Core Module."
+ id = "peacekeeper_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/peacekeeper
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/reporter_module
+ name = "Core Module Design (Reporter)"
+ desc = "Allows for the construction of a Reporter AI Core Module."
+ id = "reporter_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/reporter
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/hulkamania_module
+ name = "Core Module Design (Hulkamania)"
+ desc = "Allows for the construction of a Hulkamania AI Core Module."
+ id = "hulkamania_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/hulkamania
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/drone_module
+ name = "Core Module Design (Drone)"
+ desc = "Allows for the construction of a Drone AI Core Module."
+ id = "drone_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/drone
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/antimov_module
+ name = "Core Module Design (Antimov)"
+ desc = "Allows for the construction of a Antimov AI Core Module."
+ id = "antimov_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/antimov
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/balance_module
+ name = "Core Module Design (Balance)"
+ desc = "Allows for the construction of a Balance AI Core Module."
+ id = "balance_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/balance
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/thermurderdynamic_module
+ name = "Core Module Design (Thermurderdynamic)"
+ desc = "Allows for the construction of a Thermurderdynamic AI Core Module."
+ id = "thermurderdynamic_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/thermurderdynamic
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
+
+/datum/design/board/damaged
+ name = "Core Module Design (Damaged)"
+ desc = "Allows for the construction of a Damaged AI Core Module."
+ id = "damaged_module"
+ materials = list(/datum/material/glass = 1000, /datum/material/diamond = 2000, /datum/material/bluespace = 1000)
+ build_path = /obj/item/ai_module/core/full/damaged
+ category = list("AI Modules")
+ departmental_flags = DEPARTMENT_BITFLAG_SCIENCE
diff --git a/code/modules/research/designs/autolathe_designs.dm b/code/modules/research/designs/autolathe_designs.dm
index 49d00c4398804..23209fd7c86c0 100644
--- a/code/modules/research/designs/autolathe_designs.dm
+++ b/code/modules/research/designs/autolathe_designs.dm
@@ -11,6 +11,15 @@
category = list("initial","Tools","Tool Designs")
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+/datum/design/watering_can
+ name = "Watering Can"
+ id = "watering_can"
+ build_type = AUTOLATHE | PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/iron = 200)
+ build_path = /obj/item/reagent_containers/glass/watering_can
+ category = list("initial","Tools","Tool Designs")
+ departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+
/datum/design/mop
name = "Mop"
id = "mop"
@@ -315,7 +324,16 @@
build_type = AUTOLATHE
materials = list(/datum/material/iron = MINERAL_MATERIAL_AMOUNT)
build_path = /obj/item/stack/sheet/iron
- category = list("initial","Construction")
+ category = list("initial","Material")
+ maxstack = 50
+
+/datum/design/rods
+ name = "Iron Rod"
+ id = "rods"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/iron = 1000)
+ build_path = /obj/item/stack/rods
+ category = list("initial","Material")
maxstack = 50
/datum/design/glass
@@ -324,7 +342,7 @@
build_type = AUTOLATHE
materials = list(/datum/material/glass = MINERAL_MATERIAL_AMOUNT)
build_path = /obj/item/stack/sheet/glass
- category = list("initial","Construction")
+ category = list("initial","Material")
maxstack = 50
/datum/design/rglass
@@ -333,16 +351,79 @@
build_type = AUTOLATHE | SMELTER | PROTOLATHE | AWAY_LATHE
materials = list(/datum/material/iron = 1000, /datum/material/glass = MINERAL_MATERIAL_AMOUNT)
build_path = /obj/item/stack/sheet/rglass
- category = list("initial","Construction","Stock Parts")
+ category = list("initial","Material","Stock Parts")
maxstack = 50
-/datum/design/rods
- name = "Iron Rod"
- id = "rods"
+/datum/design/silver
+ name = "Silver"
+ id = "silver"
build_type = AUTOLATHE
- materials = list(/datum/material/iron = 1000)
- build_path = /obj/item/stack/rods
- category = list("initial","Construction")
+ materials = list(/datum/material/silver = MINERAL_MATERIAL_AMOUNT)
+ build_path = /obj/item/stack/sheet/mineral/silver
+ category = list("initial","Material")
+ maxstack = 50
+
+/datum/design/gold
+ name = "Gold"
+ id = "gold"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/gold = MINERAL_MATERIAL_AMOUNT)
+ build_path = /obj/item/stack/sheet/mineral/gold
+ category = list("initial","Material")
+ maxstack = 50
+
+/datum/design/diamond
+ name = "Diamond"
+ id = "diamond"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/diamond = MINERAL_MATERIAL_AMOUNT)
+ build_path = /obj/item/stack/sheet/mineral/diamond
+ category = list("initial","Material")
+ maxstack = 50
+
+/datum/design/plasma
+ name = "Plasma"
+ id = "plasma"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/plasma = MINERAL_MATERIAL_AMOUNT)
+ build_path = /obj/item/stack/sheet/mineral/plasma
+ category = list("initial","Material")
+ maxstack = 50
+
+/datum/design/uranium
+ name = "Uranium"
+ id = "uranium"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/uranium = MINERAL_MATERIAL_AMOUNT)
+ build_path = /obj/item/stack/sheet/mineral/uranium
+ category = list("initial","Material")
+ maxstack = 50
+
+/datum/design/bananium
+ name = "Bananium"
+ id = "bananium"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/bananium = MINERAL_MATERIAL_AMOUNT)
+ build_path = /obj/item/stack/sheet/mineral/bananium
+ category = list("initial","Material")
+ maxstack = 50
+
+/datum/design/titanium
+ name = "Titanium"
+ id = "titanium"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/titanium = MINERAL_MATERIAL_AMOUNT)
+ build_path = /obj/item/stack/sheet/mineral/titanium
+ category = list("initial","Material")
+ maxstack = 50
+
+/datum/design/plastic
+ name = "Plastic"
+ id = "plastic"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/plastic= MINERAL_MATERIAL_AMOUNT)
+ build_path = /obj/item/stack/sheet/plastic
+ category = list("initial","Material")
maxstack = 50
/datum/design/rcd_ammo
@@ -1265,3 +1346,27 @@
build_path = /obj/item/toner/large
category = list("initial", "Misc", "Equipment")
departmental_flags = DEPARTMENT_BITFLAG_ENGINEERING | DEPARTMENT_BITFLAG_SERVICE
+
+/datum/design/solar
+ name = "Solar Panel Frame"
+ id = "solar_panel"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/iron = 3500, /datum/material/glass = 1000)
+ build_path = /obj/item/solar_assembly
+ category = list("initial", "Construction")
+
+/datum/design/tracker_electronics
+ name = "Solar Tracking Electronics"
+ id = "solar_tracker"
+ build_type = AUTOLATHE
+ materials = list(/datum/material/iron = 100, /datum/material/glass = 500)
+ build_path = /obj/item/electronics/tracker
+ category = list("initial", "Electronics", "Construction")
+
+/datum/design/fishing_rod_basic
+ name = "Fishing Rod"
+ id = "fishing_rod"
+ build_type = AUTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/iron = 200, /datum/material/glass = 200)
+ build_path = /obj/item/fishing_rod
+ category = list("initial", "Misc", "Equipment")
diff --git a/code/modules/research/designs/mechfabricator_designs.dm b/code/modules/research/designs/mechfabricator_designs.dm
index 8054199f5ede9..dd90c20c2ebd4 100644
--- a/code/modules/research/designs/mechfabricator_designs.dm
+++ b/code/modules/research/designs/mechfabricator_designs.dm
@@ -1388,3 +1388,17 @@
materials = list(/datum/material/iron = 2500, /datum/material/glass = 2000, /datum/material/uranium = 1000, /datum/material/bluespace = 1000)
build_path = /obj/item/mod/module/anomaly_locked/kinesis
department_type = MODULE_ENGINEERING
+
+/datum/design/module/mod_sonar
+ name = "MOD Module: Active Sonar"
+ id = "mod_sonar"
+ materials = list(/datum/material/titanium = 250, /datum/material/glass = 1000, /datum/material/gold = 500, /datum/material/uranium = 250)
+ build_path = /obj/item/mod/module/active_sonar
+ department_type = MODULE_SECURITY
+
+/datum/design/module/projectile_dampener
+ name = "MOD Module: Projectile Dampener"
+ id = "mod_projectile_dampener"
+ materials = list(/datum/material/iron = 1000, /datum/material/bluespace = 500)
+ build_path = /obj/item/mod/module/projectile_dampener
+ department_type = MODULE_SECURITY
diff --git a/code/modules/research/designs/misc_designs.dm b/code/modules/research/designs/misc_designs.dm
index f168e7c6adc12..cc9859767358d 100644
--- a/code/modules/research/designs/misc_designs.dm
+++ b/code/modules/research/designs/misc_designs.dm
@@ -439,6 +439,20 @@
category = list("Equipment")
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+
+/////////////////////////////////////////
+/////////////Hydroponics/////////////////
+/////////////////////////////////////////
+
+/datum/design/adv_watering_can
+ name = "Advanced Watering Can"
+ id = "adv_watering_can"
+ build_type = PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/iron = 2500, /datum/material/glass = 200)
+ build_path = /obj/item/reagent_containers/glass/watering_can/advanced
+ category = list("initial","Tools","Tool Designs")
+ departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+
/////////////////////////////////////////
/////////////Holobarriers////////////////
/////////////////////////////////////////
@@ -719,3 +733,15 @@
build_path = /obj/item/plate/oven_tray
category = list("initial","Equipment")
departmental_flags = DEPARTMENT_BITFLAG_SERVICE
+
+/////////////////////////////////////////
+/////////Fishing Equipment///////////////
+/////////////////////////////////////////
+
+/datum/design/fishing_rod_tech
+ name = "Advanced Fishing Rod"
+ id = "fishing_rod_tech"
+ build_type = PROTOLATHE | AWAY_LATHE
+ materials = list(/datum/material/uranium = 1000, /datum/material/plastic = 2000)
+ build_path = /obj/item/fishing_rod/tech
+ category = list("Equipment")
diff --git a/code/modules/research/experimentor.dm b/code/modules/research/experimentor.dm
index 73050e9e31a87..efebdc283c6a7 100644
--- a/code/modules/research/experimentor.dm
+++ b/code/modules/research/experimentor.dm
@@ -233,7 +233,7 @@
/obj/machinery/rnd/experimentor/proc/throwSmoke(turf/where)
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(0, location = where)
+ smoke.set_up(0, holder = src, location = where)
smoke.start()
@@ -310,7 +310,7 @@
tmp_holder.add_reagent(chosenchem , 50)
investigate_log("Experimentor has released [chosenchem] smoke.", INVESTIGATE_EXPERIMENTOR)
var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
- smoke.set_up(0, location = src, carry = tmp_holder, silent = TRUE)
+ smoke.set_up(0, holder = src, location = src, carry = tmp_holder, silent = TRUE)
playsound(src, 'sound/effects/smoke.ogg', 50, TRUE, -3)
smoke.start()
qdel(tmp_holder)
@@ -322,7 +322,7 @@
tmp_holder.my_atom = src
tmp_holder.add_reagent(chosenchem , 50)
var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
- smoke.set_up(0, location = src, carry = tmp_holder, silent = TRUE)
+ smoke.set_up(0, holder = src, location = src, carry = tmp_holder, silent = TRUE)
playsound(src, 'sound/effects/smoke.ogg', 50, TRUE, -3)
smoke.start()
qdel(tmp_holder)
@@ -406,7 +406,7 @@
tmp_holder.add_reagent(/datum/reagent/consumable/frostoil, 50)
investigate_log("Experimentor has released frostoil gas.", INVESTIGATE_EXPERIMENTOR)
var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
- smoke.set_up(0, location = src, carry = tmp_holder, silent = TRUE)
+ smoke.set_up(0, holder = src, location = src, carry = tmp_holder, silent = TRUE)
playsound(src, 'sound/effects/smoke.ogg', 50, TRUE, -3)
smoke.start()
qdel(tmp_holder)
@@ -428,7 +428,7 @@
else if(prob(EFFECT_PROB_MEDIUM-badThingCoeff))
visible_message(span_warning("[src] malfunctions, releasing a flurry of chilly air as [exp_on] pops out!"))
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(0, location = loc)
+ smoke.set_up(0, holder = src, location = loc)
smoke.start()
ejectItem()
////////////////////////////////////////////////////////////////////////////////////////////////
@@ -599,7 +599,7 @@
/obj/item/relic/proc/throwSmoke(turf/where)
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(0, location = get_turf(where))
+ smoke.set_up(0, holder = src, location = get_turf(where))
smoke.start()
/obj/item/relic/proc/corgicannon(mob/user)
diff --git a/code/modules/research/rdconsole.dm b/code/modules/research/rdconsole.dm
index 4996a55853aa4..2e710f4185b4f 100644
--- a/code/modules/research/rdconsole.dm
+++ b/code/modules/research/rdconsole.dm
@@ -22,7 +22,7 @@ Nothing else in the console has ID requirements.
icon_screen = "rdcomp"
icon_keyboard = "rd_key"
circuit = /obj/item/circuitboard/computer/rdconsole
- req_access = list(ACCESS_SCIENCE) // Locking and unlocking the console requires science access
+ req_access = list(ACCESS_RESEARCH) // Locking and unlocking the console requires research access
/// Reference to global science techweb
var/datum/techweb/stored_research
/// The stored technology disk, if present
diff --git a/code/modules/research/server.dm b/code/modules/research/server.dm
index 5f7cf77a9d06b..e76cb50e37f9a 100644
--- a/code/modules/research/server.dm
+++ b/code/modules/research/server.dm
@@ -9,26 +9,20 @@
/// The ninja has blown the HDD up.
#define HDD_OVERLOADED 4
+#define SERVER_NOMINAL_TEXT "Nominal"
+
/obj/machinery/rnd/server
name = "\improper R&D Server"
desc = "A computer system running a deep neural network that processes arbitrary information to produce data useable in the development of new technologies. In layman's terms, it makes research points."
icon = 'icons/obj/machines/research.dmi'
icon_state = "RD-server-on"
base_icon_state = "RD-server"
- var/heat_health = 100
- //Code for point mining here.
- var/working = TRUE //temperature should break it.
+ req_access = list(ACCESS_RD)
+
+ /// if TRUE, we are currently operational and giving out research points.
+ var/working = TRUE
+ /// if TRUE, someone manually disabled us via console.
var/research_disabled = FALSE
- var/server_id = 0
- var/base_mining_income = 2
- var/current_temp = 0
- var/heat_gen = 100
- var/heating_power = 40000
- var/delay = 5
- var/temp_tolerance_low = 0
- var/temp_tolerance_high = T20C
- var/temp_penalty_coefficient = 0.5 //1 = -1 points per degree above high tolerance. 0.5 = -0.5 points per degree above high tolerance.
- req_access = list(ACCESS_RD) //ONLY THE R&D CAN CHANGE SERVER SETTINGS.
/obj/machinery/rnd/server/Initialize(mapload)
. = ..()
@@ -39,98 +33,65 @@
SSresearch.servers -= src
return ..()
-/obj/machinery/rnd/server/RefreshParts()
- . = ..()
- var/tot_rating = 0
- for(var/obj/item/stock_parts/SP in src)
- tot_rating += SP.rating
- heat_gen /= max(1, tot_rating)
-
/obj/machinery/rnd/server/update_icon_state()
- if(machine_stat & EMPED || machine_stat & NOPOWER)
+ if(machine_stat & NOPOWER)
icon_state = "[base_icon_state]-off"
- return ..()
- icon_state = "[base_icon_state]-[research_disabled ? "halt" : "on"]"
+ else
+ // "working" will cover EMP'd, disabled, or just broken
+ icon_state = "[base_icon_state]-[working ? "on" : "halt"]"
return ..()
/obj/machinery/rnd/server/power_change()
refresh_working()
return ..()
+/obj/machinery/rnd/server/on_set_machine_stat()
+ refresh_working()
+ return ..()
+
+/// Checks if we should be working or not, and updates accordingly.
/obj/machinery/rnd/server/proc/refresh_working()
- if(machine_stat & EMPED || research_disabled || machine_stat & NOPOWER)
+ if(machine_stat & (NOPOWER|EMPED) || research_disabled)
working = FALSE
else
working = TRUE
+
update_current_power_usage()
- update_appearance()
+ update_appearance(UPDATE_ICON_STATE)
/obj/machinery/rnd/server/emp_act()
. = ..()
if(. & EMP_PROTECT_SELF)
return
set_machine_stat(machine_stat | EMPED)
- addtimer(CALLBACK(src, .proc/unemp), 600)
+ addtimer(CALLBACK(src, .proc/fix_emp), 60 SECONDS)
refresh_working()
-/obj/machinery/rnd/server/proc/unemp()
+/// Callback to un-emp the server afetr some time.
+/obj/machinery/rnd/server/proc/fix_emp()
set_machine_stat(machine_stat & ~EMPED)
refresh_working()
+/// Toggles whether or not researched_disabled is, yknow, disabled
/obj/machinery/rnd/server/proc/toggle_disable(mob/user)
research_disabled = !research_disabled
log_game("[key_name(user)] [research_disabled ? "shut off" : "turned on"] [src] at [loc_name(user)]")
refresh_working()
-/obj/machinery/rnd/server/proc/get_env_temp()
- var/turf/open/L = loc
- if(isturf(L))
- return L.temperature
- return 0 //what
-
-/obj/machinery/rnd/server/proc/produce_heat(heat_amt)
- if(!(machine_stat & (NOPOWER|BROKEN))) //Blatently stolen from space heater.
- var/turf/L = loc
- if(istype(L))
- var/datum/gas_mixture/env = L.return_air()
- if(env.temperature < (heat_amt+T0C))
-
- var/transfer_moles = 0.25 * env.total_moles()
-
- var/datum/gas_mixture/removed = env.remove(transfer_moles)
-
- if(removed)
-
- var/heat_capacity = removed.heat_capacity()
- if(heat_capacity == 0 || heat_capacity == null)
- heat_capacity = 1
- removed.temperature = min((removed.temperature*heat_capacity + heating_power)/heat_capacity, 1000)
-
- env.merge(removed)
- air_update_turf(FALSE, FALSE)
-
-/proc/fix_noid_research_servers()
- var/list/no_id_servers = list()
- var/list/server_ids = list()
- for(var/obj/machinery/rnd/server/S in GLOB.machines)
- switch(S.server_id)
- if(-1)
- continue
- if(0)
- no_id_servers += S
- else
- server_ids += S.server_id
-
- for(var/obj/machinery/rnd/server/S in no_id_servers)
- var/num = 1
- while(!S.server_id)
- if(num in server_ids)
- num++
- else
- S.server_id = num
- server_ids += num
- no_id_servers -= S
-
+/// Gets status text based on this server's status for the computer.
+/obj/machinery/rnd/server/proc/get_status_text()
+ if(machine_stat & EMPED)
+ return "O&F@I*$ - R3*&O$T R@U!R%D"
+ else if(machine_stat & NOPOWER)
+ return "Offline - Server Unpowered"
+ else if(research_disabled)
+ return "Offline - Server Control Disabled"
+ else if(!working)
+ // If, for some reason, working is FALSE even though we're not emp'd or powerless,
+ // We need something to update our working state - such as rebooting the server
+ return "Offline - Reboot Required"
+
+ return SERVER_NOMINAL_TEXT
/obj/machinery/computer/rdservercontrol
name = "R&D Server Controller"
@@ -165,15 +126,24 @@
var/list/dat = list()
dat += "Connected Servers:"
- dat += "
Server
Operating Temp
Status
"
- for(var/obj/machinery/rnd/server/S in GLOB.machines)
- dat += "
"
for(var/i=stored_research.research_logs.len, i>0, i--)
@@ -226,6 +196,12 @@
return ..()
+/obj/machinery/rnd/server/master/get_status_text()
+ . = ..()
+ // Give us a special message if we're nominal, but our hard drive is gone
+ if(. == SERVER_NOMINAL_TEXT && !source_code_hdd)
+ return "Nominal - Hard Drive Missing"
+
/obj/machinery/rnd/server/master/examine(mob/user)
. = ..()
@@ -311,6 +287,7 @@
to_chat(user, span_notice("You cut the final wire and remove [source_code_hdd]."))
try_put_in_hand(source_code_hdd, user)
source_code_hdd = null
+ SSresearch.income_modifier *= 0.5
return TRUE
to_chat(user, span_notice("You delicately cut the wire. [hdd_wires] wire\s left..."))
return TRUE
diff --git a/code/modules/research/stock_parts.dm b/code/modules/research/stock_parts.dm
index 7b1eea4851d3a..6c4203371e3c0 100644
--- a/code/modules/research/stock_parts.dm
+++ b/code/modules/research/stock_parts.dm
@@ -83,6 +83,7 @@ If you create T5+ please take a pass at mech_fabricator.dm. The parts being good
name = "bluespace rapid part exchange device"
desc = "A version of the RPED that allows for replacement of parts and scanning from a distance, along with higher capacity for parts."
icon_state = "BS_RPED"
+ inhand_icon_state = "BS_RPED"
w_class = WEIGHT_CLASS_NORMAL
works_from_distance = TRUE
pshoom_or_beepboopblorpzingshadashwoosh = 'sound/items/pshoom.ogg'
diff --git a/code/modules/research/techweb/all_nodes.dm b/code/modules/research/techweb/all_nodes.dm
index 27fd6c80cf001..5810bba470866 100644
--- a/code/modules/research/techweb/all_nodes.dm
+++ b/code/modules/research/techweb/all_nodes.dm
@@ -64,6 +64,7 @@
"turbine_part_compressor",
"turbine_part_rotor",
"turbine_part_stator",
+ "watering_can",
)
/datum/techweb_node/mmi
@@ -880,37 +881,64 @@
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2000)
-/datum/techweb_node/ai
- id = "ai"
+/datum/techweb_node/ai_basic
+ id = "ai_basic"
display_name = "Artificial Intelligence"
description = "AI unit research."
prereq_ids = list("adv_robotics")
design_ids = list(
"aicore",
+ "borg_ai_control",
+ "intellicard",
+ "mecha_tracking_ai_control",
"aifixer",
"aiupload",
+ "reset_module",
"asimov_module",
- "borg_ai_control",
- "corporate_module",
"default_module",
- "freeform_module",
- "freeformcore_module",
- "intellicard",
- "mecha_tracking_ai_control",
- "onehuman_module",
- "overlord_module",
- "oxygen_module",
+ "nutimov_module",
"paladin_module",
+ "robocop_module",
+ "corporate_module",
+ "drone_module",
+ "oxygen_module",
+ "safeguard_module",
"protectstation_module",
- "purge_module",
"quarantine_module",
+ "freeform_module",
"remove_module",
- "reset_module",
- "safeguard_module",
- "tyrant_module",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+/datum/techweb_node/ai_adv
+ id = "ai_adv"
+ display_name = "Advanced Artificial Intelligence"
+ description = "State of the art lawsets to be used for AI research."
+ prereq_ids = list("ai_basic")
+ design_ids = list(
+ "asimovpp_module",
+ "paladin_devotion_module",
+ "dungeon_master_module",
+ "painter_module",
+ "ten_commandments_module",
+ "hippocratic_module",
+ "maintain_module",
+ "liveandletlive_module",
+ "reporter_module",
+ "hulkamania_module",
+ "peacekeeper_module",
+ "overlord_module",
+ "tyrant_module",
+ "antimov_module",
+ "balance_module",
+ "thermurderdynamic_module",
+ "damaged_module",
+ "freeformcore_module",
+ "onehuman_module",
+ "purge_module",
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 3000)
+
//Any kind of point adjustment needs to happen before SSresearch sets up the whole node tree, it gets cached
/datum/techweb_node/ai/New()
. = ..()
@@ -1175,7 +1203,7 @@
"cybernetic_stomach_tier2",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 1000)
-
+
/datum/techweb_node/cyber_organs_upgraded
id = "cyber_organs_upgraded"
@@ -1293,16 +1321,19 @@
id = "botany"
display_name = "Botanical Engineering"
description = "Botanical tools"
- prereq_ids = list("adv_engi", "biotech")
+ prereq_ids = list("biotech")
design_ids = list(
"biogenerator",
"flora_gun",
+ "gene_shears",
"hydro_tray",
"portaseeder",
"seed_extractor",
+ "adv_watering_can",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 4000)
- discount_experiments = list(/datum/experiment/scanning/random/plants/wild = 3000)
+ required_experiments = list(/datum/experiment/scanning/random/plants/wild)
+ discount_experiments = list(/datum/experiment/scanning/random/plants/traits = 3000)
/datum/techweb_node/exp_tools
id = "exp_tools"
@@ -1311,7 +1342,6 @@
prereq_ids = list("adv_engi")
design_ids = list(
"exwelder",
- "gene_shears",
"handdrill",
"jawsoflife",
"laserscalpel",
@@ -1538,6 +1568,8 @@
"mod_mag_harness",
"mod_pathfinder",
"mod_holster",
+ "mod_sonar",
+ "mod_projectile_dampener",
)
research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
@@ -2132,6 +2164,18 @@
hidden = TRUE
experimental = TRUE
+/datum/techweb_node/fishing
+ id = "fishing"
+ display_name = "Fishing Technology"
+ description = "Cutting edge fishing advancements."
+ prereq_ids = list("base")
+ design_ids = list(
+ "fishing_rod_tech"
+ )
+ research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 2500)
+ hidden = TRUE
+ experimental = TRUE
+
//Helpers for debugging/balancing the techweb in its entirety!
/proc/total_techweb_points()
var/list/datum/techweb_node/processing = list()
diff --git a/code/modules/research/xenobiology/crossbreeding/_misc.dm b/code/modules/research/xenobiology/crossbreeding/_misc.dm
index 3fcb987a1f179..90316d00e4af4 100644
--- a/code/modules/research/xenobiology/crossbreeding/_misc.dm
+++ b/code/modules/research/xenobiology/crossbreeding/_misc.dm
@@ -136,7 +136,7 @@ Slimecrossing Items
icon_state = "slimebarrier_thick"
can_atmos_pass = ATMOS_PASS_NO
opacity = TRUE
- timeleft = 100
+ initial_duration = 10 SECONDS
//Rainbow barrier - Chilling Rainbow
/obj/effect/forcefield/slimewall/rainbow
diff --git a/code/modules/research/xenobiology/crossbreeding/_mobs.dm b/code/modules/research/xenobiology/crossbreeding/_mobs.dm
index be45d28060846..14e4d56fb0bb0 100644
--- a/code/modules/research/xenobiology/crossbreeding/_mobs.dm
+++ b/code/modules/research/xenobiology/crossbreeding/_mobs.dm
@@ -4,30 +4,36 @@ Slimecrossing Mobs
Collected here for clarity.
*/
-//Slime transformation power - Burning Black
-/obj/effect/proc_holder/spell/targeted/shapeshift/slimeform
+/// Slime transformation power - from Burning Black
+/datum/action/cooldown/spell/shapeshift/slime_form
name = "Slime Transformation"
desc = "Transform from a human to a slime, or back again!"
- action_icon_state = "transformslime"
- cooldown_min = 0
- charge_max = 0
+ button_icon_state = "transformslime"
+ cooldown_time = 0 SECONDS
+
invocation_type = INVOCATION_NONE
- shapeshift_type = /mob/living/simple_animal/slime/transformedslime
+
convert_damage = TRUE
convert_damage_type = CLONE
+ possible_shapes = list(/mob/living/simple_animal/slime/transformed_slime)
+
+ /// If TRUE, we self-delete (remove ourselves) the next time we turn back into a human
var/remove_on_restore = FALSE
-/obj/effect/proc_holder/spell/targeted/shapeshift/slimeform/restore_form(mob/living/shape)
+/datum/action/cooldown/spell/shapeshift/slime_form/restore_form(mob/living/shape)
+ . = ..()
+ if(!.)
+ return
+
if(remove_on_restore)
- if(shape.mind)
- shape.mind.RemoveSpell(src)
- return ..()
+ qdel(src)
-//Transformed slime - Burning Black
-/mob/living/simple_animal/slime/transformedslime
+/// Transformed slime - from Burning Black
+/mob/living/simple_animal/slime/transformed_slime
-/mob/living/simple_animal/slime/transformedslime/Reproduce() //Just in case.
- to_chat(src, span_warning("I can't reproduce..."))
+// Just in case.
+/mob/living/simple_animal/slime/transformed_slime/Reproduce()
+ to_chat(src, span_warning("I can't reproduce...")) // Mood
return
//Slime corgi - Chilling Pink
diff --git a/code/modules/research/xenobiology/crossbreeding/burning.dm b/code/modules/research/xenobiology/crossbreeding/burning.dm
index 80a856ce4535f..1d162c53a7349 100644
--- a/code/modules/research/xenobiology/crossbreeding/burning.dm
+++ b/code/modules/research/xenobiology/crossbreeding/burning.dm
@@ -49,8 +49,8 @@ Burning extracts:
tmp_holder.add_reagent(/datum/reagent/consumable/condensedcapsaicin, 100)
var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
- smoke.set_up(7, location = get_turf(user), carry = tmp_holder)
- smoke.start()
+ smoke.set_up(7, holder = src, location = get_turf(user), carry = tmp_holder)
+ smoke.start(log = TRUE)
..()
/obj/item/slimecross/burning/purple
@@ -124,8 +124,8 @@ Burning extracts:
tmp_holder.add_reagent(/datum/reagent/consumable/frostoil, 40)
user.reagents.add_reagent(/datum/reagent/medicine/regen_jelly, 10)
var/datum/effect_system/fluid_spread/smoke/chem/smoke = new
- smoke.set_up(7, location = get_turf(user), carry = tmp_holder)
- smoke.start()
+ smoke.set_up(7, holder = src, location = get_turf(user), carry = tmp_holder)
+ smoke.start(log = TRUE)
..()
/obj/item/slimecross/burning/silver
@@ -276,15 +276,14 @@ Burning extracts:
effect_desc = "Transforms the user into a slime. They can transform back at will and do not lose any items."
/obj/item/slimecross/burning/black/do_effect(mob/user)
- var/mob/living/L = user
- if(!istype(L))
+ if(!isliving(user))
return
user.visible_message(span_danger("[src] absorbs [user], transforming [user.p_them()] into a slime!"))
- var/obj/effect/proc_holder/spell/targeted/shapeshift/slimeform/S = new()
- S.remove_on_restore = TRUE
- user.mind.AddSpell(S)
- S.cast(list(user),user)
- ..()
+ var/datum/action/cooldown/spell/shapeshift/slime_form/transform = new(user.mind || user)
+ transform.remove_on_restore = TRUE
+ transform.Grant(user)
+ transform.cast(user)
+ return ..()
/obj/item/slimecross/burning/lightpink
colour = "light pink"
diff --git a/code/modules/research/xenobiology/vatgrowing/biopsy_tool.dm b/code/modules/research/xenobiology/vatgrowing/biopsy_tool.dm
index d01e4a32138cb..176b4fc78e3c1 100644
--- a/code/modules/research/xenobiology/vatgrowing/biopsy_tool.dm
+++ b/code/modules/research/xenobiology/vatgrowing/biopsy_tool.dm
@@ -4,6 +4,7 @@
desc = "Don't worry, it won't sting."
icon = 'icons/obj/xenobiology/vatgrowing.dmi'
icon_state = "biopsy"
+ worn_icon_state = "biopsy"
///Adds the swabbing component to the biopsy tool
/obj/item/biopsy_tool/Initialize(mapload)
diff --git a/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm b/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm
index da1bf67c7adb0..edeeaad05c6a3 100644
--- a/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm
+++ b/code/modules/research/xenobiology/vatgrowing/samples/_micro_organism.dm
@@ -96,7 +96,7 @@
/datum/micro_organism/cell_line/proc/succeed_growing(obj/machinery/plumbing/growing_vat/vat)
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(0, location = vat.loc)
+ smoke.set_up(0, holder = vat, location = vat.loc)
smoke.start()
for(var/created_thing in resulting_atoms)
for(var/x in 1 to resulting_atoms[created_thing])
diff --git a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
index 1c2da8d5ac7ba..637c5d033f03d 100644
--- a/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
+++ b/code/modules/research/xenobiology/vatgrowing/samples/cell_lines/common.dm
@@ -474,7 +474,7 @@
resulting_atoms = list(/mob/living/simple_animal/hostile/netherworld = 1)
/datum/micro_organism/cell_line/netherworld/succeed_growing(obj/machinery/plumbing/growing_vat/vat)
- var/random_result = pick(typesof(/mob/living/simple_animal/hostile/netherworld)) //i looked myself, pretty much all of them are reasonably strong and somewhat on the same level. except migo is the jackpot and the blank body is whiff.
+ var/random_result = pick(typesof(/mob/living/simple_animal/hostile/netherworld) - /mob/living/simple_animal/hostile/netherworld/statue) //i looked myself, pretty much all of them are reasonably strong and somewhat on the same level. except migo is the jackpot and the blank body is whiff.
resulting_atoms = list()
resulting_atoms[random_result] = 1
return ..()
diff --git a/code/modules/research/xenobiology/xenobiology.dm b/code/modules/research/xenobiology/xenobiology.dm
index a1a00c2be10e5..69a51a8d8748d 100644
--- a/code/modules/research/xenobiology/xenobiology.dm
+++ b/code/modules/research/xenobiology/xenobiology.dm
@@ -316,7 +316,7 @@
return 250
if(SLIME_ACTIVATE_MAJOR)
- user.reagents.create_foam(/datum/effect_system/fluid_spread/foam, 20)
+ user.reagents.create_foam(/datum/effect_system/fluid_spread/foam, 20, log = TRUE)
user.visible_message(span_danger("Foam spews out from [user]'s skin!"), span_warning("You activate [src], and foam bursts out of your skin!"))
return 600
diff --git a/code/modules/security_levels/keycard_authentication.dm b/code/modules/security_levels/keycard_authentication.dm
index 26f43924410ca..738b65ec8680a 100644
--- a/code/modules/security_levels/keycard_authentication.dm
+++ b/code/modules/security_levels/keycard_authentication.dm
@@ -43,7 +43,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/keycard_auth, 26)
var/list/data = list()
data["waiting"] = waiting
data["auth_required"] = event_source ? event_source.event : 0
- data["red_alert"] = (seclevel2num(get_security_level()) >= SEC_LEVEL_RED) ? 1 : 0
+ data["red_alert"] = (SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED) ? 1 : 0
data["emergency_maint"] = GLOB.emergency_access
data["bsa_unlock"] = GLOB.bsa_unlock
return data
@@ -131,7 +131,7 @@ MAPPING_DIRECTIONAL_HELPERS(/obj/machinery/keycard_auth, 26)
deadchat_broadcast(" confirmed [event] at [span_name("[A2.name]")].", span_name("[confirmer]"), confirmer, message_type=DEADCHAT_ANNOUNCEMENT)
switch(event)
if(KEYCARD_RED_ALERT)
- set_security_level(SEC_LEVEL_RED)
+ SSsecurity_level.set_level(SEC_LEVEL_RED)
if(KEYCARD_EMERGENCY_MAINTENANCE_ACCESS)
make_maint_all_access()
if(KEYCARD_BSA_UNLOCK)
diff --git a/code/modules/security_levels/security_level_datums.dm b/code/modules/security_levels/security_level_datums.dm
new file mode 100644
index 0000000000000..c6addd4d4ebab
--- /dev/null
+++ b/code/modules/security_levels/security_level_datums.dm
@@ -0,0 +1,82 @@
+/**
+ * Security levels
+ *
+ * These are used by the security level subsystem. Each one of these represents a security level that a player can set.
+ *
+ * Base type is abstract
+ */
+
+/datum/security_level
+ /// The name of this security level.
+ var/name = "not set"
+ /// The numerical level of this security level, see defines for more information.
+ var/number_level = -1
+ /// The sound that we will play when this security level is set
+ var/sound
+ /// The looping sound that will be played while the security level is set
+ var/looping_sound
+ /// The looping sound interval
+ var/looping_sound_interval
+ /// The shuttle call time modification of this security level
+ var/shuttle_call_time_mod = 0
+ /// Our announcement when lowering to this level
+ var/lowering_to_announcement
+ /// Our announcement when elevating to this level
+ var/elevating_to_announcemnt
+ /// Our configuration key for lowering to text, if set, will override the default lowering to announcement.
+ var/lowering_to_configuration_key
+ /// Our configuration key for elevating to text, if set, will override the default elevating to announcement.
+ var/elevating_to_configuration_key
+
+/datum/security_level/New()
+ . = ..()
+ if(lowering_to_configuration_key) // I'm not sure about you, but isn't there an easier way to do this?
+ lowering_to_announcement = global.config.Get(lowering_to_configuration_key)
+ if(elevating_to_configuration_key)
+ elevating_to_announcemnt = global.config.Get(elevating_to_configuration_key)
+
+/**
+ * GREEN
+ *
+ * No threats
+ */
+/datum/security_level/green
+ name = "green"
+ number_level = SEC_LEVEL_GREEN
+ lowering_to_configuration_key = /datum/config_entry/string/alert_green
+ shuttle_call_time_mod = 2
+
+/**
+ * BLUE
+ *
+ * Caution advised
+ */
+/datum/security_level/blue
+ name = "blue"
+ number_level = SEC_LEVEL_BLUE
+ lowering_to_configuration_key = /datum/config_entry/string/alert_blue_downto
+ elevating_to_configuration_key = /datum/config_entry/string/alert_blue_upto
+ shuttle_call_time_mod = 1
+
+/**
+ * RED
+ *
+ * Hostile threats
+ */
+/datum/security_level/red
+ name = "red"
+ number_level = SEC_LEVEL_RED
+ lowering_to_configuration_key = /datum/config_entry/string/alert_red_downto
+ elevating_to_configuration_key = /datum/config_entry/string/alert_red_upto
+ shuttle_call_time_mod = 0.5
+
+/**
+ * DELTA
+ *
+ * Station destruction is imminent
+ */
+/datum/security_level/delta
+ name = "delta"
+ number_level = SEC_LEVEL_DELTA
+ elevating_to_configuration_key = /datum/config_entry/string/alert_delta
+ shuttle_call_time_mod = 0.25
diff --git a/code/modules/security_levels/security_levels.dm b/code/modules/security_levels/security_levels.dm
deleted file mode 100644
index c289f90b6073e..0000000000000
--- a/code/modules/security_levels/security_levels.dm
+++ /dev/null
@@ -1,82 +0,0 @@
-/proc/set_security_level(level)
- switch(level)
- if("green")
- level = SEC_LEVEL_GREEN
- if("blue")
- level = SEC_LEVEL_BLUE
- if("red")
- level = SEC_LEVEL_RED
- if("delta")
- level = SEC_LEVEL_DELTA
-
- //Will not be announced if you try to set to the same level as it already is
- if(level >= SEC_LEVEL_GREEN && level <= SEC_LEVEL_DELTA && level != SSsecurity_level.current_level)
- switch(level)
- if(SEC_LEVEL_GREEN)
- minor_announce(CONFIG_GET(string/alert_green), "Attention! Security level lowered to green:")
- if(SSshuttle.emergency.mode == SHUTTLE_CALL || SSshuttle.emergency.mode == SHUTTLE_RECALL)
- if(SSsecurity_level.current_level >= SEC_LEVEL_RED)
- SSshuttle.emergency.modTimer(4)
- else
- SSshuttle.emergency.modTimer(2)
- if(SEC_LEVEL_BLUE)
- if(SSsecurity_level.current_level < SEC_LEVEL_BLUE)
- minor_announce(CONFIG_GET(string/alert_blue_upto), "Attention! Security level elevated to blue:",1)
- if(SSshuttle.emergency.mode == SHUTTLE_CALL || SSshuttle.emergency.mode == SHUTTLE_RECALL)
- SSshuttle.emergency.modTimer(0.5)
- else
- minor_announce(CONFIG_GET(string/alert_blue_downto), "Attention! Security level lowered to blue:")
- if(SSshuttle.emergency.mode == SHUTTLE_CALL || SSshuttle.emergency.mode == SHUTTLE_RECALL)
- SSshuttle.emergency.modTimer(2)
- if(SEC_LEVEL_RED)
- if(SSsecurity_level.current_level < SEC_LEVEL_RED)
- minor_announce(CONFIG_GET(string/alert_red_upto), "Attention! Code red!",1)
- if(SSshuttle.emergency.mode == SHUTTLE_CALL || SSshuttle.emergency.mode == SHUTTLE_RECALL)
- if(SSsecurity_level.current_level == SEC_LEVEL_GREEN)
- SSshuttle.emergency.modTimer(0.25)
- else
- SSshuttle.emergency.modTimer(0.5)
- else
- minor_announce(CONFIG_GET(string/alert_red_downto), "Attention! Code red!")
- if(SEC_LEVEL_DELTA)
- minor_announce(CONFIG_GET(string/alert_delta), "Attention! Delta security level reached!",1)
- if(SSshuttle.emergency.mode == SHUTTLE_CALL || SSshuttle.emergency.mode == SHUTTLE_RECALL)
- if(SSsecurity_level.current_level == SEC_LEVEL_GREEN)
- SSshuttle.emergency.modTimer(0.25)
- else if(SSsecurity_level.current_level == SEC_LEVEL_BLUE)
- SSshuttle.emergency.modTimer(0.5)
-
- SSsecurity_level.set_level(level)
-
-/proc/get_security_level()
- switch(SSsecurity_level.current_level)
- if(SEC_LEVEL_GREEN)
- return "green"
- if(SEC_LEVEL_BLUE)
- return "blue"
- if(SEC_LEVEL_RED)
- return "red"
- if(SEC_LEVEL_DELTA)
- return "delta"
-
-/proc/num2seclevel(num)
- switch(num)
- if(SEC_LEVEL_GREEN)
- return "green"
- if(SEC_LEVEL_BLUE)
- return "blue"
- if(SEC_LEVEL_RED)
- return "red"
- if(SEC_LEVEL_DELTA)
- return "delta"
-
-/proc/seclevel2num(seclevel)
- switch( lowertext(seclevel) )
- if("green")
- return SEC_LEVEL_GREEN
- if("blue")
- return SEC_LEVEL_BLUE
- if("red")
- return SEC_LEVEL_RED
- if("delta")
- return SEC_LEVEL_DELTA
diff --git a/code/modules/shuttle/emergency.dm b/code/modules/shuttle/emergency.dm
index 4ee761e74dd54..73b613d297039 100644
--- a/code/modules/shuttle/emergency.dm
+++ b/code/modules/shuttle/emergency.dm
@@ -211,7 +211,7 @@
if(SSshuttle.emergency.hijack_status >= HIJACKED)
to_chat(user, span_warning("The emergency shuttle is already loaded with a corrupt navigational payload. What more do you want from it?"))
return
- if(hijack_last_stage_increase >= world.time + hijack_stage_cooldown)
+ if(hijack_last_stage_increase >= world.time - hijack_stage_cooldown)
say("Error - Catastrophic software error detected. Input is currently on timeout.")
return
hijack_hacking = TRUE
@@ -246,7 +246,9 @@
if(HIJACKED)
msg = "SYSTEM OVERRIDE - Resetting course to \[[scramble_message_replace_chars("###########", 100)]\] \
([scramble_message_replace_chars("#######", 100)]/[scramble_message_replace_chars("#######", 100)]/[scramble_message_replace_chars("#######", 100)]) \
- {AUTH - ROOT (uid: 0)}.[SSshuttle.emergency.mode == SHUTTLE_ESCAPE ? "Diverting from existing route - Bluespace exit in [hijack_completion_flight_time_set/10] seconds." : ""]"
+ {AUTH - ROOT (uid: 0)}.\
+ [SSshuttle.emergency.mode == SHUTTLE_ESCAPE ? "Diverting from existing route - Bluespace exit in \
+ [hijack_completion_flight_time_set >= INFINITY ? "[scramble_message_replace_chars("\[ERROR\]")]" : hijack_completion_flight_time_set/10] seconds." : ""]"
minor_announce(scramble_message_replace_chars(msg, replaceprob = 10), "Emergency Shuttle", TRUE)
/obj/machinery/computer/emergency_shuttle/emag_act(mob/user)
@@ -319,7 +321,7 @@
/obj/docking_port/mobile/emergency/request(obj/docking_port/stationary/S, area/signalOrigin, reason, redAlert, set_coefficient=null)
if(!isnum(set_coefficient))
- var/security_num = seclevel2num(get_security_level())
+ var/security_num = SSsecurity_level.get_current_level_as_number()
switch(security_num)
if(SEC_LEVEL_GREEN)
set_coefficient = 2
@@ -569,7 +571,7 @@
var/obj/machinery/computer/shuttle/C = getControlConsole()
if(!istype(C, /obj/machinery/computer/shuttle/pod))
return ..()
- if(SSsecurity_level.current_level >= SEC_LEVEL_RED || (C && (C.obj_flags & EMAGGED)))
+ if(SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED || (C && (C.obj_flags & EMAGGED)))
if(launch_status == UNLAUNCHED)
launch_status = EARLY_LAUNCHED
return ..()
@@ -730,7 +732,7 @@
/obj/item/storage/pod/can_interact(mob/user)
if(!..())
return FALSE
- if(SSsecurity_level.current_level >= SEC_LEVEL_RED || unlocked)
+ if(SSsecurity_level.get_current_level_as_number() >= SEC_LEVEL_RED || unlocked)
return TRUE
to_chat(user, "The storage unit will only unlock during a Red or Delta security alert.")
return FALSE
diff --git a/code/modules/shuttle/on_move.dm b/code/modules/shuttle/on_move.dm
index 2ddc73e25277d..2af77fe49fddb 100644
--- a/code/modules/shuttle/on_move.dm
+++ b/code/modules/shuttle/on_move.dm
@@ -99,7 +99,7 @@ All ShuttleMove procs go here
/atom/movable/proc/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock)
return move_mode
-// Called on atoms to move the atom to the new location
+/// Called on atoms to move the atom to the new location
/atom/movable/proc/onShuttleMove(turf/newT, turf/oldT, list/movement_force, move_dir, obj/docking_port/stationary/old_dock, obj/docking_port/mobile/moving_dock)
if(newT == oldT) // In case of in place shuttle rotation shenanigans.
return
@@ -219,17 +219,6 @@ All ShuttleMove procs go here
if(is_mining_level(z)) //Avoids double logging and landing on other Z-levels due to badminnery
SSblackbox.record_feedback("associative", "colonies_dropped", 1, list("x" = x, "y" = y, "z" = z))
-/obj/machinery/gravity_generator/main/beforeShuttleMove(turf/newT, rotation, move_mode, obj/docking_port/mobile/moving_dock)
- . = ..()
- on = FALSE
- update_list()
-
-/obj/machinery/gravity_generator/main/afterShuttleMove(turf/oldT, list/movement_force, shuttle_dir, shuttle_preferred_direction, move_dir, rotation)
- . = ..()
- if(charge_count != 0 && charging_state != POWER_UP)
- on = TRUE
- update_list()
-
/obj/machinery/atmospherics/afterShuttleMove(turf/oldT, list/movement_force, shuttle_dir, shuttle_preferred_direction, move_dir, rotation)
. = ..()
var/missing_nodes = FALSE
diff --git a/code/modules/shuttle/shuttle.dm b/code/modules/shuttle/shuttle.dm
index 6eee9c119a8bb..6da991ff2777d 100644
--- a/code/modules/shuttle/shuttle.dm
+++ b/code/modules/shuttle/shuttle.dm
@@ -39,7 +39,7 @@
///are we registered in SSshuttles?
var/registered = FALSE
- ///register to SSshuttles
+///register to SSshuttles
/obj/docking_port/proc/register()
if(registered)
WARNING("docking_port registered multiple times")
@@ -47,7 +47,7 @@
registered = TRUE
return
- ///unregister from SSshuttles
+///unregister from SSshuttles
/obj/docking_port/proc/unregister()
if(!registered)
WARNING("docking_port unregistered multiple times")
@@ -57,7 +57,7 @@
/obj/docking_port/proc/Check_id()
return
- //these objects are indestructible
+//these objects are indestructible
/obj/docking_port/Destroy(force)
// unless you assert that you know what you're doing. Horrible things
// may result.
@@ -68,7 +68,7 @@
return QDEL_HINT_LETMELIVE
/obj/docking_port/has_gravity(turf/T)
- return FALSE
+ return TRUE
/obj/docking_port/take_damage()
return
diff --git a/code/modules/shuttle/shuttle_rotate.dm b/code/modules/shuttle/shuttle_rotate.dm
index 8d0f8c289bb7a..a7ea83579e6ed 100644
--- a/code/modules/shuttle/shuttle_rotate.dm
+++ b/code/modules/shuttle/shuttle_rotate.dm
@@ -98,11 +98,6 @@ If ever any of these procs are useful for non-shuttles, rename it to proc/rotate
params = NONE
return ..()
-//prevents shuttles attempting to rotate this since it messes up sprites
-/obj/machinery/gravity_generator/shuttleRotate(rotation, params)
- params = NONE
- return ..()
-
/obj/machinery/door/airlock/shuttleRotate(rotation, params)
. = ..()
if(cyclelinkeddir && (params & ROTATE_DIR))
diff --git a/code/modules/spells/spell.dm b/code/modules/spells/spell.dm
index 6188ec76be4a6..5fdf82a9a794c 100644
--- a/code/modules/spells/spell.dm
+++ b/code/modules/spells/spell.dm
@@ -1,609 +1,425 @@
-#define TARGET_CLOSEST 0
-#define TARGET_RANDOM 1
-
-
-/obj/effect/proc_holder
- var/panel = "Debug"//What panel the proc holder needs to go on.
- var/active = FALSE //Used by toggle based abilities.
- var/ranged_mousepointer
- var/mob/living/ranged_ability_user
- var/ranged_clickcd_override = -1
- var/has_action = TRUE
- var/datum/action/spell_action/action = null
- var/action_icon = 'icons/mob/actions/actions_spells.dmi'
- var/action_icon_state = "spell_default"
- var/action_background_icon_state = "bg_spell"
- var/base_action = /datum/action/spell_action
- var/datum/weakref/owner
-
-/obj/effect/proc_holder/Initialize(mapload, mob/living/new_owner)
- . = ..()
- owner = WEAKREF(new_owner)
- if(has_action)
- action = new base_action(src)
-
-/obj/effect/proc_holder/Destroy()
- if(!QDELETED(action))
- qdel(action)
- action = null
- return ..()
-
-/obj/effect/proc_holder/proc/on_gain(mob/living/user)
- return
-
-/obj/effect/proc_holder/proc/on_lose(mob/living/user)
- return
-
-/obj/effect/proc_holder/proc/fire(mob/living/user)
- return TRUE
-
-/obj/effect/proc_holder/proc/get_panel_text()
- return ""
-
-GLOBAL_LIST_INIT(spells, typesof(/obj/effect/proc_holder/spell)) //needed for the badmin verb for now
-
-/obj/effect/proc_holder/Destroy()
- QDEL_NULL(action)
- if(ranged_ability_user)
- remove_ranged_ability()
- return ..()
-
-/obj/effect/proc_holder/singularity_act()
- return
-
-/obj/effect/proc_holder/singularity_pull()
- return
-
-/obj/effect/proc_holder/proc/InterceptClickOn(mob/living/caller, params, atom/A)
- if(caller.ranged_ability != src || ranged_ability_user != caller) //I'm not actually sure how these would trigger, but, uh, safety, I guess?
- to_chat(caller, span_warning("[caller.ranged_ability.name] has been disabled."))
- caller.ranged_ability.remove_ranged_ability()
- return TRUE //TRUE for failed, FALSE for passed.
- if(ranged_clickcd_override >= 0)
- ranged_ability_user.next_click = world.time + ranged_clickcd_override
- else
- ranged_ability_user.next_click = world.time + CLICK_CD_CLICK_ABILITY
- ranged_ability_user.face_atom(A)
- return FALSE
-
-/obj/effect/proc_holder/proc/add_ranged_ability(mob/living/user, msg, forced)
- if(!user || !user.client)
- return
- if(user.ranged_ability && user.ranged_ability != src)
- if(forced)
- to_chat(user, span_warning("[user.ranged_ability.name] has been replaced by [name]."))
- user.ranged_ability.remove_ranged_ability()
- else
- return
- user.ranged_ability = src
- user.click_intercept = src
- user.update_mouse_pointer()
- ranged_ability_user = user
- if(msg)
- to_chat(ranged_ability_user, msg)
- active = TRUE
- update_appearance()
-
-/obj/effect/proc_holder/proc/remove_ranged_ability(msg)
- if(!ranged_ability_user || !ranged_ability_user.client || (ranged_ability_user.ranged_ability && ranged_ability_user.ranged_ability != src)) //To avoid removing the wrong ability
- return
- ranged_ability_user.ranged_ability = null
- ranged_ability_user.click_intercept = null
- ranged_ability_user.update_mouse_pointer()
- if(msg)
- to_chat(ranged_ability_user, msg)
- ranged_ability_user = null
- active = FALSE
- update_appearance()
-
-/obj/effect/proc_holder/spell
+/**
+ * # The spell action
+ *
+ * This is the base action for how many of the game's
+ * spells (and spell adjacent) abilities function.
+ * These spells function off of a cooldown-based system.
+ *
+ * ## Pre-spell checks:
+ * - [can_cast_spell][/datum/action/cooldown/spell/can_cast_spell] checks if the OWNER
+ * of the spell is able to cast the spell.
+ * - [is_valid_target][/datum/action/cooldown/spell/is_valid_target] checks if the TARGET
+ * THE SPELL IS BEING CAST ON is a valid target for the spell. NOTE: The CAST TARGET is often THE SAME as THE OWNER OF THE SPELL,
+ * but is not always - depending on how [Pre Activate][/datum/action/cooldown/spell/PreActivate] is resolved.
+ * - [can_invoke][/datum/action/cooldown/spell/can_invoke] is run in can_cast_spell to check if
+ * the OWNER of the spell is able to say the current invocation.
+ *
+ * ## The spell chain:
+ * - [before_cast][/datum/action/cooldown/spell/before_cast] is the last chance for being able
+ * to interrupt a spell cast. This returns a bitflag. if SPELL_CANCEL_CAST is set, the spell will not continue.
+ * - [spell_feedback][/datum/action/cooldown/spell/spell_feedback] is called right before cast, and handles
+ * invocation and sound effects. Overridable, if you want a special method of invocation or sound effects,
+ * or you want your spell to handle invocation / sound via special means.
+ * - [cast][/datum/action/cooldown/spell/cast] is where the brunt of the spell effects should be done
+ * and implemented.
+ * - [after_cast][/datum/action/cooldown/spell/after_cast] is the aftermath - final effects that follow
+ * the main cast of the spell. By now, the spell cooldown has already started
+ *
+ * ## Other procs called / may be called within the chain:
+ * - [invocation][/datum/action/cooldown/spell/invocation] handles saying any vocal (or emotive) invocations the spell
+ * may have, and can be overriden or extended. Called by spell_feedback.
+ * - [reset_spell_cooldown][/datum/action/cooldown/spell/reset_spell_cooldown] is a way to handle reverting a spell's
+ * cooldown and making it ready again if it fails to go off at any point. Not called anywhere by default. If you
+ * want to cancel a spell in before_cast and would like the cooldown restart, call this.
+ *
+ * ## Other procs of note:
+ * - [level_spell][/datum/action/cooldown/spell/level_spell] is where the process of adding a spell level is handled.
+ * this can be extended if you wish to add unique effects on level up for wizards.
+ * - [delevel_spell][/datum/action/cooldown/spell/delevel_spell] is where the process of removing a spell level is handled.
+ * this can be extended if you wish to undo unique effects on level up for wizards.
+ * - [update_spell_name][/datum/action/cooldown/spell/update_spell_name] updates the prefix of the spell name based on its level.
+ */
+/datum/action/cooldown/spell
name = "Spell"
desc = "A wizard spell."
+ background_icon_state = "bg_spell"
+ icon_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "spell_default"
+ check_flags = AB_CHECK_CONSCIOUS
panel = "Spells"
- var/sound = null //The sound the spell makes when it is cast
- anchored = TRUE // Crap like fireball projectiles are proc_holders, this is needed so fireballs don't get blown back into your face via atmos etc.
- pass_flags = PASSTABLE
- density = FALSE
- opacity = FALSE
- ///checked by some holy sects to punish the caster for casting things that do not align with their sect's alignment - see magic.dm in defines to learn more
+ /// The sound played on cast.
+ var/sound = null
+ /// The school of magic the spell belongs to.
+ /// Checked by some holy sects to punish the
+ /// caster for casting things that do not align
+ /// with their sect's alignment - see magic.dm in defines to learn more
var/school = SCHOOL_UNSET
+ /// If the spell uses the wizard spell rank system, the cooldown reduction per rank of the spell
+ var/cooldown_reduction_per_rank = 0 SECONDS
+ /// What is uttered when the user casts the spell
+ var/invocation
+ /// What is shown in chat when the user casts the spell, only matters for INVOCATION_EMOTE
+ var/invocation_self_message
+ /// What type of invocation the spell is.
+ /// Can be "none", "whisper", "shout", "emote"
+ var/invocation_type = INVOCATION_NONE
+ /// Flag for certain states that the spell requires the user be in to cast.
+ var/spell_requirements = SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_NO_ANTIMAGIC
+ /// This determines what type of antimagic is needed to block the spell.
+ /// (MAGIC_RESISTANCE, MAGIC_RESISTANCE_MIND, MAGIC_RESISTANCE_HOLY)
+ /// If SPELL_REQUIRES_NO_ANTIMAGIC is set in Spell requirements,
+ /// The spell cannot be cast if the caster has any of the antimagic flags set.
+ var/antimagic_flags = MAGIC_RESISTANCE
+ /// The current spell level, if taken multiple times by a wizard
+ var/spell_level = 1
+ /// The max possible spell level
+ var/spell_max_level = 5
+ /// If set to a positive number, the spell will produce sparks when casted.
+ var/sparks_amt = 0
+ /// The typepath of the smoke to create on cast.
+ var/smoke_type
+ /// The amount of smoke to create on cast. This is a range, so a value of 5 will create enough smoke to cover everything within 5 steps.
+ var/smoke_amt = 0
- var/charge_type = "recharge" //can be recharge or charges, see charge_max and charge_counter descriptions; can also be based on the holder's vars now, use "holder_var" for that
+/datum/action/cooldown/spell/Grant(mob/grant_to)
+ // If our spell is mind-bound, we only wanna grant it to our mind
+ if(istype(target, /datum/mind))
+ var/datum/mind/mind_target = target
+ if(mind_target.current != grant_to)
+ return
+
+ . = ..()
+ if(!owner)
+ return
- var/charge_max = 10 SECONDS //recharge time in deciseconds if charge_type = "recharge" or starting charges if charge_type = "charges"
- var/charge_counter = 0 //can only cast spells if it equals recharge, ++ each decisecond if charge_type = "recharge" or -- each cast if charge_type = "charges"
- var/still_recharging_msg = "The spell is still recharging."
- var/recharging = TRUE
+ // Register some signals so our button's icon stays up to date
+ if(spell_requirements & SPELL_REQUIRES_OFF_CENTCOM)
+ RegisterSignal(owner, COMSIG_MOVABLE_Z_CHANGED, .proc/update_icon_on_signal)
+ if(spell_requirements & (SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_WIZARD_GARB))
+ RegisterSignal(owner, COMSIG_MOB_EQUIPPED_ITEM, .proc/update_icon_on_signal)
+ RegisterSignal(owner, list(COMSIG_MOB_ENTER_JAUNT, COMSIG_MOB_AFTER_EXIT_JAUNT), .proc/update_icon_on_signal)
+ owner.client?.stat_panel.send_message("check_spells")
- var/holder_var_type = "bruteloss" //only used if charge_type equals to "holder_var"
- var/holder_var_amount = 20 //same. The amount adjusted with the mob's var when the spell is used
+/datum/action/cooldown/spell/Remove(mob/living/remove_from)
- var/clothes_req = TRUE //see if it requires clothes
- var/human_req = FALSE //spell can only be cast by humans
- var/nonabstract_req = FALSE //spell can only be cast by mobs that are physical entities
- var/stat_allowed = FALSE //see if it requires being conscious/alive, need to set to 1 for ghostpells
- var/phase_allowed = FALSE // If true, the spell can be cast while phased, eg. blood crawling, ethereal jaunting
+ remove_from.client?.stat_panel.send_message("check_spells")
+ UnregisterSignal(remove_from, list(
+ COMSIG_MOB_AFTER_EXIT_JAUNT,
+ COMSIG_MOB_ENTER_JAUNT,
+ COMSIG_MOB_EQUIPPED_ITEM,
+ COMSIG_MOVABLE_Z_CHANGED,
+ ))
- /// This determines what type of antimagic is needed to block the spell (MAGIC_RESISTANCE, MAGIC_RESISTANCE_MIND, MAGIC_RESISTANCE_HOLY)
- var/antimagic_flags = MAGIC_RESISTANCE
+ return ..()
- var/invocation = "HURP DURP" //what is uttered when the wizard casts the spell
- var/invocation_emote_self = null
- var/invocation_type = INVOCATION_NONE //can be none, whisper, emote and shout
- var/range = 7 //the range of the spell; outer radius for aoe spells
- var/message = "" //whatever it says to the guy affected by it
- var/selection_type = "view" //can be "range" or "view"
- var/spell_level = 0 //if a spell can be taken multiple times, this raises
- var/level_max = 4 //The max possible level_max is 4
- var/cooldown_min = 0 //This defines what spell quickened four times has as a cooldown. Make sure to set this for every spell
- var/player_lock = TRUE //If it can be used by simple mobs
-
- var/overlay = 0
- var/overlay_icon = 'icons/obj/wizard.dmi'
- var/overlay_icon_state = "spell"
- var/overlay_lifespan = 0
-
- var/sparks_spread = 0
- var/sparks_amt = 0 //cropped at 10
- /// The typepath of the smoke to create on cast.
- var/smoke_spread = null
- /// The amount of smoke to create on case. This is a range so a value of 5 will create enough smoke to cover everything within 5 steps.
- var/smoke_amt = 0
+/datum/action/cooldown/spell/IsAvailable()
+ return ..() && can_cast_spell(feedback = FALSE)
- var/centcom_cancast = TRUE //Whether or not the spell should be allowed on z2
+/datum/action/cooldown/spell/Trigger(trigger_flags, atom/target)
+ // We implement this can_cast_spell check before the parent call of Trigger()
+ // to allow people to click unavailable abilities to get a feedback chat message
+ // about why the ability is unavailable.
+ // It is otherwise redundant, however, as IsAvailable() checks can_cast_spell as well.
+ if(!can_cast_spell())
+ return FALSE
- action_icon = 'icons/mob/actions/actions_spells.dmi'
- action_icon_state = "spell_default"
- action_background_icon_state = "bg_spell"
- base_action = /datum/action/spell_action/spell
+ return ..()
-/obj/effect/proc_holder/spell/proc/cast_check(skipcharge = 0,mob/user = usr) //checks if the spell can be cast based on its settings; skipcharge is used when an additional cast_check is called inside the spell
- if(SEND_SIGNAL(user, COMSIG_MOB_PRE_CAST_SPELL, src) & COMPONENT_CANCEL_SPELL)
+/datum/action/cooldown/spell/set_click_ability(mob/on_who)
+ if(SEND_SIGNAL(on_who, COMSIG_MOB_SPELL_ACTIVATED, src) & SPELL_CANCEL_CAST)
return FALSE
- if(player_lock)
- if(!user.mind || !(src in user.mind.spell_list) && !(src in user.mob_spell_list))
- to_chat(user, span_warning("You shouldn't have this spell! Something's wrong."))
- return FALSE
- else
- if(!(src in user.mob_spell_list))
- return FALSE
+ return ..()
- var/turf/T = get_turf(user)
- if(is_centcom_level(T.z) && !centcom_cancast) //Certain spells are not allowed on the centcom zlevel
- to_chat(user, span_warning("You can't cast this spell here!"))
+// Where the cast chain starts
+/datum/action/cooldown/spell/PreActivate(atom/target)
+ if(!is_valid_target(target))
return FALSE
- if(!skipcharge)
- if(!charge_check(user))
- return FALSE
+ return Activate(target)
- if(user.stat && !stat_allowed)
- to_chat(user, span_warning("Not when you're incapacitated!"))
+/// Checks if the owner of the spell can currently cast it.
+/// Does not check anything involving potential targets.
+/datum/action/cooldown/spell/proc/can_cast_spell(feedback = TRUE)
+ if(!owner)
+ CRASH("[type] - can_cast_spell called on a spell without an owner!")
+
+ // Certain spells are not allowed on the centcom zlevel
+ var/turf/caster_turf = get_turf(owner)
+ if((spell_requirements & SPELL_REQUIRES_OFF_CENTCOM) && is_centcom_level(caster_turf.z))
+ if(feedback)
+ to_chat(owner, span_warning("You can't cast [src] here!"))
return FALSE
- if(!user.can_cast_magic(antimagic_flags))
+ if((spell_requirements & SPELL_REQUIRES_MIND) && !owner.mind)
+ // No point in feedback here, as mindless mobs aren't players
return FALSE
- if(!phase_allowed && istype(user.loc, /obj/effect/dummy) || HAS_TRAIT(user, TRAIT_ROD_FORM))
- to_chat(user, span_warning("[name] cannot be cast unless you are completely manifested in the material plane!"))
+ if((spell_requirements & SPELL_REQUIRES_MIME_VOW) && !owner.mind?.miming)
+ // In the future this can be moved out of spell checks exactly
+ if(feedback)
+ to_chat(owner, span_warning("You must dedicate yourself to silence first!"))
return FALSE
- var/mob/living/L = user
- if(istype(L) && (invocation_type == INVOCATION_WHISPER || invocation_type == INVOCATION_SHOUT) && !L.can_speak_vocal())
- to_chat(user, span_warning("You can't get the words out!"))
+ // If the spell requires the user has no antimagic equipped, and they're holding antimagic
+ // that corresponds with the spell's antimagic, then they can't actually cast the spell
+ if((spell_requirements & SPELL_REQUIRES_NO_ANTIMAGIC) && !owner.can_cast_magic(antimagic_flags))
+ if(feedback)
+ to_chat(owner, span_warning("Some form of antimagic is preventing you from casting [src]!"))
return FALSE
- if(ishuman(user))
+ if(!(spell_requirements & SPELL_CASTABLE_WHILE_PHASED) && HAS_TRAIT(owner, TRAIT_MAGICALLY_PHASED))
+ if(feedback)
+ to_chat(owner, span_warning("[src] cannot be cast unless you are completely manifested in the material plane!"))
+ return FALSE
- var/mob/living/carbon/human/H = user
+ if(!can_invoke(feedback = feedback))
+ return FALSE
- if(clothes_req)
- if(!(H.wear_suit?.clothing_flags & CASTING_CLOTHES))
- to_chat(H, span_warning("You don't feel strong enough without your robe!"))
+ if(ishuman(owner))
+ if(spell_requirements & SPELL_REQUIRES_WIZARD_GARB)
+ var/mob/living/carbon/human/human_owner = owner
+ if(!(human_owner.wear_suit?.clothing_flags & CASTING_CLOTHES))
+ to_chat(owner, span_warning("You don't feel strong enough without your robe!"))
return FALSE
- if(!(H.head?.clothing_flags & CASTING_CLOTHES))
- to_chat(H, span_warning("You don't feel strong enough without your hat!"))
+ if(!(human_owner.head?.clothing_flags & CASTING_CLOTHES))
+ to_chat(owner, span_warning("You don't feel strong enough without your hat!"))
return FALSE
+
else
- if(clothes_req || human_req)
- to_chat(user, span_warning("This spell can only be cast by humans!"))
+ // If the spell requires wizard equipment and we're not a human (can't wear robes or hats), that's just a given
+ if(spell_requirements & (SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_HUMAN))
+ if(feedback)
+ to_chat(owner, span_warning("[src] can only be cast by humans!"))
return FALSE
- if(nonabstract_req && (isbrain(user) || ispAI(user)))
- to_chat(user, span_warning("This spell can only be cast by physical beings!"))
+
+ if(!(spell_requirements & SPELL_CASTABLE_AS_BRAIN) && isbrain(owner))
+ if(feedback)
+ to_chat(owner, span_warning("[src] can't be cast in this state!"))
return FALSE
+ // Being put into a card form breaks a lot of spells, so we'll just forbid them in these states
+ if(ispAI(owner) || (isAI(owner) && istype(owner.loc, /obj/item/aicard)))
+ return FALSE
- if(!skipcharge)
- switch(charge_type)
- if("recharge")
- charge_counter = 0 //doesn't start recharging until the targets selecting ends
- if("charges")
- charge_counter-- //returns the charge if the targets selecting fails
- if("holdervar")
- adjust_var(user, holder_var_type, holder_var_amount)
- if(action)
- action.UpdateButtons()
return TRUE
-/obj/effect/proc_holder/spell/proc/charge_check(mob/user, silent = FALSE)
- switch(charge_type)
- if("recharge")
- if(charge_counter < charge_max)
- if(!silent)
- to_chat(user, still_recharging_msg)
- return FALSE
- if("charges")
- if(!charge_counter)
- if(!silent)
- to_chat(user, span_warning("[name] has no charges left!"))
- return FALSE
+/**
+ * Check if the target we're casting on is a valid target.
+ * For self-casted spells, the target being checked (cast_on) is the caster.
+ *
+ * Return TRUE if cast_on is valid, FALSE otherwise
+ */
+/datum/action/cooldown/spell/proc/is_valid_target(atom/cast_on)
return TRUE
-/obj/effect/proc_holder/spell/proc/invocation(mob/user = usr) //spelling the spell out and setting it on recharge/reducing charges amount
- switch(invocation_type)
- if(INVOCATION_SHOUT)
- if(prob(50))//Auto-mute? Fuck that noise
- user.say(invocation, forced = "spell")
- else
- user.say(replacetext(invocation," ","`"), forced = "spell")
- if(INVOCATION_WHISPER)
- if(prob(50))
- user.whisper(invocation)
- else
- user.whisper(replacetext(invocation," ","`"))
- if(INVOCATION_EMOTE)
- user.visible_message(invocation, invocation_emote_self) //same style as in mob/living/emote.dm
+// The actual cast chain occurs here, in Activate().
+// You should generally not be overriding or extending Activate() for spells.
+// Defer to any of the cast chain procs instead.
+/datum/action/cooldown/spell/Activate(atom/cast_on)
+ SHOULD_NOT_OVERRIDE(TRUE)
+
+ // Pre-casting of the spell
+ // Pre-cast is the very last chance for a spell to cancel
+ // Stuff like target input can go here.
+ var/precast_result = before_cast(cast_on)
+ if(precast_result & SPELL_CANCEL_CAST)
+ return FALSE
-/obj/effect/proc_holder/spell/proc/playMagSound()
- playsound(get_turf(usr), sound,50,TRUE)
+ // Spell is officially being cast
+ if(!(precast_result & SPELL_NO_FEEDBACK))
+ // We do invocation and sound effects here, before actual cast
+ // That way stuff like teleports or shape-shifts can be invoked before ocurring
+ spell_feedback()
-/obj/effect/proc_holder/spell/Initialize(mapload)
- . = ..()
- START_PROCESSING(SSfastprocess, src)
+ // Actually cast the spell. Main effects go here
+ cast(cast_on)
- still_recharging_msg = span_warning("[name] is still recharging!")
- charge_counter = charge_max
+ if(!(precast_result & SPELL_NO_IMMEDIATE_COOLDOWN))
+ // The entire spell is done, start the actual cooldown at its set duration
+ StartCooldown()
-/obj/effect/proc_holder/spell/Destroy()
- STOP_PROCESSING(SSfastprocess, src)
- qdel(action)
- return ..()
+ // And then proceed with the aftermath of the cast
+ // Final effects that happen after all the casting is done can go here
+ after_cast(cast_on)
+ UpdateButtons()
+
+ return TRUE
+
+/**
+ * Actions done before the actual cast is called.
+ * This is the last chance to cancel the spell from being cast.
+ *
+ * Can be used for target selection or to validate checks on the caster (cast_on).
+ *
+ * Returns a bitflag.
+ * - SPELL_CANCEL_CAST will stop the spell from being cast.
+ * - SPELL_NO_FEEDBACK will prevent the spell from calling [proc/spell_feedback] on cast. (invocation, sounds)
+ * - SPELL_NO_IMMEDIATE_COOLDOWN will prevent the spell from starting its cooldown between cast and before after_cast.
+ */
+/datum/action/cooldown/spell/proc/before_cast(atom/cast_on)
+ SHOULD_CALL_PARENT(TRUE)
-/obj/effect/proc_holder/spell/Click()
- if(cast_check())
- choose_targets()
- return 1
+ var/sig_return = SEND_SIGNAL(src, COMSIG_SPELL_BEFORE_CAST, cast_on)
+ if(owner)
+ sig_return |= SEND_SIGNAL(owner, COMSIG_MOB_BEFORE_SPELL_CAST, src, cast_on)
-/obj/effect/proc_holder/spell/proc/choose_targets(mob/user = usr) //depends on subtype - /targeted or /aoe_turf
- return
+ return sig_return
/**
- * can_target: Checks if we are allowed to cast the spell on a target.
+ * Actions done as the main effect of the spell.
*
- * Arguments:
- * * target The atom that is being targeted by the spell.
- * * user The mob using the spell.
- * * silent If the checks should not give any feedback messages.
+ * For spells without a click intercept, [cast_on] will be the owner.
+ * For click spells, [cast_on] is whatever the owner clicked on in casting the spell.
*/
-/obj/effect/proc_holder/spell/proc/can_target(atom/target, mob/user, silent = FALSE)
- return TRUE
+/datum/action/cooldown/spell/proc/cast(atom/cast_on)
+ SHOULD_CALL_PARENT(TRUE)
-/obj/effect/proc_holder/spell/proc/start_recharge()
- recharging = TRUE
-
-/obj/effect/proc_holder/spell/process(delta_time)
- if(recharging && charge_type == "recharge" && (charge_counter < charge_max))
- charge_counter += delta_time * 10
- if(charge_counter >= charge_max)
- action.UpdateButtons()
- charge_counter = charge_max
- recharging = FALSE
-
-/obj/effect/proc_holder/spell/proc/perform(list/targets, recharge = TRUE, mob/user = usr) //if recharge is started is important for the trigger spells
- before_cast(targets)
- invocation(user)
- if(user?.ckey)
- user.log_message(span_danger("cast the spell [name]."), LOG_ATTACK)
- if(recharge)
- recharging = TRUE
- if(sound)
- playMagSound()
- SEND_SIGNAL(user, COMSIG_MOB_CAST_SPELL, src)
- cast(targets,user=user)
- after_cast(targets)
- if(action)
- action.UpdateButtons()
-
-/obj/effect/proc_holder/spell/proc/before_cast(list/targets)
- if(overlay)
- for(var/atom/target in targets)
- var/location
- if(isliving(target))
- location = target.loc
- else if(isturf(target))
- location = target
- var/obj/effect/overlay/spell = new /obj/effect/overlay(location)
- spell.icon = overlay_icon
- spell.icon_state = overlay_icon_state
- spell.set_anchored(TRUE)
- spell.set_density(FALSE)
- QDEL_IN(spell, overlay_lifespan)
-
-/obj/effect/proc_holder/spell/proc/after_cast(list/targets)
- for(var/atom/target in targets)
- var/location
- if(isliving(target))
- location = target.loc
- else if(isturf(target))
- location = target
- if(isliving(target) && message)
- to_chat(target, text("[message]"))
- if(sparks_spread)
- do_sparks(sparks_amt, FALSE, location)
- if(ispath(smoke_spread, /datum/effect_system/fluid_spread/smoke)) // Dear god this code is :agony:
- var/datum/effect_system/fluid_spread/smoke/smoke = new smoke_spread()
- smoke.set_up(smoke_amt, location = location)
- smoke.start()
-
-
-/obj/effect/proc_holder/spell/proc/cast(list/targets,mob/user = usr)
-
-/obj/effect/proc_holder/spell/proc/view_or_range(distance = world.view, center=usr, type="view")
- switch(type)
- if("view")
- . = view(distance,center)
- if("range")
- . = range(distance,center)
-
-/obj/effect/proc_holder/spell/proc/revert_cast(mob/user = usr) //resets recharge or readds a charge
- switch(charge_type)
- if("recharge")
- charge_counter = charge_max
- if("charges")
- charge_counter++
- if("holdervar")
- adjust_var(user, holder_var_type, -holder_var_amount)
- if(action)
- action.UpdateButtons()
-
-/obj/effect/proc_holder/spell/proc/adjust_var(mob/living/target = usr, type, amount) //handles the adjustment of the var when the spell is used. has some hardcoded types
- if (!istype(target))
- return
- switch(type)
- if("bruteloss")
- target.adjustBruteLoss(amount)
- if("fireloss")
- target.adjustFireLoss(amount)
- if("toxloss")
- target.adjustToxLoss(amount)
- if("oxyloss")
- target.adjustOxyLoss(amount)
- if("stun")
- target.AdjustStun(amount)
- if("knockdown")
- target.AdjustKnockdown(amount)
- if("paralyze")
- target.AdjustParalyzed(amount)
- if("immobilize")
- target.AdjustImmobilized(amount)
- if("unconscious")
- target.AdjustUnconscious(amount)
- else
- target.vars[type] += amount //I bear no responsibility for the runtimes that'll happen if you try to adjust non-numeric or even non-existent vars
-
-/obj/effect/proc_holder/spell/targeted //can mean aoe for mobs (limited/unlimited number) or one target mob
- var/max_targets = 1 //leave 0 for unlimited targets in range, 1 for one selectable target in range, more for limited number of casts (can all target one guy, depends on target_ignore_prev) in range
- var/target_ignore_prev = 1 //only important if max_targets > 1, affects if the spell can be cast multiple times at one person from one cast
- var/include_user = 0 //if it includes usr in the target list
- var/random_target = 0 // chooses random viable target instead of asking the caster
- var/random_target_priority = TARGET_CLOSEST // if random_target is enabled how it will pick the target
-
-
-/obj/effect/proc_holder/spell/aoe_turf //affects all turfs in view or range (depends)
- var/inner_radius = -1 //for all your ring spell needs
-
-/obj/effect/proc_holder/spell/targeted/choose_targets(mob/user = usr)
- var/list/targets = list()
-
- switch(max_targets)
- if(0) //unlimited
- for(var/mob/living/target in view_or_range(range, user, selection_type))
- if(!can_target(target, user, TRUE))
- continue
- targets += target
- if(1) //single target can be picked
- if(range < 0)
- targets += user
- else
- var/possible_targets = list()
-
- for(var/mob/living/M in view_or_range(range, user, selection_type))
- if(!include_user && user == M)
- continue
- if(!can_target(M, user, TRUE))
- continue
- possible_targets += M
-
- //targets += input("Choose the target for the spell.", "Targeting") as mob in possible_targets
- //Adds a safety check post-input to make sure those targets are actually in range.
- var/mob/chosen_target
- if(!random_target)
- chosen_target = tgui_input_list(user, "Choose the target for the spell", "Targeting", sort_names(possible_targets))
- if(isnull(chosen_target))
- return
- if(!ismob(chosen_target) || user.incapacitated())
- return
- else
- switch(random_target_priority)
- if(TARGET_RANDOM)
- chosen_target = pick(possible_targets)
- if(TARGET_CLOSEST)
- for(var/mob/living/living_target in possible_targets)
- if(chosen_target)
- if(get_dist(user, living_target) < get_dist(user, chosen_target))
- if(los_check(user, living_target))
- chosen_target = living_target
- else
- if(los_check(user, living_target))
- chosen_target = living_target
- if(chosen_target in view_or_range(range, user, selection_type))
- targets += chosen_target
-
- else
- var/list/possible_targets = list()
- for(var/mob/living/target in view_or_range(range, user, selection_type))
- if(!can_target(target, user, TRUE))
- continue
- possible_targets += target
- for(var/i in 1 to max_targets)
- if(!length(possible_targets))
- break
- if(target_ignore_prev)
- var/target = pick(possible_targets)
- possible_targets -= target
- targets += target
- else
- targets += pick(possible_targets)
-
- if(!include_user && (user in targets))
- targets -= user
-
- if(!length(targets)) //doesn't waste the spell
- revert_cast(user)
+ SEND_SIGNAL(src, COMSIG_SPELL_CAST, cast_on)
+ if(owner)
+ SEND_SIGNAL(owner, COMSIG_MOB_CAST_SPELL, src, cast_on)
+ if(owner.ckey)
+ owner.log_message("cast the spell [name][cast_on != owner ? " on / at [cast_on]":""].", LOG_ATTACK)
+
+/**
+ * Actions done after the main cast is finished.
+ * This is called after the cooldown's already begun.
+ *
+ * It can be used to apply late spell effects where order matters
+ * (for example, causing smoke *after* a teleport occurs in cast())
+ * or to clean up variables or references post-cast.
+ */
+/datum/action/cooldown/spell/proc/after_cast(atom/cast_on)
+ SHOULD_CALL_PARENT(TRUE)
+
+ SEND_SIGNAL(src, COMSIG_SPELL_AFTER_CAST, cast_on)
+ if(!owner)
return
- perform(targets, user=user)
+ SEND_SIGNAL(owner, COMSIG_MOB_AFTER_SPELL_CAST, src, cast_on)
-/obj/effect/proc_holder/spell/aoe_turf/choose_targets(mob/user = usr)
- var/list/targets = list()
+ // Sparks and smoke can only occur if there's an owner to source them from.
+ if(sparks_amt)
+ do_sparks(sparks_amt, FALSE, get_turf(owner))
- for(var/turf/target in view_or_range(range,user,selection_type))
- if(!can_target(target, user, TRUE))
- continue
- if(!(target in view_or_range(inner_radius,user,selection_type)))
- targets += target
+ if(ispath(smoke_type, /datum/effect_system/fluid_spread/smoke))
+ var/datum/effect_system/fluid_spread/smoke/smoke = new smoke_type()
+ smoke.set_up(smoke_amt, holder = owner, location = get_turf(owner))
+ smoke.start()
- if(!length(targets)) //doesn't waste the spell
- revert_cast()
+/// Provides feedback after a spell cast occurs, in the form of a cast sound and/or invocation
+/datum/action/cooldown/spell/proc/spell_feedback()
+ if(!owner)
return
- perform(targets,user=user)
+ if(invocation_type != INVOCATION_NONE)
+ invocation()
+ if(sound)
+ playsound(get_turf(owner), sound, 50, TRUE)
-/obj/effect/proc_holder/spell/proc/updateButtons(status_only, force)
- action.UpdateButtons(status_only, force)
+/// The invocation that accompanies the spell, called from spell_feedback() before cast().
+/datum/action/cooldown/spell/proc/invocation()
+ switch(invocation_type)
+ if(INVOCATION_SHOUT)
+ if(prob(50))
+ owner.say(invocation, forced = "spell ([src])")
+ else
+ owner.say(replacetext(invocation," ","`"), forced = "spell ([src])")
-/obj/effect/proc_holder/spell/proc/can_be_cast_by(mob/caster)
- if((human_req || clothes_req) && !ishuman(caster))
- return FALSE
- return TRUE
+ if(INVOCATION_WHISPER)
+ if(prob(50))
+ owner.whisper(invocation, forced = "spell ([src])")
+ else
+ owner.whisper(replacetext(invocation," ","`"), forced = "spell ([src])")
-/obj/effect/proc_holder/spell/targeted/proc/los_check(mob/A,mob/B)
- //Checks for obstacles from A to B
- var/obj/dummy = new(A.loc)
- dummy.pass_flags |= PASSTABLE
- var/turf/previous_step = get_turf(A)
- var/first_step = TRUE
- for(var/turf/next_step as anything in (get_line(A, B) - previous_step))
- if(first_step)
- for(var/obj/blocker in previous_step)
- if(!blocker.density || !(blocker.flags_1 & ON_BORDER_1))
- continue
- if(blocker.CanPass(dummy, get_dir(previous_step, next_step)))
- continue
- return FALSE // Could not leave the first turf.
- first_step = FALSE
- for(var/atom/movable/movable as anything in next_step)
- if(!movable.CanPass(dummy, get_dir(next_step, previous_step)))
- qdel(dummy)
- return FALSE
- previous_step = next_step
- qdel(dummy)
- return TRUE
+ if(INVOCATION_EMOTE)
+ owner.visible_message(invocation, invocation_self_message)
-/obj/effect/proc_holder/spell/proc/can_cast(mob/user = usr)
- if(((!user.mind) || !(src in user.mind.spell_list)) && !(src in user.mob_spell_list))
- return FALSE
+/// Checks if the current OWNER of the spell is in a valid state to say the spell's invocation
+/datum/action/cooldown/spell/proc/can_invoke(feedback = TRUE)
+ if(spell_requirements & SPELL_CASTABLE_WITHOUT_INVOCATION)
+ return TRUE
+
+ if(invocation_type == INVOCATION_NONE)
+ return TRUE
- if(!charge_check(user,TRUE))
+ // If you want a spell usable by ghosts for some reason, it must be INVOCATION_NONE
+ if(!isliving(owner))
+ if(feedback)
+ to_chat(owner, span_warning("You need to be living to invoke [src]!"))
return FALSE
- if(user.stat && !stat_allowed)
+ var/mob/living/living_owner = owner
+ if(invocation_type == INVOCATION_EMOTE && HAS_TRAIT(living_owner, TRAIT_EMOTEMUTE))
+ if(feedback)
+ to_chat(owner, span_warning("You can't position your hands correctly to invoke [src]!"))
return FALSE
- if(!user.can_cast_magic(antimagic_flags))
+ if((invocation_type == INVOCATION_WHISPER || invocation_type == INVOCATION_SHOUT) && !living_owner.can_speak_vocal())
+ if(feedback)
+ to_chat(owner, span_warning("You can't get the words out to invoke [src]!"))
return FALSE
- if(!ishuman(user))
- if(clothes_req || human_req)
- return FALSE
- if(nonabstract_req && (isbrain(user) || ispAI(user)))
- return FALSE
return TRUE
-/obj/effect/proc_holder/spell/self //Targets only the caster. Good for buffs and heals, but probably not wise for fireballs (although they usually fireball themselves anyway, honke)
- range = -1 //Duh
+/// Resets the cooldown of the spell, sending COMSIG_SPELL_CAST_RESET
+/// and allowing it to be used immediately (+ updating button icon accordingly)
+/datum/action/cooldown/spell/proc/reset_spell_cooldown()
+ SEND_SIGNAL(src, COMSIG_SPELL_CAST_RESET)
+ next_use_time -= cooldown_time // Basically, ensures that the ability can be used now
+ UpdateButtons()
-/obj/effect/proc_holder/spell/self/choose_targets(mob/user = usr)
- if(!user)
- revert_cast()
- return
- perform(null,user=user)
-
-/obj/effect/proc_holder/spell/self/basic_heal //This spell exists mainly for debugging purposes, and also to show how casting works
- name = "Lesser Heal"
- desc = "Heals a small amount of brute and burn damage."
- human_req = TRUE
- clothes_req = FALSE
- charge_max = 100
- cooldown_min = 50
- invocation = "Victus sano!"
- invocation_type = INVOCATION_WHISPER
- school = SCHOOL_RESTORATION
- sound = 'sound/magic/staff_healing.ogg'
-
-/obj/effect/proc_holder/spell/self/basic_heal/cast(list/targets, mob/living/carbon/human/user) //Note the lack of "list/targets" here. Instead, use a "user" var depending on mob requirements.
- //Also, notice the lack of a "for()" statement that looks through the targets. This is, again, because the spell can only have a single target.
- user.visible_message(span_warning("A wreath of gentle light passes over [user]!"), span_notice("You wreath yourself in healing light!"))
- user.adjustBruteLoss(-10)
- user.adjustFireLoss(-10)
-
-/obj/effect/proc_holder/spell/vv_get_dropdown()
- . = ..()
- VV_DROPDOWN_OPTION("", "---------")
- if(clothes_req)
- VV_DROPDOWN_OPTION(VV_HK_SPELL_SET_ROBELESS, "Set Robeless")
- else
- VV_DROPDOWN_OPTION(VV_HK_SPELL_UNSET_ROBELESS, "Unset Robeless")
+/**
+ * Levels the spell up a single level, reducing the cooldown.
+ * If bypass_cap is TRUE, will level the spell up past it's set cap.
+ */
+/datum/action/cooldown/spell/proc/level_spell(bypass_cap = FALSE)
+ // Spell cannot be levelled
+ if(spell_max_level <= 1)
+ return FALSE
- if(human_req)
- VV_DROPDOWN_OPTION(VV_HK_SPELL_UNSET_HUMANONLY, "Unset Require Humanoid Mob")
- else
- VV_DROPDOWN_OPTION(VV_HK_SPELL_SET_HUMANONLY, "Set Require Humanoid Mob")
+ // Spell is at cap, and we will not bypass it
+ if(!bypass_cap && (spell_level >= spell_max_level))
+ return FALSE
- if(nonabstract_req)
- VV_DROPDOWN_OPTION(VV_HK_SPELL_UNSET_NONABSTRACT, "Unset Require Body")
- else
- VV_DROPDOWN_OPTION(VV_HK_SPELL_SET_NONABSTRACT, "Set Require Body")
+ spell_level++
+ cooldown_time = max(cooldown_time - cooldown_reduction_per_rank, 0)
+ update_spell_name()
+ return TRUE
-/obj/effect/proc_holder/spell/vv_do_topic(list/href_list)
- . = ..()
- if(href_list[VV_HK_SPELL_SET_ROBELESS])
- clothes_req = FALSE
- return
- if(href_list[VV_HK_SPELL_UNSET_ROBELESS])
- clothes_req = TRUE
- return
- if(href_list[VV_HK_SPELL_UNSET_HUMANONLY])
- human_req = FALSE
- return
- if(href_list[VV_HK_SPELL_SET_HUMANONLY])
- human_req = TRUE
- return
- if(href_list[VV_HK_SPELL_UNSET_NONABSTRACT])
- nonabstract_req = FALSE
- return
- if(href_list[VV_HK_SPELL_SET_NONABSTRACT])
- nonabstract_req = TRUE
- return
+/**
+ * Levels the spell down a single level, down to 1.
+ */
+/datum/action/cooldown/spell/proc/delevel_spell()
+ // Spell cannot be levelled
+ if(spell_max_level <= 1)
+ return FALSE
+
+ if(spell_level <= 1)
+ return FALSE
+
+ spell_level--
+ cooldown_time = min(cooldown_time + cooldown_reduction_per_rank, initial(cooldown_time))
+ update_spell_name()
+ return TRUE
+
+/**
+ * Updates the spell's name based on its level.
+ */
+/datum/action/cooldown/spell/proc/update_spell_name()
+ var/spell_title = ""
+ switch(spell_level)
+ if(2)
+ spell_title = "Efficient "
+ if(3)
+ spell_title = "Quickened "
+ if(4)
+ spell_title = "Free "
+ if(5)
+ spell_title = "Instant "
+ if(6)
+ spell_title = "Ludicrous "
+
+ name = "[spell_title][initial(name)]"
+ UpdateButtons()
diff --git a/code/modules/spells/spell_types/aimed.dm b/code/modules/spells/spell_types/aimed.dm
deleted file mode 100644
index 64ca91f0541d2..0000000000000
--- a/code/modules/spells/spell_types/aimed.dm
+++ /dev/null
@@ -1,207 +0,0 @@
-
-/obj/effect/proc_holder/spell/aimed
- name = "aimed projectile spell"
- base_icon_state = "projectile"
- var/projectile_type = /obj/projectile/magic/teleport
- var/deactive_msg = "You discharge your projectile..."
- var/active_msg = "You charge your projectile!"
- var/active_icon_state = "projectile"
- var/list/projectile_var_overrides = list()
- var/projectile_amount = 1 //Projectiles per cast.
- var/current_amount = 0 //How many projectiles left.
- var/projectiles_per_fire = 1 //Projectiles per fire. Probably not a good thing to use unless you override ready_projectile().
-
-/obj/effect/proc_holder/spell/aimed/Click()
- var/mob/living/user = usr
- if(!istype(user))
- return
- if(!can_cast(user))
- remove_ranged_ability(span_warning("You can no longer cast [name]!"))
- return
-
- if(active)
- on_deactivation(user)
- else
- on_activation(user)
-
-/**
- * Activate the spell for user.
- */
-/obj/effect/proc_holder/spell/aimed/proc/on_activation(mob/user)
- SHOULD_CALL_PARENT(TRUE)
-
- current_amount = projectile_amount
- add_ranged_ability(user, span_notice("[active_msg] Left-click to shoot it at a target!"), TRUE)
-
-/**
- * Deactivate the spell from user.
- */
-/obj/effect/proc_holder/spell/aimed/proc/on_deactivation(mob/user)
- SHOULD_CALL_PARENT(TRUE)
-
- if(charge_type == "recharge")
- var/refund_percent = current_amount / projectile_amount
- charge_counter = charge_max * refund_percent
- start_recharge()
- remove_ranged_ability(span_notice("[deactive_msg]"))
-
-
-/obj/effect/proc_holder/spell/aimed/update_icon()
- if(!action)
- return
-
- . = ..()
- action.button_icon_state = "[base_icon_state][active]"
- action.UpdateButtons()
-
-/obj/effect/proc_holder/spell/aimed/InterceptClickOn(mob/living/caller, params, atom/target)
- if(..())
- return FALSE
- var/ran_out = (current_amount <= 0)
- if(!cast_check(!ran_out, ranged_ability_user))
- remove_ranged_ability()
- return FALSE
- var/list/targets = list(target)
- perform(targets, ran_out, user = ranged_ability_user)
- return TRUE
-
-/obj/effect/proc_holder/spell/aimed/cast(list/targets, mob/living/user)
- var/target = targets[1]
- var/turf/T = user.loc
- var/turf/U = get_step(user, user.dir) // Get the tile infront of the move, based on their direction
- if(!isturf(U) || !isturf(T))
- return FALSE
- fire_projectile(user, target)
- user.newtonian_move(get_dir(U, T))
- if(current_amount <= 0)
- remove_ranged_ability() //Auto-disable the ability once you run out of bullets.
- charge_counter = 0
- start_recharge()
- on_deactivation(user)
- return TRUE
-
-/obj/effect/proc_holder/spell/aimed/proc/fire_projectile(mob/living/user, atom/target)
- current_amount--
- for(var/i in 1 to projectiles_per_fire)
- var/obj/projectile/P = new projectile_type(user.loc)
- P.firer = user
- P.preparePixelProjectile(target, user)
- for(var/V in projectile_var_overrides)
- if(P.vars[V])
- P.vv_edit_var(V, projectile_var_overrides[V])
- ready_projectile(P, target, user, i)
- P.fire()
- return TRUE
-
-/obj/effect/proc_holder/spell/aimed/proc/ready_projectile(obj/projectile/P, atom/target, mob/user, iteration)
- P.fired_from = src
- return
-
-/obj/effect/proc_holder/spell/aimed/lightningbolt
- name = "Lightning Bolt"
- desc = "Fire a lightning bolt at your foes! It will jump between targets, but can't knock them down."
- school = SCHOOL_EVOCATION
- charge_max = 100
- clothes_req = FALSE
- invocation = "P'WAH, UNLIM'TED P'WAH"
- invocation_type = INVOCATION_SHOUT
- cooldown_min = 20
- base_icon_state = "lightning"
- action_icon_state = "lightning0"
- sound = 'sound/magic/lightningbolt.ogg'
- active = FALSE
- projectile_var_overrides = list("zap_range" = 15, "zap_power" = 20000, "zap_flags" = ZAP_MOB_DAMAGE)
- active_msg = "You energize your hands with arcane lightning!"
- deactive_msg = "You let the energy flow out of your hands back into yourself..."
- projectile_type = /obj/projectile/magic/aoe/lightning
-
-/obj/effect/proc_holder/spell/aimed/lightningbolt/on_gain(mob/living/user)
- . = ..()
- ADD_TRAIT(user, TRAIT_TESLA_SHOCKIMMUNE, "lightning_bolt_spell")
-
-/obj/effect/proc_holder/spell/aimed/lightningbolt/on_lose(mob/living/user)
- . = ..()
- REMOVE_TRAIT(user, TRAIT_TESLA_SHOCKIMMUNE, "lightning_bolt_spell")
-
-/obj/effect/proc_holder/spell/aimed/fireball
- name = "Fireball"
- desc = "This spell fires an explosive fireball at a target."
- school = SCHOOL_EVOCATION
- charge_max = 60
- clothes_req = FALSE
- invocation = "ONI SOMA"
- invocation_type = INVOCATION_SHOUT
- range = 20
- cooldown_min = 20 //10 deciseconds reduction per rank
- projectile_type = /obj/projectile/magic/fireball
- base_icon_state = "fireball"
- action_icon_state = "fireball0"
- sound = 'sound/magic/fireball.ogg'
- active_msg = "You prepare to cast your fireball spell!"
- deactive_msg = "You extinguish your fireball... for now."
- active = FALSE
-
-/obj/effect/proc_holder/spell/aimed/fireball/fire_projectile(list/targets, mob/living/user)
- var/range = 6 + 2*spell_level
- projectile_var_overrides = list("range" = range)
- return ..()
-
-/obj/effect/proc_holder/spell/aimed/spell_cards
- name = "Spell Cards"
- desc = "Blazing hot rapid-fire homing cards. Send your foes to the shadow realm with their mystical power!"
- school = SCHOOL_EVOCATION
- charge_max = 50
- clothes_req = FALSE
- invocation = "Sigi'lu M'Fan 'Tasia"
- invocation_type = INVOCATION_SHOUT
- range = 40
- cooldown_min = 10
- projectile_amount = 5
- projectiles_per_fire = 7
- projectile_type = /obj/projectile/magic/spellcard
- base_icon_state = "spellcard"
- action_icon_state = "spellcard0"
- var/datum/weakref/current_target_weakref
- var/projectile_turnrate = 10
- var/projectile_pixel_homing_spread = 32
- var/projectile_initial_spread_amount = 30
- var/projectile_location_spread_amount = 12
- var/datum/component/lockon_aiming/lockon_component
- ranged_clickcd_override = TRUE
-
-/obj/effect/proc_holder/spell/aimed/spell_cards/on_activation(mob/M)
- . = ..()
- QDEL_NULL(lockon_component)
- lockon_component = M.AddComponent(/datum/component/lockon_aiming, 5, GLOB.typecache_living, 1, null, CALLBACK(src, .proc/on_lockon_component))
-
-/obj/effect/proc_holder/spell/aimed/spell_cards/proc/on_lockon_component(list/locked_weakrefs)
- if(!length(locked_weakrefs))
- current_target_weakref = null
- return
- current_target_weakref = locked_weakrefs[1]
- var/atom/A = current_target_weakref.resolve()
- if(A)
- var/mob/M = lockon_component.parent
- M.face_atom(A)
-
-/obj/effect/proc_holder/spell/aimed/spell_cards/on_deactivation(mob/M)
- . = ..()
- QDEL_NULL(lockon_component)
-
-/obj/effect/proc_holder/spell/aimed/spell_cards/ready_projectile(obj/projectile/P, atom/target, mob/user, iteration)
- . = ..()
- if(current_target_weakref)
- var/atom/A = current_target_weakref.resolve()
- if(A && get_dist(A, user) < 7)
- P.homing_turn_speed = projectile_turnrate
- P.homing_inaccuracy_min = projectile_pixel_homing_spread
- P.homing_inaccuracy_max = projectile_pixel_homing_spread
- P.set_homing_target(current_target_weakref.resolve())
- var/rand_spr = rand()
- var/total_angle = projectile_initial_spread_amount * 2
- var/adjusted_angle = total_angle - ((projectile_initial_spread_amount / projectiles_per_fire) * 0.5)
- var/one_fire_angle = adjusted_angle / projectiles_per_fire
- var/current_angle = iteration * one_fire_angle * rand_spr - (projectile_initial_spread_amount / 2)
- P.pixel_x = rand(-projectile_location_spread_amount, projectile_location_spread_amount)
- P.pixel_y = rand(-projectile_location_spread_amount, projectile_location_spread_amount)
- P.preparePixelProjectile(target, user, null, current_angle)
diff --git a/code/modules/spells/spell_types/aoe_spell/_aoe_spell.dm b/code/modules/spells/spell_types/aoe_spell/_aoe_spell.dm
new file mode 100644
index 0000000000000..1d240bad61ea1
--- /dev/null
+++ b/code/modules/spells/spell_types/aoe_spell/_aoe_spell.dm
@@ -0,0 +1,57 @@
+/**
+ * ## AOE spells
+ *
+ * A spell that iterates over atoms near the caster and casts a spell on them.
+ * Calls cast_on_thing_in_aoe on all atoms returned by get_things_to_cast_on by default.
+ */
+/datum/action/cooldown/spell/aoe
+ /// The max amount of targets we can affect via our AOE. 0 = unlimited
+ var/max_targets = 0
+ /// The radius of the aoe.
+ var/aoe_radius = 7
+
+// At this point, cast_on == owner. Either works.
+/datum/action/cooldown/spell/aoe/cast(atom/cast_on)
+ . = ..()
+ // Get every atom around us to our aoe cast on
+ var/list/atom/things_to_cast_on = get_things_to_cast_on(cast_on)
+ // If we have a target limit, shuffle it (for fariness)
+ if(max_targets > 0)
+ things_to_cast_on = shuffle(things_to_cast_on)
+
+ SEND_SIGNAL(src, COMSIG_SPELL_AOE_ON_CAST, things_to_cast_on, cast_on)
+
+ // Now go through and cast our spell where applicable
+ var/num_targets = 0
+ for(var/thing_to_target in things_to_cast_on)
+ if(max_targets > 0 && num_targets >= max_targets)
+ continue
+
+ cast_on_thing_in_aoe(thing_to_target, cast_on)
+ num_targets++
+
+/**
+ * Gets a list of atoms around [center]
+ * that are within range and affected by our aoe.
+ */
+/datum/action/cooldown/spell/aoe/proc/get_things_to_cast_on(atom/center)
+ var/list/things = list()
+ for(var/atom/nearby_thing in range(aoe_radius, center))
+ if(nearby_thing == owner || nearby_thing == center)
+ continue
+
+ things += nearby_thing
+
+ return things
+
+/**
+ * Actually cause effects on the thing in our aoe.
+ * Override this for your spell! Not cast().
+ *
+ * Arguments
+ * * victim - the atom being affected by our aoe
+ * * caster - the mob who cast the aoe
+ */
+/datum/action/cooldown/spell/aoe/proc/cast_on_thing_in_aoe(atom/victim, atom/caster)
+ SHOULD_CALL_PARENT(FALSE)
+ CRASH("[type] did not implement cast_on_thing_in_aoe and either has no effects or implemented the spell incorrectly.")
diff --git a/code/modules/spells/spell_types/aoe_spell/area_conversion.dm b/code/modules/spells/spell_types/aoe_spell/area_conversion.dm
new file mode 100644
index 0000000000000..bde25b779332e
--- /dev/null
+++ b/code/modules/spells/spell_types/aoe_spell/area_conversion.dm
@@ -0,0 +1,25 @@
+/datum/action/cooldown/spell/aoe/area_conversion
+ name = "Area Conversion"
+ desc = "This spell instantly converts a small area around you."
+ background_icon_state = "bg_cult"
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "areaconvert"
+
+ school = SCHOOL_TRANSMUTATION
+ cooldown_time = 5 SECONDS
+
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+
+ aoe_radius = 2
+
+/datum/action/cooldown/spell/aoe/area_conversion/get_things_to_cast_on(atom/center)
+ var/list/things = list()
+ for(var/turf/nearby_turf in range(aoe_radius, center))
+ things += nearby_turf
+
+ return things
+
+/datum/action/cooldown/spell/aoe/area_conversion/cast_on_thing_in_aoe(turf/victim, atom/caster)
+ playsound(victim, 'sound/items/welder.ogg', 75, TRUE)
+ victim.narsie_act(FALSE, TRUE, 100 - (get_dist(victim, caster) * 25))
diff --git a/code/modules/spells/spell_types/aoe_spell/knock.dm b/code/modules/spells/spell_types/aoe_spell/knock.dm
new file mode 100644
index 0000000000000..fd9e4503de8fd
--- /dev/null
+++ b/code/modules/spells/spell_types/aoe_spell/knock.dm
@@ -0,0 +1,20 @@
+/datum/action/cooldown/spell/aoe/knock
+ name = "Knock"
+ desc = "This spell opens nearby doors and closets."
+ button_icon_state = "knock"
+
+ sound = 'sound/magic/knock.ogg'
+ school = SCHOOL_TRANSMUTATION
+ cooldown_time = 10 SECONDS
+ cooldown_reduction_per_rank = 2 SECONDS
+
+ invocation = "AULIE OXIN FIERA"
+ invocation_type = INVOCATION_WHISPER
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+ aoe_radius = 3
+
+/datum/action/cooldown/spell/aoe/knock/get_things_to_cast_on(atom/center)
+ return RANGE_TURFS(aoe_radius, center)
+
+/datum/action/cooldown/spell/aoe/knock/cast_on_thing_in_aoe(turf/victim, atom/caster)
+ SEND_SIGNAL(victim, COMSIG_ATOM_MAGICALLY_UNLOCKED, src, caster)
diff --git a/code/modules/spells/spell_types/aoe_spell/magic_missile.dm b/code/modules/spells/spell_types/aoe_spell/magic_missile.dm
new file mode 100644
index 0000000000000..a1513c1ca897d
--- /dev/null
+++ b/code/modules/spells/spell_types/aoe_spell/magic_missile.dm
@@ -0,0 +1,47 @@
+/datum/action/cooldown/spell/aoe/magic_missile
+ name = "Magic Missile"
+ desc = "This spell fires several, slow moving, magic projectiles at nearby targets."
+ button_icon_state = "magicm"
+ sound = 'sound/magic/magic_missile.ogg'
+
+ school = SCHOOL_EVOCATION
+ cooldown_time = 20 SECONDS
+ cooldown_reduction_per_rank = 3.5 SECONDS
+
+ invocation = "FORTI GY AMA"
+ invocation_type = INVOCATION_SHOUT
+
+ aoe_radius = 7
+
+ /// The projectile type fired at all people around us
+ var/obj/projectile/projectile_type = /obj/projectile/magic/aoe/magic_missile
+
+/datum/action/cooldown/spell/aoe/magic_missile/get_things_to_cast_on(atom/center)
+ var/list/things = list()
+ for(var/mob/living/nearby_mob in view(aoe_radius, center))
+ if(nearby_mob == owner || nearby_mob == center)
+ continue
+
+ things += nearby_mob
+
+ return things
+
+/datum/action/cooldown/spell/aoe/magic_missile/cast_on_thing_in_aoe(mob/living/victim, atom/caster)
+ fire_projectile(victim, caster)
+
+/datum/action/cooldown/spell/aoe/magic_missile/proc/fire_projectile(atom/victim, mob/caster)
+ var/obj/projectile/to_fire = new projectile_type()
+ to_fire.preparePixelProjectile(victim, caster)
+ to_fire.fire()
+
+/datum/action/cooldown/spell/aoe/magic_missile/lesser
+ name = "Lesser Magic Missile"
+ desc = "This spell fires several, slow moving, magic projectiles at nearby targets."
+ background_icon_state = "bg_demon"
+
+ cooldown_time = 40 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+ max_targets = 6
+ projectile_type = /obj/projectile/magic/aoe/magic_missile/lesser
diff --git a/code/modules/spells/spell_types/aoe_spell/repulse.dm b/code/modules/spells/spell_types/aoe_spell/repulse.dm
new file mode 100644
index 0000000000000..9e24ccde61a20
--- /dev/null
+++ b/code/modules/spells/spell_types/aoe_spell/repulse.dm
@@ -0,0 +1,87 @@
+/datum/action/cooldown/spell/aoe/repulse
+ /// The max throw range of the repulsioon.
+ var/max_throw = 5
+ /// A visual effect to be spawned on people who are thrown away.
+ var/obj/effect/sparkle_path = /obj/effect/temp_visual/gravpush
+ /// The moveforce of the throw done by the repulsion.
+ var/repulse_force = MOVE_FORCE_EXTREMELY_STRONG
+
+/datum/action/cooldown/spell/aoe/repulse/get_things_to_cast_on(atom/center)
+ var/list/things = list()
+ for(var/atom/movable/nearby_movable in view(aoe_radius, center))
+ if(nearby_movable == owner || nearby_movable == center)
+ continue
+ if(nearby_movable.anchored)
+ continue
+
+ things += nearby_movable
+
+ return things
+
+/datum/action/cooldown/spell/aoe/repulse/cast_on_thing_in_aoe(atom/movable/victim, atom/caster)
+ if(ismob(victim))
+ var/mob/victim_mob = victim
+ if(victim_mob.can_block_magic(antimagic_flags))
+ return
+
+ var/turf/throwtarget = get_edge_target_turf(caster, get_dir(caster, get_step_away(victim, caster)))
+ var/dist_from_caster = get_dist(victim, caster)
+
+ if(dist_from_caster == 0)
+ if(isliving(victim))
+ var/mob/living/victim_living = victim
+ victim_living.Paralyze(10 SECONDS)
+ victim_living.adjustBruteLoss(5)
+ to_chat(victim, span_userdanger("You're slammed into the floor by [caster]!"))
+ else
+ if(sparkle_path)
+ // Created sparkles will disappear on their own
+ new sparkle_path(get_turf(victim), get_dir(caster, victim))
+
+ if(isliving(victim))
+ var/mob/living/victim_living = victim
+ victim_living.Paralyze(4 SECONDS)
+ to_chat(victim, span_userdanger("You're thrown back by [caster]!"))
+
+ // So stuff gets tossed around at the same time.
+ victim.safe_throw_at(throwtarget, ((clamp((max_throw - (clamp(dist_from_caster - 2, 0, dist_from_caster))), 3, max_throw))), 1, caster, force = repulse_force)
+
+/datum/action/cooldown/spell/aoe/repulse/wizard
+ name = "Repulse"
+ desc = "This spell throws everything around the user away."
+ button_icon_state = "repulse"
+ sound = 'sound/magic/repulse.ogg'
+
+ school = SCHOOL_EVOCATION
+ invocation = "GITTAH WEIGH"
+ invocation_type = INVOCATION_SHOUT
+ aoe_radius = 5
+
+ cooldown_time = 40 SECONDS
+ cooldown_reduction_per_rank = 6.25 SECONDS
+
+/datum/action/cooldown/spell/aoe/repulse/xeno
+ name = "Tail Sweep"
+ desc = "Throw back attackers with a sweep of your tail."
+ background_icon_state = "bg_alien"
+ icon_icon = 'icons/mob/actions/actions_xeno.dmi'
+ button_icon_state = "tailsweep"
+ panel = "Alien"
+ sound = 'sound/magic/tail_swing.ogg'
+
+ cooldown_time = 15 SECONDS
+ spell_requirements = NONE
+
+ invocation_type = INVOCATION_NONE
+ antimagic_flags = NONE
+ aoe_radius = 2
+
+ sparkle_path = /obj/effect/temp_visual/dir_setting/tailsweep
+
+/datum/action/cooldown/spell/aoe/repulse/xeno/cast(atom/cast_on)
+ if(iscarbon(cast_on))
+ var/mob/living/carbon/carbon_caster = cast_on
+ playsound(get_turf(carbon_caster), 'sound/voice/hiss5.ogg', 80, TRUE, TRUE)
+ carbon_caster.spin(6, 1)
+
+ return ..()
diff --git a/code/modules/spells/spell_types/aoe_spell/sacred_flame.dm b/code/modules/spells/spell_types/aoe_spell/sacred_flame.dm
new file mode 100644
index 0000000000000..450544a7a1f66
--- /dev/null
+++ b/code/modules/spells/spell_types/aoe_spell/sacred_flame.dm
@@ -0,0 +1,39 @@
+/datum/action/cooldown/spell/aoe/sacred_flame
+ name = "Sacred Flame"
+ desc = "Makes everyone around you more flammable, and lights yourself on fire."
+ button_icon_state = "sacredflame"
+ sound = 'sound/magic/fireball.ogg'
+
+ school = SCHOOL_EVOCATION
+ cooldown_time = 6 SECONDS
+
+ invocation = "FI'RAN DADISKO"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+ aoe_radius = 6
+
+ /// The amount of firestacks to put people afflicted.
+ var/firestacks_to_give = 20
+
+/datum/action/cooldown/spell/aoe/sacred_flame/get_things_to_cast_on(atom/center)
+ var/list/things = list()
+ for(var/mob/living/nearby_mob in view(aoe_radius, center))
+ things += nearby_mob
+
+ return things
+
+/datum/action/cooldown/spell/aoe/sacred_flame/cast_on_thing_in_aoe(mob/living/victim, mob/living/caster)
+ if(victim.can_block_magic(antimagic_flags))
+ return
+
+ victim.adjust_fire_stacks(firestacks_to_give)
+ // Let people who got afflicted know they're suddenly a matchstick
+ // But skip the caster - they'll know anyways.
+ if(victim != caster)
+ to_chat(victim, span_warning("You suddenly feel very flammable."))
+
+/datum/action/cooldown/spell/aoe/sacred_flame/cast(mob/living/cast_on)
+ . = ..()
+ cast_on.ignite_mob()
+ to_chat(cast_on, span_danger("You feel a roaring flame build up inside you!"))
diff --git a/code/modules/spells/spell_types/area_teleport.dm b/code/modules/spells/spell_types/area_teleport.dm
deleted file mode 100644
index 3463d9fa6eafb..0000000000000
--- a/code/modules/spells/spell_types/area_teleport.dm
+++ /dev/null
@@ -1,92 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/area_teleport
- name = "Area teleport"
- desc = "This spell teleports you to a type of area of your selection."
- nonabstract_req = TRUE
- school = SCHOOL_TRANSLOCATION
-
- var/randomise_selection = FALSE //if it lets the usr choose the teleport loc or picks it from the list
- var/invocation_area = TRUE //if the invocation appends the selected area
- var/sound1 = 'sound/weapons/zapbang.ogg'
- var/sound2 = 'sound/weapons/zapbang.ogg'
-
- var/say_destination = TRUE
-
-/obj/effect/proc_holder/spell/targeted/area_teleport/perform(list/targets, recharge = 1,mob/living/user = usr)
- var/thearea = before_cast(targets)
- if(!thearea || !cast_check(1))
- revert_cast()
- return
- invocation(thearea,user)
- if(charge_type == "recharge" && recharge)
- INVOKE_ASYNC(src, .proc/start_recharge)
- cast(targets,thearea,user)
- after_cast(targets)
-
-/obj/effect/proc_holder/spell/targeted/area_teleport/before_cast(list/targets)
- var/target_area = null
-
- if(!randomise_selection)
- target_area = tgui_input_list(usr, "Area to teleport to", "Teleport", GLOB.teleportlocs)
- else
- target_area = pick(GLOB.teleportlocs)
- if(isnull(target_area))
- return
- if(isnull(GLOB.teleportlocs[target_area]))
- return
- var/area/thearea = GLOB.teleportlocs[target_area]
-
- return thearea
-
-/obj/effect/proc_holder/spell/targeted/area_teleport/cast(list/targets,area/thearea,mob/user = usr)
- playsound(get_turf(user), sound1, 50,TRUE)
- for(var/mob/living/target in targets)
- var/list/L = list()
- for(var/turf/T in get_area_turfs(thearea.type))
- if(!T.density)
- var/clear = TRUE
- for(var/obj/O in T)
- if(O.density)
- clear = FALSE
- break
- if(clear)
- L+=T
-
- if(!length(L))
- to_chat(usr, span_warning("The spell matrix was unable to locate a suitable teleport destination for an unknown reason. Sorry."))
- return
-
- if(target?.buckled)
- target.buckled.unbuckle_mob(target, force=1)
-
- var/list/tempL = L
- var/attempt = null
- var/success = FALSE
- while(length(tempL))
- attempt = pick(tempL)
- do_teleport(target, attempt, channel = TELEPORT_CHANNEL_MAGIC)
- if(get_turf(target) == attempt)
- success = TRUE
- break
- else
- tempL.Remove(attempt)
-
- if(!success)
- do_teleport(target, L, channel = TELEPORT_CHANNEL_MAGIC)
- playsound(get_turf(user), sound2, 50,TRUE)
-
-/obj/effect/proc_holder/spell/targeted/area_teleport/invocation(area/chosenarea = null,mob/living/user = usr)
- if(!invocation_area || !chosenarea)
- ..()
- else
- var/words
- if(say_destination)
- words = "[invocation] [uppertext(chosenarea.name)]"
- else
- words = "[invocation]"
-
- switch(invocation_type)
- if(INVOCATION_SHOUT)
- user.say(words, forced = "spell")
- playsound(user.loc, pick('sound/misc/null.ogg','sound/misc/null.ogg'), 100, TRUE)
- if(INVOCATION_WHISPER)
- user.whisper(words, forced = "spell")
diff --git a/code/modules/spells/spell_types/bloodcrawl.dm b/code/modules/spells/spell_types/bloodcrawl.dm
deleted file mode 100644
index b47941fb34647..0000000000000
--- a/code/modules/spells/spell_types/bloodcrawl.dm
+++ /dev/null
@@ -1,54 +0,0 @@
-/obj/effect/proc_holder/spell/bloodcrawl
- name = "Blood Crawl"
- desc = "Use pools of blood to phase out of existence."
- charge_max = 0
- clothes_req = FALSE
- //If you couldn't cast this while phased, you'd have a problem
- phase_allowed = TRUE
- selection_type = "range"
- range = 1
- cooldown_min = 0
- overlay = null
- action_icon = 'icons/mob/actions/actions_minor_antag.dmi'
- action_icon_state = "bloodcrawl"
- action_background_icon_state = "bg_demon"
- var/phased = FALSE
-
-/obj/effect/proc_holder/spell/bloodcrawl/on_lose(mob/living/user)
- if(phased)
- user.phasein(get_turf(user), TRUE)
-
-/obj/effect/proc_holder/spell/bloodcrawl/cast_check(skipcharge = 0,mob/user = usr)
- . = ..()
- if(!.)
- return FALSE
- var/area/noteleport_check = get_area(user)
- if(noteleport_check && noteleport_check.area_flags & NOTELEPORT)
- to_chat(user, span_danger("Some dull, universal force is between you and your other existence, preventing you from blood crawling."))
- return FALSE
-
-/obj/effect/proc_holder/spell/bloodcrawl/choose_targets(mob/user = usr)
- for(var/obj/effect/decal/cleanable/target in range(range, get_turf(user)))
- if(target.can_bloodcrawl_in())
- perform(target)
- return
- revert_cast()
- to_chat(user, span_warning("There must be a nearby source of blood!"))
-
-/obj/effect/proc_holder/spell/bloodcrawl/perform(obj/effect/decal/cleanable/target, recharge = 1, mob/living/user = usr)
- if(istype(user))
- if(istype(user, /mob/living/simple_animal/hostile/imp/slaughter))
- var/mob/living/simple_animal/hostile/imp/slaughter/slaught = user
- slaught.current_hitstreak = 0
- slaught.wound_bonus = initial(slaught.wound_bonus)
- slaught.bare_wound_bonus = initial(slaught.bare_wound_bonus)
- if(phased)
- if(user.phasein(target))
- phased = FALSE
- else
- if(user.phaseout(target))
- phased = TRUE
- start_recharge()
- return
- revert_cast()
- to_chat(user, span_warning("You are unable to blood crawl!"))
diff --git a/code/modules/spells/spell_types/charge.dm b/code/modules/spells/spell_types/charge.dm
deleted file mode 100644
index 754b731f7807d..0000000000000
--- a/code/modules/spells/spell_types/charge.dm
+++ /dev/null
@@ -1,56 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/charge
- name = "Charge"
- desc = "This spell can be used to recharge a variety of things in your hands, \
- from magical artifacts to electrical components. A creative wizard can even use it \
- to grant magical power to a fellow magic user."
- sound = 'sound/magic/charge.ogg'
- action_icon_state = "charge"
-
- school = SCHOOL_TRANSMUTATION
- charge_max = 600
- clothes_req = FALSE
- invocation = "DIRI CEL"
- invocation_type = INVOCATION_WHISPER
- range = -1
- cooldown_min = 400 //50 deciseconds reduction per rank
- include_user = TRUE
-
-/obj/effect/proc_holder/spell/targeted/charge/cast(list/targets, mob/user = usr)
- // Charge people we're pulling first and foremost
- if(isliving(user.pulling))
- var/mob/living/pulled_living = user.pulling
- var/pulled_has_spells = FALSE
-
- for(var/obj/effect/proc_holder/spell/spell in pulled_living.mob_spell_list | pulled_living.mind?.spell_list)
- spell.charge_counter = spell.charge_max
- spell.recharging = FALSE
- spell.update_appearance()
- pulled_has_spells = TRUE
-
- if(pulled_has_spells)
- to_chat(pulled_living, span_notice("You feel raw magic flowing through you. It feels good!"))
- to_chat(user, span_notice("[pulled_living] suddenly feels very warm!"))
- return
-
- to_chat(pulled_living, span_notice("You feel very strange for a moment, but then it passes."))
-
- // Then charge their main hand item, then charge their offhand item
- var/obj/item/to_charge = user.get_active_held_item() || user.get_inactive_held_item()
- if(!to_charge)
- to_chat(user, span_notice("You feel magical power surging through your hands, but the feeling rapidly fades."))
- return
-
- var/charge_return = SEND_SIGNAL(to_charge, COMSIG_ITEM_MAGICALLY_CHARGED, src, user)
-
- if(QDELETED(to_charge))
- to_chat(user, span_warning("[src] seems to react adversely with [to_charge]!"))
- return
-
- if(charge_return & COMPONENT_ITEM_BURNT_OUT)
- to_chat(user, span_warning("[to_charge] seems to react negatively to [src], becoming uncomfortably warm!"))
-
- else if(charge_return & COMPONENT_ITEM_CHARGED)
- to_chat(user, span_notice("[to_charge] suddenly feels very warm!"))
-
- else
- to_chat(user, span_notice("[to_charge] doesn't seem to be react to [src]."))
diff --git a/code/modules/spells/spell_types/cone/_cone.dm b/code/modules/spells/spell_types/cone/_cone.dm
new file mode 100644
index 0000000000000..0832b01b97dfb
--- /dev/null
+++ b/code/modules/spells/spell_types/cone/_cone.dm
@@ -0,0 +1,123 @@
+/**
+ * ## Cone spells
+ *
+ * Cone spells shoot off as a cone from the caster.
+ */
+/datum/action/cooldown/spell/cone
+ /// This controls how many levels the cone has. Increase this value to make a bigger cone.
+ var/cone_levels = 3
+ /// This value determines if the cone penetrates walls.
+ var/respect_density = FALSE
+
+/datum/action/cooldown/spell/cone/cast(atom/cast_on)
+ . = ..()
+ var/list/cone_turfs = get_cone_turfs(get_turf(cast_on), cast_on.dir, cone_levels)
+ SEND_SIGNAL(src, COMSIG_SPELL_CONE_ON_CAST, cone_turfs, cast_on)
+ make_cone(cone_turfs, cast_on)
+
+/datum/action/cooldown/spell/cone/proc/make_cone(list/cone_turfs, atom/caster)
+ for(var/list/turf_list in cone_turfs)
+ do_cone_effects(turf_list, caster)
+
+/// This proc does obj, mob and turf cone effects on all targets in the passed list.
+/datum/action/cooldown/spell/cone/proc/do_cone_effects(list/target_turf_list, atom/caster, level = 1)
+ SEND_SIGNAL(src, COMSIG_SPELL_CONE_ON_LAYER_EFFECT, target_turf_list, caster, level)
+ for(var/turf/target_turf as anything in target_turf_list)
+ if(QDELETED(target_turf)) //if turf is no longer there
+ continue
+
+ do_turf_cone_effect(target_turf, caster, level)
+ if(!isopenturf(target_turf))
+ continue
+
+ for(var/atom/movable/movable_content as anything in target_turf)
+ if(isobj(movable_content))
+ do_obj_cone_effect(movable_content, level)
+ else if(isliving(movable_content))
+ do_mob_cone_effect(movable_content, level)
+
+///This proc deterimines how the spell will affect turfs.
+/datum/action/cooldown/spell/cone/proc/do_turf_cone_effect(turf/target_turf, atom/caster, level)
+ return
+
+///This proc deterimines how the spell will affect objects.
+/datum/action/cooldown/spell/cone/proc/do_obj_cone_effect(obj/target_obj, atom/caster, level)
+ return
+
+///This proc deterimines how the spell will affect mobs.
+/datum/action/cooldown/spell/cone/proc/do_mob_cone_effect(mob/living/target_mob, atom/caster, level)
+ return
+
+///This proc creates a list of turfs that are hit by the cone.
+/datum/action/cooldown/spell/cone/proc/get_cone_turfs(turf/starter_turf, dir_to_use, cone_levels = 3)
+ var/list/turfs_to_return = list()
+ var/turf/turf_to_use = starter_turf
+ var/turf/left_turf
+ var/turf/right_turf
+ var/right_dir
+ var/left_dir
+ switch(dir_to_use)
+ if(NORTH)
+ left_dir = WEST
+ right_dir = EAST
+ if(SOUTH)
+ left_dir = EAST
+ right_dir = WEST
+ if(EAST)
+ left_dir = NORTH
+ right_dir = SOUTH
+ if(WEST)
+ left_dir = SOUTH
+ right_dir = NORTH
+
+ for(var/i in 1 to cone_levels)
+ var/list/level_turfs = list()
+ turf_to_use = get_step(turf_to_use, dir_to_use)
+ level_turfs += turf_to_use
+ if(i != 1)
+ left_turf = get_step(turf_to_use, left_dir)
+ level_turfs += left_turf
+ right_turf = get_step(turf_to_use, right_dir)
+ level_turfs += right_turf
+ for(var/left_i in 1 to i -calculate_cone_shape(i))
+ if(left_turf.density && respect_density)
+ break
+ left_turf = get_step(left_turf, left_dir)
+ level_turfs += left_turf
+ for(var/right_i in 1 to i -calculate_cone_shape(i))
+ if(right_turf.density && respect_density)
+ break
+ right_turf = get_step(right_turf, right_dir)
+ level_turfs += right_turf
+ turfs_to_return += list(level_turfs)
+ if(i == cone_levels)
+ continue
+ if(turf_to_use.density && respect_density)
+ break
+ return turfs_to_return
+
+///This proc adjusts the cones width depending on the level.
+/datum/action/cooldown/spell/cone/proc/calculate_cone_shape(current_level)
+ var/end_taper_start = round(cone_levels * 0.8)
+ if(current_level > end_taper_start)
+ return (current_level % end_taper_start) * 2 //someone more talented and probably come up with a better formula.
+ else
+ return 2
+
+/**
+ * ### Staggered Cone
+ *
+ * Staggered Cone spells will reach each cone level
+ * gradually / with a delay, instead of affecting the entire
+ * cone area at once.
+ */
+/datum/action/cooldown/spell/cone/staggered
+
+ /// The delay between each cone level triggering.
+ var/delay_between_level = 0.2 SECONDS
+
+/datum/action/cooldown/spell/cone/staggered/make_cone(list/cone_turfs, atom/caster)
+ var/level_counter = 0
+ for(var/list/turf_list in cone_turfs)
+ level_counter++
+ addtimer(CALLBACK(src, .proc/do_cone_effects, turf_list, caster, level_counter), delay_between_level * level_counter)
diff --git a/code/modules/spells/spell_types/cone_spells.dm b/code/modules/spells/spell_types/cone_spells.dm
deleted file mode 100644
index 47f3348778d98..0000000000000
--- a/code/modules/spells/spell_types/cone_spells.dm
+++ /dev/null
@@ -1,117 +0,0 @@
-/obj/effect/proc_holder/spell/cone
- name = "Cone of Nothing"
- desc = "Does nothing in a cone! Wow!"
- school = SCHOOL_EVOCATION
- charge_max = 100
- clothes_req = FALSE
- invocation = "FUKAN NOTHAN"
- invocation_type = INVOCATION_SHOUT
- sound = 'sound/magic/forcewall.ogg'
- action_icon_state = "shield"
- range = -1
- cooldown_min = 0.5 SECONDS
- ///This controls how many levels the cone has, increase this value to make a bigger cone.
- var/cone_levels = 3
- ///This value determines if the cone penetrates walls.
- var/respect_density = FALSE
-
-/obj/effect/proc_holder/spell/cone/choose_targets(mob/user = usr)
- perform(null, user=user)
-
-///This proc creates a list of turfs that are hit by the cone
-/obj/effect/proc_holder/spell/cone/proc/cone_helper(turf/starter_turf, dir_to_use, cone_levels = 3)
- var/list/turfs_to_return = list()
- var/turf/turf_to_use = starter_turf
- var/turf/left_turf
- var/turf/right_turf
- var/right_dir
- var/left_dir
- switch(dir_to_use)
- if(NORTH)
- left_dir = WEST
- right_dir = EAST
- if(SOUTH)
- left_dir = EAST
- right_dir = WEST
- if(EAST)
- left_dir = NORTH
- right_dir = SOUTH
- if(WEST)
- left_dir = SOUTH
- right_dir = NORTH
-
-
- for(var/i in 1 to cone_levels)
- var/list/level_turfs = list()
- turf_to_use = get_step(turf_to_use, dir_to_use)
- level_turfs += turf_to_use
- if(i != 1)
- left_turf = get_step(turf_to_use, left_dir)
- level_turfs += left_turf
- right_turf = get_step(turf_to_use, right_dir)
- level_turfs += right_turf
- for(var/left_i in 1 to i -calculate_cone_shape(i))
- if(left_turf.density && respect_density)
- break
- left_turf = get_step(left_turf, left_dir)
- level_turfs += left_turf
- for(var/right_i in 1 to i -calculate_cone_shape(i))
- if(right_turf.density && respect_density)
- break
- right_turf = get_step(right_turf, right_dir)
- level_turfs += right_turf
- turfs_to_return += list(level_turfs)
- if(i == cone_levels)
- continue
- if(turf_to_use.density && respect_density)
- break
- return turfs_to_return
-
-/obj/effect/proc_holder/spell/cone/cast(list/targets,mob/user = usr)
- var/list/cone_turfs = cone_helper(get_turf(user), user.dir, cone_levels)
- for(var/list/turf_list in cone_turfs)
- do_cone_effects(turf_list)
-
-///This proc does obj, mob and turf cone effects on all targets in a list
-/obj/effect/proc_holder/spell/cone/proc/do_cone_effects(list/target_turf_list, level)
- for(var/target_turf in target_turf_list)
- if(!target_turf) //if turf is no longer there
- continue
- do_turf_cone_effect(target_turf, level)
- if(isopenturf(target_turf))
- var/turf/open/open_turf = target_turf
- for(var/movable_content in open_turf)
- if(isobj(movable_content))
- do_obj_cone_effect(movable_content, level)
- else if(isliving(movable_content))
- do_mob_cone_effect(movable_content, level)
-
-///This proc deterimines how the spell will affect turfs.
-/obj/effect/proc_holder/spell/cone/proc/do_turf_cone_effect(turf/target_turf, level)
- return
-
-///This proc deterimines how the spell will affect objects.
-/obj/effect/proc_holder/spell/cone/proc/do_obj_cone_effect(obj/target_obj, level)
- return
-
-///This proc deterimines how the spell will affect mobs.
-/obj/effect/proc_holder/spell/cone/proc/do_mob_cone_effect(mob/living/target_mob, level)
- return
-
-///This proc adjusts the cones width depending on the level.
-/obj/effect/proc_holder/spell/cone/proc/calculate_cone_shape(current_level)
- var/end_taper_start = round(cone_levels * 0.8)
- if(current_level > end_taper_start)
- return (current_level % end_taper_start) * 2 //someone more talented and probably come up with a better formula.
- else
- return 2
-
-///This type of cone gradually affects each level of the cone instead of affecting the entire area at once.
-/obj/effect/proc_holder/spell/cone/staggered
-
-/obj/effect/proc_holder/spell/cone/staggered/cast(list/targets,mob/user = usr)
- var/level_counter = 0
- var/list/cone_turfs = cone_helper(get_turf(user), user.dir, cone_levels)
- for(var/list/turf_list in cone_turfs)
- level_counter++
- addtimer(CALLBACK(src, .proc/do_cone_effects, turf_list, level_counter), 2 * level_counter)
diff --git a/code/modules/spells/spell_types/conjure.dm b/code/modules/spells/spell_types/conjure.dm
deleted file mode 100644
index c91a6fb2b0537..0000000000000
--- a/code/modules/spells/spell_types/conjure.dm
+++ /dev/null
@@ -1,109 +0,0 @@
-/obj/effect/proc_holder/spell/aoe_turf/conjure
- name = "Conjure"
- desc = "This spell conjures objs of the specified types in range."
-
- school = SCHOOL_CONJURATION
-
- var/list/summon_type = list() //determines what exactly will be summoned
- //should be text, like list("/mob/living/simple_animal/bot/ed209")
-
- var/summon_lifespan = 0 // 0=permanent, any other time in deciseconds
- var/summon_amt = 1 //amount of objects summoned
- var/summon_ignore_density = FALSE //if set to TRUE, adds dense tiles to possible spawn places
- var/summon_ignore_prev_spawn_points = TRUE //if set to TRUE, each new object is summoned on a new spawn point
-
- var/list/new_vars = list() //vars of the summoned objects will be replaced with those where they meet
- //should have format of list("emagged" = 1,"name" = "Wizard's Justicebot"), for example
-
- var/cast_sound = 'sound/items/welder.ogg'
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/cast(list/targets,mob/user = usr)
- playsound(get_turf(user), cast_sound, 50,TRUE)
- for(var/turf/T in targets)
- if(T.density && !summon_ignore_density)
- targets -= T
-
- for(var/i in 1 to summon_amt)
- if(!targets.len)
- break
- var/summoned_object_type = pick(summon_type)
- var/spawn_place = pick(targets)
- if(summon_ignore_prev_spawn_points)
- targets -= spawn_place
- if(ispath(summoned_object_type, /turf))
- var/turf/O = spawn_place
- var/N = summoned_object_type
- O.ChangeTurf(N, flags = CHANGETURF_INHERIT_AIR)
- else
- var/atom/summoned_object = new summoned_object_type(spawn_place)
-
- for(var/varName in new_vars)
- if(varName in new_vars)
- summoned_object.vv_edit_var(varName, new_vars[varName])
- summoned_object.flags_1 |= ADMIN_SPAWNED_1
- if(summon_lifespan)
- QDEL_IN(summoned_object, summon_lifespan)
-
- post_summon(summoned_object, user)
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/proc/post_summon(atom/summoned_object, mob/user)
- return
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/summon_ed_swarm //test purposes - Also a lot of fun
- name = "Dispense Wizard Justice"
- desc = "This spell dispenses wizard justice."
- summon_type = list(/mob/living/simple_animal/bot/secbot/ed209)
- summon_amt = 10
- range = 3
- new_vars = list(
- "emagged" = 2,
- "remote_disabled" = 1,
- "shoot_sound" = 'sound/weapons/laser.ogg',
- "projectile" = /obj/projectile/beam/laser,
- "security_mode_flags" = ~(SECBOT_DECLARE_ARRESTS),
- "name" = "Wizard's Justicebot",
- )
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/link_worlds
- name = "Link Worlds"
- desc = "A whole new dimension for you to play with! They won't be happy about it, though."
- invocation = "WTF"
- clothes_req = FALSE
- charge_max = 600
- cooldown_min = 200
- summon_type = list(/obj/structure/spawner/nether)
- summon_amt = 1
- range = 1
- cast_sound = 'sound/weapons/marauder.ogg'
-
-/obj/effect/proc_holder/spell/targeted/conjure_item
- name = "Summon weapon"
- desc = "A generic spell that should not exist. This summons an instance of a specific type of item, or if one already exists, un-summons it. Summons into hand if possible."
- invocation_type = INVOCATION_NONE
- include_user = TRUE
- range = -1
- clothes_req = FALSE
- ///List of weakrefs to items summoned
- var/list/datum/weakref/item_refs = list()
- var/item_type = /obj/item/banhammer
- school = SCHOOL_CONJURATION
- charge_max = 150
- cooldown_min = 10
- var/delete_old = TRUE //TRUE to delete the last summoned object if it's still there, FALSE for infinite item stream weeeee
-
-/obj/effect/proc_holder/spell/targeted/conjure_item/cast(list/targets, mob/user = usr)
- if (delete_old && length(item_refs))
- QDEL_LIST(item_refs)
- return
- for(var/mob/living/carbon/C in targets)
- if(C.dropItemToGround(C.get_active_held_item()))
- C.put_in_hands(make_item(), TRUE)
-
-/obj/effect/proc_holder/spell/targeted/conjure_item/Destroy()
- QDEL_LIST(item_refs)
- return ..()
-
-/obj/effect/proc_holder/spell/targeted/conjure_item/proc/make_item()
- var/obj/item/item = new item_type
- item_refs += WEAKREF(item)
- return item
diff --git a/code/modules/spells/spell_types/conjure/_conjure.dm b/code/modules/spells/spell_types/conjure/_conjure.dm
new file mode 100644
index 0000000000000..9483bb57b43d1
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/_conjure.dm
@@ -0,0 +1,50 @@
+/datum/action/cooldown/spell/conjure
+ sound = 'sound/items/welder.ogg'
+ school = SCHOOL_CONJURATION
+
+ /// The radius around the caster the items will appear. 0 = spawns on top of the caster.
+ var/summon_radius = 7
+ /// A list of types that will be created on summon.
+ /// The type is picked from this list, not all provided are guaranteed.
+ var/list/summon_type = list()
+ /// How long before the summons will be despawned. Set to 0 for permanent.
+ var/summon_lifespan = 0
+ /// Amount of summons to create.
+ var/summon_amount = 1
+ /// If TRUE, summoned objects will not be spawned in dense turfs.
+ var/summon_respects_density = FALSE
+ /// If TRUE, no two summons can be spawned in the same turf.
+ var/summon_respects_prev_spawn_points = TRUE
+
+/datum/action/cooldown/spell/conjure/cast(atom/cast_on)
+ . = ..()
+ var/list/to_summon_in = list()
+ for(var/turf/summon_turf in range(summon_radius, cast_on))
+ if(summon_respects_density && summon_turf.density)
+ continue
+ to_summon_in += summon_turf
+
+ for(var/i in 1 to summon_amount)
+ if(!length(to_summon_in))
+ break
+
+ var/atom/summoned_object_type = pick(summon_type)
+ var/turf/spawn_place = pick(to_summon_in)
+ if(summon_respects_prev_spawn_points)
+ to_summon_in -= spawn_place
+
+ if(ispath(summoned_object_type, /turf))
+ spawn_place.ChangeTurf(summoned_object_type, flags = CHANGETURF_INHERIT_AIR)
+
+ else
+ var/atom/summoned_object = new summoned_object_type(spawn_place)
+
+ summoned_object.flags_1 |= ADMIN_SPAWNED_1
+ if(summon_lifespan > 0)
+ QDEL_IN(summoned_object, summon_lifespan)
+
+ post_summon(summoned_object, cast_on)
+
+/// Called on atoms summoned after they are created, allows extra variable editing and such of created objects
+/datum/action/cooldown/spell/conjure/proc/post_summon(atom/summoned_object, atom/cast_on)
+ return
diff --git a/code/modules/spells/spell_types/conjure/bees.dm b/code/modules/spells/spell_types/conjure/bees.dm
new file mode 100644
index 0000000000000..036abbc0f9b6f
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/bees.dm
@@ -0,0 +1,18 @@
+/datum/action/cooldown/spell/conjure/bee
+ name = "Lesser Summon Bees"
+ desc = "This spell magically kicks a transdimensional beehive, \
+ instantly summoning a swarm of bees to your location. \
+ These bees are NOT friendly to anyone."
+ button_icon_state = "bee"
+ sound = 'sound/voice/moth/scream_moth.ogg'
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 1 MINUTES
+ cooldown_reduction_per_rank = 10 SECONDS
+
+ invocation = "NOT THE BEES"
+ invocation_type = INVOCATION_SHOUT
+
+ summon_radius = 3
+ summon_type = list(/mob/living/simple_animal/hostile/bee/toxin)
+ summon_amount = 9
diff --git a/code/modules/spells/spell_types/conjure/carp.dm b/code/modules/spells/spell_types/conjure/carp.dm
new file mode 100644
index 0000000000000..45007ee85037b
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/carp.dm
@@ -0,0 +1,13 @@
+/datum/action/cooldown/spell/conjure/carp
+ name = "Summon Carp"
+ desc = "This spell conjures a simple carp."
+ sound = 'sound/magic/summon_karp.ogg'
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 2 MINUTES
+
+ invocation = "NOUK FHUNMM SACP RISSKA"
+ invocation_type = INVOCATION_SHOUT
+
+ summon_radius = 1
+ summon_type = list(/mob/living/simple_animal/hostile/carp)
diff --git a/code/modules/spells/spell_types/conjure/constructs.dm b/code/modules/spells/spell_types/conjure/constructs.dm
new file mode 100644
index 0000000000000..50124ce1319fa
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/constructs.dm
@@ -0,0 +1,20 @@
+/datum/action/cooldown/spell/conjure/construct
+ name = "Summon Construct Shell"
+ desc = "This spell conjures a construct which may be controlled by Shades."
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "artificer"
+ sound = 'sound/magic/summonitems_generic.ogg'
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 1 MINUTES
+
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+
+ summon_radius = 0
+ summon_type = list(/obj/structure/constructshell)
+
+/datum/action/cooldown/spell/conjure/construct/lesser // Used by artificers.
+ name = "Create Construct Shell"
+ background_icon_state = "bg_demon"
+ cooldown_time = 3 MINUTES
diff --git a/code/modules/spells/spell_types/conjure/creatures.dm b/code/modules/spells/spell_types/conjure/creatures.dm
new file mode 100644
index 0000000000000..c51d4d114df00
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/creatures.dm
@@ -0,0 +1,15 @@
+/datum/action/cooldown/spell/conjure/creature
+ name = "Summon Creature Swarm"
+ desc = "This spell tears the fabric of reality, allowing horrific daemons to spill forth."
+ sound = 'sound/magic/summonitems_generic.ogg'
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 2 MINUTES
+
+ invocation = "IA IA"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = NONE
+
+ summon_radius = 3
+ summon_type = list(/mob/living/simple_animal/hostile/netherworld)
+ summon_amount = 10
diff --git a/code/modules/spells/spell_types/conjure/cult_turfs.dm b/code/modules/spells/spell_types/conjure/cult_turfs.dm
new file mode 100644
index 0000000000000..7fec43aa8d404
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/cult_turfs.dm
@@ -0,0 +1,29 @@
+/datum/action/cooldown/spell/conjure/cult_floor
+ name = "Summon Cult Floor"
+ desc = "This spell constructs a cult floor."
+ background_icon_state = "bg_cult"
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "floorconstruct"
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 2 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+
+ summon_radius = 0
+ summon_type = list(/turf/open/floor/engine/cult)
+
+/datum/action/cooldown/spell/conjure/cult_wall
+ name = "Summon Cult Wall"
+ desc = "This spell constructs a cult wall."
+ background_icon_state = "bg_cult"
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "lesserconstruct"
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 10 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+
+ summon_radius = 0
+ summon_type = list(/turf/closed/wall/mineral/cult/artificer) // We don't want artificer-based runed metal farms.
diff --git a/code/modules/spells/spell_types/conjure/ed_swarm.dm b/code/modules/spells/spell_types/conjure/ed_swarm.dm
new file mode 100644
index 0000000000000..db122e4c846a7
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/ed_swarm.dm
@@ -0,0 +1,22 @@
+// test purposes - Also a lot of fun
+/datum/action/cooldown/spell/conjure/summon_ed_swarm
+ name = "Dispense Wizard Justice"
+ desc = "This spell dispenses wizard justice."
+
+ summon_radius = 3
+ summon_type = list(/mob/living/simple_animal/bot/secbot/ed209)
+ summon_amount = 10
+
+/datum/action/cooldown/spell/conjure/summon_ed_swarm/post_summon(atom/summoned_object, atom/cast_on)
+ if(!istype(summoned_object, /mob/living/simple_animal/bot/secbot/ed209))
+ return
+
+ var/mob/living/simple_animal/bot/secbot/ed209/summoned_bot = summoned_object
+ summoned_bot.name = "Wizard's Justicebot"
+
+ summoned_bot.security_mode_flags = ~SECBOT_DECLARE_ARRESTS
+ summoned_bot.bot_mode_flags &= ~BOT_MODE_REMOTE_ENABLED
+ summoned_bot.bot_mode_flags |= BOT_COVER_EMAGGED
+
+ summoned_bot.projectile = /obj/projectile/beam/laser
+ summoned_bot.shoot_sound = 'sound/weapons/laser.ogg'
diff --git a/code/modules/spells/spell_types/conjure/invisible_chair.dm b/code/modules/spells/spell_types/conjure/invisible_chair.dm
new file mode 100644
index 0000000000000..e0694898c096c
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/invisible_chair.dm
@@ -0,0 +1,34 @@
+/datum/action/cooldown/spell/conjure/invisible_chair
+ name = "Invisible Chair"
+ desc = "The mime's performance transmutates a chair into physical reality."
+ background_icon_state = "bg_mime"
+ icon_icon = 'icons/mob/actions/actions_mime.dmi'
+ button_icon_state = "invisible_chair"
+ panel = "Mime"
+ sound = null
+
+ school = SCHOOL_MIME
+ cooldown_time = 30 SECONDS
+ invocation = "Someone does a weird gesture." // Overriden in before cast
+ invocation_self_message = span_notice("You conjure an invisible chair and sit down.")
+ invocation_type = INVOCATION_EMOTE
+
+ spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIME_VOW
+ antimagic_flags = NONE
+ spell_max_level = 1
+
+ summon_radius = 0
+ summon_type = list(/obj/structure/chair/mime)
+ summon_lifespan = 25 SECONDS
+
+/datum/action/cooldown/spell/conjure/invisible_chair/before_cast(atom/cast_on)
+ . = ..()
+ invocation = span_notice("[cast_on] pulls out an invisible chair and sits down.")
+
+/datum/action/cooldown/spell/conjure/invisible_chair/post_summon(atom/summoned_object, mob/living/carbon/human/cast_on)
+ if(!isobj(summoned_object))
+ return
+
+ var/obj/chair = summoned_object
+ chair.setDir(cast_on.dir)
+ chair.buckle_mob(cast_on)
diff --git a/code/modules/spells/spell_types/conjure/invisible_wall.dm b/code/modules/spells/spell_types/conjure/invisible_wall.dm
new file mode 100644
index 0000000000000..9433fbd7df0a5
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/invisible_wall.dm
@@ -0,0 +1,26 @@
+/datum/action/cooldown/spell/conjure/invisible_wall
+ name = "Invisible Wall"
+ desc = "The mime's performance transmutates a wall into physical reality."
+ background_icon_state = "bg_mime"
+ icon_icon = 'icons/mob/actions/actions_mime.dmi'
+ button_icon_state = "invisible_wall"
+ panel = "Mime"
+ sound = null
+
+ school = SCHOOL_MIME
+ cooldown_time = 30 SECONDS
+ invocation = "Someone does a weird gesture." // Overriden in before cast
+ invocation_self_message = span_notice("You form a wall in front of yourself.")
+ invocation_type = INVOCATION_EMOTE
+
+ spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIME_VOW
+ antimagic_flags = NONE
+ spell_max_level = 1
+
+ summon_radius = 0
+ summon_type = list(/obj/effect/forcefield/mime)
+ summon_lifespan = 30 SECONDS
+
+/datum/action/cooldown/spell/conjure/invisible_wall/before_cast(atom/cast_on)
+ . = ..()
+ invocation = span_notice("[cast_on] looks as if a wall is in front of [cast_on.p_them()].")
diff --git a/code/modules/spells/spell_types/conjure/link_worlds.dm b/code/modules/spells/spell_types/conjure/link_worlds.dm
new file mode 100644
index 0000000000000..f227fc1a13e9a
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/link_worlds.dm
@@ -0,0 +1,15 @@
+/datum/action/cooldown/spell/conjure/link_worlds
+ name = "Link Worlds"
+ desc = "A whole new dimension for you to play with! They won't be happy about it, though."
+
+ sound = 'sound/weapons/marauder.ogg'
+ cooldown_time = 1 MINUTES
+ cooldown_reduction_per_rank = 10 SECONDS
+
+ invocation = "WTF"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = NONE
+
+ summon_radius = 1
+ summon_type = list(/obj/structure/spawner/nether)
+ summon_amount = 1
diff --git a/code/modules/spells/spell_types/conjure/presents.dm b/code/modules/spells/spell_types/conjure/presents.dm
new file mode 100644
index 0000000000000..057fef9b9b4a8
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/presents.dm
@@ -0,0 +1,14 @@
+/datum/action/cooldown/spell/conjure/presents
+ name = "Conjure Presents!"
+ desc = "This spell lets you reach into S-space and retrieve presents! Yay!"
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 1 MINUTES
+ cooldown_reduction_per_rank = 13.75 SECONDS
+
+ invocation = "HO HO HO"
+ invocation_type = INVOCATION_SHOUT
+
+ summon_radius = 3
+ summon_type = list(/obj/item/a_gift)
+ summon_amount = 5
diff --git a/code/modules/spells/spell_types/conjure/soulstone.dm b/code/modules/spells/spell_types/conjure/soulstone.dm
new file mode 100644
index 0000000000000..cce5d1ab797ca
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/soulstone.dm
@@ -0,0 +1,30 @@
+/datum/action/cooldown/spell/conjure/soulstone
+ name = "Summon Soulstone"
+ desc = "This spell reaches into Nar'Sie's realm, summoning one of the legendary fragments across time and space."
+ background_icon_state = "bg_demon"
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "summonsoulstone"
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 4 MINUTES
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+
+ summon_radius = 0
+ summon_type = list(/obj/item/soulstone)
+
+/datum/action/cooldown/spell/conjure/soulstone/cult
+ name = "Create Nar'sian Soulstone"
+ cooldown_time = 6 MINUTES
+
+/datum/action/cooldown/spell/conjure/soulstone/noncult
+ name = "Create Soulstone"
+ summon_type = list(/obj/item/soulstone/anybody)
+
+/datum/action/cooldown/spell/conjure/soulstone/purified
+ name = "Create Purified Soulstone"
+ summon_type = list(/obj/item/soulstone/anybody/purified)
+
+/datum/action/cooldown/spell/conjure/soulstone/mystic
+ name = "Create Mystic Soulstone"
+ summon_type = list(/obj/item/soulstone/mystic)
diff --git a/code/modules/spells/spell_types/conjure/the_traps.dm b/code/modules/spells/spell_types/conjure/the_traps.dm
new file mode 100644
index 0000000000000..e9717a1325329
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure/the_traps.dm
@@ -0,0 +1,35 @@
+/datum/action/cooldown/spell/conjure/the_traps
+ name = "The Traps!"
+ desc = "Summon a number of traps around you. They will damage and enrage any enemies that step on them."
+ button_icon_state = "the_traps"
+
+ cooldown_time = 25 SECONDS
+ cooldown_reduction_per_rank = 5 SECONDS
+
+ invocation = "CAVERE INSIDIAS"
+ invocation_type = INVOCATION_SHOUT
+
+ summon_radius = 3
+ summon_type = list(
+ /obj/structure/trap/stun,
+ /obj/structure/trap/fire,
+ /obj/structure/trap/chill,
+ /obj/structure/trap/damage,
+ )
+ summon_lifespan = 5 MINUTES
+ summon_amount = 5
+
+ /// The amount of charges the traps spawn with.
+ var/trap_charges = 1
+
+/datum/action/cooldown/spell/conjure/the_traps/post_summon(atom/summoned_object, atom/cast_on)
+ if(!istype(summoned_object, /obj/structure/trap))
+ return
+
+ var/obj/structure/trap/summoned_trap = summoned_object
+ summoned_trap.charges = trap_charges
+
+ if(ismob(cast_on))
+ var/mob/mob_caster = cast_on
+ if(mob_caster.mind)
+ summoned_trap.immune_minds += owner.mind
diff --git a/code/modules/spells/spell_types/conjure_item/_conjure_item.dm b/code/modules/spells/spell_types/conjure_item/_conjure_item.dm
new file mode 100644
index 0000000000000..5abac48b19733
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure_item/_conjure_item.dm
@@ -0,0 +1,46 @@
+/datum/action/cooldown/spell/conjure_item
+ school = SCHOOL_CONJURATION
+ invocation_type = INVOCATION_NONE
+
+ /// Typepath of whatever item we summon
+ var/obj/item/item_type
+ /// If TRUE, we delete any previously created items when we cast the spell
+ var/delete_old = TRUE
+ /// List of weakrefs to items summoned
+ var/list/datum/weakref/item_refs
+
+/datum/action/cooldown/spell/conjure_item/Destroy()
+ // If we delete_old, clean up all of our items on delete
+ if(delete_old)
+ QDEL_LAZYLIST(item_refs)
+
+ // If we don't delete_old, just let all the items be free
+ else
+ LAZYNULL(item_refs)
+
+ return ..()
+
+/datum/action/cooldown/spell/conjure_item/is_valid_target(atom/cast_on)
+ return iscarbon(cast_on)
+
+/datum/action/cooldown/spell/conjure_item/cast(mob/living/carbon/cast_on)
+ if(delete_old && LAZYLEN(item_refs))
+ QDEL_LAZYLIST(item_refs)
+
+ var/obj/item/existing_item = cast_on.get_active_held_item()
+ if(existing_item)
+ cast_on.dropItemToGround(existing_item)
+
+ var/obj/item/created = make_item()
+ if(QDELETED(created))
+ CRASH("[type] tried to create an item, but failed. It's item type is [item_type].")
+
+ cast_on.put_in_hands(created, del_on_fail = TRUE)
+ return ..()
+
+/// Instantiates the item we're conjuring and returns it.
+/// Item is made in nullspace and moved out in cast().
+/datum/action/cooldown/spell/conjure_item/proc/make_item()
+ var/obj/item/made_item = new item_type()
+ LAZYADD(item_refs, WEAKREF(made_item))
+ return made_item
diff --git a/code/modules/spells/spell_types/conjure_item/infinite_guns.dm b/code/modules/spells/spell_types/conjure_item/infinite_guns.dm
new file mode 100644
index 0000000000000..98921da4879dc
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure_item/infinite_guns.dm
@@ -0,0 +1,41 @@
+/datum/action/cooldown/spell/conjure_item/infinite_guns
+ school = SCHOOL_CONJURATION
+ cooldown_time = 1.25 MINUTES
+ cooldown_reduction_per_rank = 18.5 SECONDS
+
+ invocation_type = INVOCATION_NONE
+
+ item_type = /obj/item/gun/ballistic/rifle
+ // Enchanted guns self delete / do wacky stuff, anyways
+ delete_old = FALSE
+
+/datum/action/cooldown/spell/conjure_item/infinite_guns/Remove(mob/living/remove_from)
+ var/obj/item/existing = remove_from.is_holding_item_of_type(item_type)
+ if(existing)
+ qdel(existing)
+
+ return ..()
+
+// Because enchanted guns self-delete and regenerate themselves,
+// override make_item here and let's not bother with tracking their weakrefs.
+/datum/action/cooldown/spell/conjure_item/infinite_guns/make_item()
+ return new item_type()
+
+/datum/action/cooldown/spell/conjure_item/infinite_guns/gun
+ name = "Lesser Summon Guns"
+ desc = "Why reload when you have infinite guns? \
+ Summons an unending stream of bolt action rifles that deal little damage, \
+ but will knock targets down. Requires both hands free to use. \
+ Learning this spell makes you unable to learn Arcane Barrage."
+ button_icon_state = "bolt_action"
+
+ item_type = /obj/item/gun/ballistic/rifle/enchanted
+
+/datum/action/cooldown/spell/conjure_item/infinite_guns/arcane_barrage
+ name = "Arcane Barrage"
+ desc = "Fire a torrent of arcane energy at your foes with this (powerful) spell. \
+ Deals much more damage than Lesser Summon Guns, but won't knock targets down. Requires both hands free to use. \
+ Learning this spell makes you unable to learn Lesser Summon Gun."
+ button_icon_state = "arcane_barrage"
+
+ item_type = /obj/item/gun/ballistic/rifle/enchanted/arcane_barrage
diff --git a/code/modules/spells/spell_types/conjure_item/invisible_box.dm b/code/modules/spells/spell_types/conjure_item/invisible_box.dm
new file mode 100644
index 0000000000000..b3d55fd74d8cf
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure_item/invisible_box.dm
@@ -0,0 +1,44 @@
+
+
+/datum/action/cooldown/spell/conjure_item/invisible_box
+ name = "Invisible Box"
+ desc = "The mime's performance transmutates a box into physical reality."
+ background_icon_state = "bg_mime"
+ icon_icon = 'icons/mob/actions/actions_mime.dmi'
+ button_icon_state = "invisible_box"
+ panel = "Mime"
+ sound = null
+
+ school = SCHOOL_MIME
+ cooldown_time = 30 SECONDS
+ invocation = "Someone does a weird gesture." // Overriden in before cast
+ invocation_self_message = span_notice("You conjure up an invisible box, large enough to store a few things.")
+ invocation_type = INVOCATION_EMOTE
+
+ spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIME_VOW
+ antimagic_flags = NONE
+ spell_max_level = 1
+
+ delete_old = FALSE
+ item_type = /obj/item/storage/box/mime
+ /// How long boxes last before going away
+ var/box_lifespan = 50 SECONDS
+
+/datum/action/cooldown/spell/conjure_item/invisible_box/before_cast(atom/cast_on)
+ . = ..()
+ invocation = span_notice("[cast_on] moves [cast_on.p_their()] hands in the shape of a cube, pressing a box out of the air.")
+
+/datum/action/cooldown/spell/conjure_item/invisible_box/make_item()
+ . = ..()
+ var/obj/item/made_box = .
+ made_box.alpha = 255
+ addtimer(CALLBACK(src, .proc/cleanup_box, made_box), box_lifespan)
+
+/// Callback that gets rid out of box and removes the weakref from our list
+/datum/action/cooldown/spell/conjure_item/invisible_box/proc/cleanup_box(obj/item/storage/box/box)
+ if(QDELETED(box) || !istype(box))
+ return
+
+ box.emptyStorage()
+ LAZYREMOVE(item_refs, WEAKREF(box))
+ qdel(box)
diff --git a/code/modules/spells/spell_types/conjure_item/lighting_packet.dm b/code/modules/spells/spell_types/conjure_item/lighting_packet.dm
new file mode 100644
index 0000000000000..9ae2010374c17
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure_item/lighting_packet.dm
@@ -0,0 +1,39 @@
+
+/datum/action/cooldown/spell/conjure_item/spellpacket
+ name = "Thrown Lightning"
+ desc = "Forged from eldrich energies, a packet of pure power, \
+ known as a spell packet will appear in your hand, that - when thrown - will stun the target."
+ button_icon_state = "thrownlightning"
+
+ cooldown_time = 1 SECONDS
+ spell_max_level = 1
+
+ item_type = /obj/item/spellpacket/lightningbolt
+
+/datum/action/cooldown/spell/conjure_item/spellpacket/cast(mob/living/carbon/cast_on)
+ . = ..()
+ cast_on.throw_mode_on(THROW_MODE_TOGGLE)
+
+/obj/item/spellpacket/lightningbolt
+ name = "\improper Lightning bolt Spell Packet"
+ desc = "Some birdseed wrapped in cloth that crackles with electricity."
+ icon = 'icons/obj/toy.dmi'
+ icon_state = "snappop"
+ w_class = WEIGHT_CLASS_TINY
+
+/obj/item/spellpacket/lightningbolt/throw_impact(atom/hit_atom, datum/thrownthing/throwingdatum)
+ . = ..()
+ if(.)
+ return
+
+ if(isliving(hit_atom))
+ var/mob/living/hit_living = hit_atom
+ if(!hit_living.can_block_magic())
+ hit_living.electrocute_act(80, src, flags = SHOCK_ILLUSION)
+ qdel(src)
+
+/obj/item/spellpacket/lightningbolt/throw_at(atom/target, range, speed, mob/thrower, spin = TRUE, diagonals_first = FALSE, datum/callback/callback, force = INFINITY, quickstart = TRUE)
+ . = ..()
+ if(ishuman(thrower))
+ var/mob/living/carbon/human/human_thrower = thrower
+ human_thrower.say("LIGHTNINGBOLT!!", forced = "spell")
diff --git a/code/modules/spells/spell_types/conjure_item/snowball.dm b/code/modules/spells/spell_types/conjure_item/snowball.dm
new file mode 100644
index 0000000000000..bbc783a48edfe
--- /dev/null
+++ b/code/modules/spells/spell_types/conjure_item/snowball.dm
@@ -0,0 +1,8 @@
+/datum/action/cooldown/spell/conjure_item/snowball
+ name = "Snowball"
+ desc = "Concentrates cryokinetic forces to create snowballs, useful for throwing at people."
+ icon_icon = 'icons/obj/toy.dmi'
+ button_icon_state = "snowball"
+
+ cooldown_time = 1.5 SECONDS
+ item_type = /obj/item/toy/snowball
diff --git a/code/modules/spells/spell_types/construct_spells.dm b/code/modules/spells/spell_types/construct_spells.dm
deleted file mode 100644
index 35f9810e489b5..0000000000000
--- a/code/modules/spells/spell_types/construct_spells.dm
+++ /dev/null
@@ -1,341 +0,0 @@
-//////////////////////////////Construct Spells/////////////////////////
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser
- charge_max = 3 MINUTES
- action_background_icon_state = "bg_demon"
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/construct/lesser/cult
- clothes_req = TRUE
- charge_max = 250 SECONDS
-
-/obj/effect/proc_holder/spell/aoe_turf/area_conversion
- name = "Area Conversion"
- desc = "This spell instantly converts a small area around you."
-
- school = SCHOOL_TRANSMUTATION
- charge_max = 5 SECONDS
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- range = 2
- action_icon = 'icons/mob/actions/actions_cult.dmi'
- action_icon_state = "areaconvert"
- action_background_icon_state = "bg_cult"
-
-/obj/effect/proc_holder/spell/aoe_turf/area_conversion/cast(list/targets, mob/user = usr)
- playsound(get_turf(user), 'sound/items/welder.ogg', 75, TRUE)
- for(var/turf/T in targets)
- T.narsie_act(FALSE, TRUE, 100 - (get_dist(user, T) * 25))
-
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/floor
- name = "Summon Cult Floor"
- desc = "This spell constructs a cult floor."
-
- school = SCHOOL_CONJURATION
- charge_max = 2 SECONDS
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- range = 0
- summon_type = list(/turf/open/floor/engine/cult)
- action_icon = 'icons/mob/actions/actions_cult.dmi'
- action_icon_state = "floorconstruct"
- action_background_icon_state = "bg_cult"
-
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/wall
- name = "Summon Cult Wall"
- desc = "This spell constructs a cult wall."
-
- school = SCHOOL_CONJURATION
- charge_max = 10 SECONDS
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- range = 0
- action_icon = 'icons/mob/actions/actions_cult.dmi'
- action_icon_state = "lesserconstruct"
- action_background_icon_state = "bg_cult"
-
- summon_type = list(/turf/closed/wall/mineral/cult/artificer) //we don't want artificer-based runed metal farms
-
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/wall/reinforced
- name = "Greater Construction"
- desc = "This spell constructs a reinforced metal wall."
-
- school = SCHOOL_CONJURATION
- charge_max = 30 SECONDS
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- range = 0
-
- summon_type = list(/turf/closed/wall/r_wall)
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone
- name = "Summon Soulstone"
- desc = "This spell reaches into Nar'Sie's realm, summoning one of the legendary fragments across time and space."
-
- school = SCHOOL_CONJURATION
- charge_max = 4 MINUTES
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- range = 0
- action_icon = 'icons/mob/actions/actions_cult.dmi'
- action_icon_state = "summonsoulstone"
- action_background_icon_state = "bg_demon"
-
- summon_type = list(/obj/item/soulstone)
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/cult
- clothes_req = TRUE
- charge_max = 6 MINUTES
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/noncult
- summon_type = list(/obj/item/soulstone/anybody)
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/purified
- summon_type = list(/obj/item/soulstone/anybody/purified)
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/soulstone/mystic
- summon_type = list(/obj/item/soulstone/mystic)
-
-/obj/effect/proc_holder/spell/targeted/forcewall/cult
- name = "Shield"
- desc = "This spell creates a temporary forcefield to shield yourself and allies from incoming fire."
- school = SCHOOL_TRANSMUTATION
- charge_max = 40 SECONDS
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- wall_type = /obj/effect/forcefield/cult
- action_icon = 'icons/mob/actions/actions_cult.dmi'
- action_icon_state = "cultforcewall"
- action_background_icon_state = "bg_demon"
-
-
-
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift
- name = "Phase Shift"
- desc = "This spell allows you to pass through walls."
-
- school = SCHOOL_TRANSMUTATION
- charge_max = 25 SECONDS
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- jaunt_duration = 5 SECONDS
- action_icon = 'icons/mob/actions/actions_cult.dmi'
- action_icon_state = "phaseshift"
- action_background_icon_state = "bg_demon"
- jaunt_in_time = 0.6 SECONDS
- jaunt_out_time = 0.6 SECONDS
- jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith
- jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out
-
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/jaunt_steam(mobloc)
- return
-
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/angelic
- jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith/angelic
- jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out/angelic
-
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/mystic
- jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith/mystic
- jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out/mystic
-
-/obj/effect/proc_holder/spell/targeted/projectile/magic_missile/lesser
- name = "Lesser Magic Missile"
- desc = "This spell fires several, slow moving, magic projectiles at nearby targets."
-
- school = SCHOOL_EVOCATION
- charge_max = 40 SECONDS
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- max_targets = 6
- action_icon_state = "magicm"
- action_background_icon_state = "bg_demon"
- proj_type = /obj/projectile/magic/spell/magic_missile/lesser
-
-/obj/projectile/magic/spell/magic_missile/lesser
- color = "red" //Looks more culty this way
- range = 10
-
-/obj/effect/proc_holder/spell/targeted/smoke/disable
- name = "Paralysing Smoke"
- desc = "This spell spawns a cloud of paralysing smoke."
-
- school = SCHOOL_CONJURATION
- charge_max = 20 SECONDS
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- range = -1
- include_user = TRUE
- cooldown_min = 20 //25 deciseconds reduction per rank
-
- smoke_spread = /datum/effect_system/fluid_spread/smoke/sleeping
- smoke_amt = 4
- action_icon_state = "smoke"
- action_background_icon_state = "bg_cult"
-
-/obj/effect/proc_holder/spell/pointed/abyssal_gaze
- name = "Abyssal Gaze"
- desc = "This spell instills a deep terror in your target, temporarily chilling and blinding it."
- charge_max = 75 SECONDS
- range = 5
- stat_allowed = FALSE
- school = SCHOOL_EVOCATION
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi'
- action_icon = 'icons/mob/actions/actions_cult.dmi'
- action_background_icon_state = "bg_demon"
- action_icon_state = "abyssal_gaze"
- active_msg = "You prepare to instill a deep terror in a target..."
-
-/obj/effect/proc_holder/spell/pointed/abyssal_gaze/cast(list/targets, mob/user)
- if(!LAZYLEN(targets))
- to_chat(user, span_warning("No target found in range!"))
- return FALSE
- if(!can_target(targets[1], user))
- return FALSE
-
- var/mob/living/carbon/target = targets[1]
- if(target.can_block_magic(MAGIC_RESISTANCE|MAGIC_RESISTANCE_HOLY))
- to_chat(user, span_warning("The spell had no effect!"))
- to_chat(target, span_warning("You feel a freezing darkness closing in on you, but it rapidly dissipates."))
- return FALSE
-
- to_chat(target, span_userdanger("A freezing darkness surrounds you..."))
- target.playsound_local(get_turf(target), 'sound/hallucinations/i_see_you1.ogg', 50, 1)
- user.playsound_local(get_turf(user), 'sound/effects/ghost2.ogg', 50, 1)
- target.become_blind(ABYSSAL_GAZE_BLIND)
- addtimer(CALLBACK(src, .proc/cure_blindness, target), 40)
- if(ishuman(targets[1]))
- var/mob/living/carbon/human/humi = targets[1]
- humi.adjust_coretemperature(-200)
- target.adjust_bodytemperature(-200)
-
-/**
- * cure_blidness: Cures Abyssal Gaze blindness from the target
- *
- * Arguments:
- * * target The mob that is being cured of the blindness.
- */
-/obj/effect/proc_holder/spell/pointed/abyssal_gaze/proc/cure_blindness(mob/target)
- if(isliving(target))
- var/mob/living/L = target
- L.cure_blind(ABYSSAL_GAZE_BLIND)
-
-/obj/effect/proc_holder/spell/pointed/abyssal_gaze/can_target(atom/target, mob/user, silent)
- . = ..()
- if(!.)
- return FALSE
- if(!iscarbon(target))
- if(!silent)
- to_chat(user, span_warning("You can only target carbon based lifeforms!"))
- return FALSE
- return TRUE
-
-/obj/effect/proc_holder/spell/pointed/dominate
- name = "Dominate"
- desc = "This spell dominates the mind of a lesser creature to the will of Nar'Sie, allying it only to her direct followers."
- charge_max = 1 MINUTES
- range = 7
- stat_allowed = FALSE
- school = SCHOOL_EVOCATION
- clothes_req = FALSE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi'
- action_icon = 'icons/mob/actions/actions_cult.dmi'
- action_background_icon_state = "bg_demon"
- action_icon_state = "dominate"
- active_msg = "You prepare to dominate the mind of a target..."
-
-/obj/effect/proc_holder/spell/pointed/dominate/cast(list/targets, mob/user)
- if(!LAZYLEN(targets))
- to_chat(user, span_notice("No target found in range."))
- return FALSE
- if(!can_target(targets[1], user))
- return FALSE
-
- var/mob/living/simple_animal/S = targets[1]
- S.add_atom_colour("#990000", FIXED_COLOUR_PRIORITY)
- S.faction = list("cult")
- playsound(get_turf(S), 'sound/effects/ghost.ogg', 100, TRUE)
- new /obj/effect/temp_visual/cult/sac(get_turf(S))
-
-/obj/effect/proc_holder/spell/pointed/dominate/can_target(atom/target, mob/user, silent)
- . = ..()
- if(!.)
- return FALSE
- if(!isanimal(target))
- if(!silent)
- to_chat(user, span_warning("Target is not a lesser creature!"))
- return FALSE
-
- var/mob/living/simple_animal/S = target
- if(S.mind)
- if(!silent)
- to_chat(user, span_warning("[S] is too intelligent to dominate!"))
- return FALSE
- if(S.stat)
- if(!silent)
- to_chat(user, span_warning("[S] is dead!"))
- return FALSE
- if(S.sentience_type != SENTIENCE_ORGANIC)
- if(!silent)
- to_chat(user, span_warning("[S] cannot be dominated!"))
- return FALSE
- if("cult" in S.faction)
- if(!silent)
- to_chat(user, span_warning("[S] is already serving Nar'Sie!"))
- return FALSE
- return TRUE
-
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/shift/golem
- charge_max = 80 SECONDS
- jaunt_in_type = /obj/effect/temp_visual/dir_setting/cult/phase
- jaunt_out_type = /obj/effect/temp_visual/dir_setting/cult/phase/out
-
-/obj/effect/proc_holder/spell/targeted/projectile/dumbfire/juggernaut
- name = "Gauntlet Echo"
- desc = "Channels energy into your gauntlet - firing its essence forward in a slow moving, yet devastating, attack."
- proj_type = /obj/projectile/magic/spell/juggernaut
- charge_max = 35 SECONDS
- clothes_req = FALSE
- action_icon = 'icons/mob/actions/actions_cult.dmi'
- action_icon_state = "cultfist"
- action_background_icon_state = "bg_demon"
- sound = 'sound/weapons/resonator_blast.ogg'
-
-/obj/projectile/magic/spell/juggernaut
- name = "Gauntlet Echo"
- icon_state = "cultfist"
- alpha = 180
- damage = 30
- damage_type = BRUTE
- knockdown = 50
- hitsound = 'sound/weapons/punch3.ogg'
- trigger_range = 0
- antimagic_flags = MAGIC_RESISTANCE_HOLY
- ignored_factions = list("cult")
- range = 15
- speed = 7
-
-/obj/projectile/magic/spell/juggernaut/on_hit(atom/target, blocked)
- . = ..()
- var/turf/T = get_turf(src)
- playsound(T, 'sound/weapons/resonator_blast.ogg', 100, FALSE)
- new /obj/effect/temp_visual/cult/sac(T)
- for(var/obj/O in range(src,1))
- if(O.density && !istype(O, /obj/structure/destructible/cult))
- O.take_damage(90, BRUTE, MELEE, 0)
- new /obj/effect/temp_visual/cult/turf/floor(get_turf(O))
diff --git a/code/modules/spells/spell_types/emplosion.dm b/code/modules/spells/spell_types/emplosion.dm
deleted file mode 100644
index 54aa74871e4aa..0000000000000
--- a/code/modules/spells/spell_types/emplosion.dm
+++ /dev/null
@@ -1,19 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/emplosion
- name = "Emplosion"
- desc = "This spell emplodes an area."
-
- school = SCHOOL_EVOCATION
- var/emp_heavy = 2
- var/emp_light = 3
-
- action_icon_state = "emp"
- sound = 'sound/weapons/zapbang.ogg'
-
-/obj/effect/proc_holder/spell/targeted/emplosion/cast(list/targets, mob/user = usr)
- playsound(get_turf(user), sound, 50,TRUE)
- for(var/mob/living/target in targets)
- if(target.can_block_magic())
- continue
- empulse(target.loc, emp_heavy, emp_light)
-
- return
diff --git a/code/modules/spells/spell_types/ethereal_jaunt.dm b/code/modules/spells/spell_types/ethereal_jaunt.dm
deleted file mode 100644
index 27bfba57def39..0000000000000
--- a/code/modules/spells/spell_types/ethereal_jaunt.dm
+++ /dev/null
@@ -1,138 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt
- name = "Ethereal Jaunt"
- desc = "This spell turns your form ethereal, temporarily making you invisible and able to pass through walls."
-
- school = SCHOOL_TRANSMUTATION
- charge_max = 30 SECONDS
- clothes_req = TRUE
- invocation = "none"
- invocation_type = INVOCATION_NONE
- range = -1
- cooldown_min = 10 SECONDS
- include_user = TRUE
- nonabstract_req = TRUE
- action_icon_state = "jaunt"
- /// For how long are we jaunting?
- var/jaunt_duration = 5 SECONDS
- /// For how long we become immobilized after exiting the jaunt.
- var/jaunt_in_time = 0.5 SECONDS
- /// For how long we become immobilized when using this spell.
- var/jaunt_out_time = 0 SECONDS
- /// Visual for jaunting
- var/jaunt_in_type = /obj/effect/temp_visual/wizard
- /// Visual for exiting the jaunt
- var/jaunt_out_type = /obj/effect/temp_visual/wizard/out
- /// List of valid exit points
- var/list/exit_point_list
-
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/cast_check(skipcharge = 0,mob/user = usr)
- . = ..()
- if(!.)
- return FALSE
- var/area/noteleport_check = get_area(user)
- if(noteleport_check && noteleport_check.area_flags & NOTELEPORT)
- to_chat(user, span_danger("Some dull, universal force is stopping you from jaunting here."))
- return FALSE
-
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/cast(list/targets,mob/user = usr) //magnets, so mostly hardcoded
- play_sound("enter",user)
- for(var/mob/living/target in targets)
- INVOKE_ASYNC(src, .proc/do_jaunt, target)
-
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/proc/do_jaunt(mob/living/target)
- target.notransform = 1
- var/turf/mobloc = get_turf(target)
- var/obj/effect/dummy/phased_mob/spell_jaunt/holder = new /obj/effect/dummy/phased_mob/spell_jaunt(mobloc)
- new jaunt_out_type(mobloc, target.dir)
- target.extinguish_mob()
- target.forceMove(holder)
- target.reset_perspective(holder)
- target.notransform=0 //mob is safely inside holder now, no need for protection.
- jaunt_steam(mobloc)
- if(jaunt_out_time)
- ADD_TRAIT(target, TRAIT_IMMOBILIZED, type)
- sleep(jaunt_out_time)
- REMOVE_TRAIT(target, TRAIT_IMMOBILIZED, type)
- var/turf/exit_point = get_turf(holder) //Hopefully this gets updated, otherwise this is our fallback
- LAZYINITLIST(exit_point_list)
- RegisterSignal(holder, COMSIG_MOVABLE_MOVED, .proc/update_exit_point, target)
- sleep(jaunt_duration)
-
- UnregisterSignal(holder, COMSIG_MOVABLE_MOVED)
- if(target.loc != holder) //mob warped out of the warp
- qdel(holder)
- return
-
- var/found_exit = FALSE
- for(var/turf/possible_exit as anything in exit_point_list)
- if(possible_exit.is_blocked_turf_ignore_climbable())
- continue
- exit_point = possible_exit
- found_exit = TRUE
- break
- if(!found_exit)
- to_chat(target, span_danger("Unable to find an unobstructed space, you find yourself ripped back to where you started."))
- exit_point_list.Cut()
- holder.forceMove(exit_point)
-
- mobloc = get_turf(target.loc)
- jaunt_steam(mobloc)
- ADD_TRAIT(target, TRAIT_IMMOBILIZED, type)
- holder.reappearing = 1
- play_sound("exit",target)
- sleep(25 - jaunt_in_time)
- new jaunt_in_type(mobloc, holder.dir)
- target.setDir(holder.dir)
- sleep(jaunt_in_time)
- qdel(holder)
- if(!QDELETED(target))
- if(mobloc.density)
- for(var/direction in GLOB.alldirs)
- var/turf/T = get_step(mobloc, direction)
- if(T)
- if(target.Move(T))
- break
- REMOVE_TRAIT(target, TRAIT_IMMOBILIZED, type)
-
-/**
- * Updates the exit point of the jaunt
- *
- * Called when the jaunting mob holder moves, this updates the backup exit-jaunt
- * location, in case the jaunt ends with the mob still in a wall. Five
- * spots are kept in the list, in case the last few changed since we passed
- * by (doors closing, engineers building walls, etc)
- */
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/proc/update_exit_point(mob/living/target)
- SIGNAL_HANDLER
- var/turf/location = get_turf(target)
- if(location.is_blocked_turf_ignore_climbable())
- return
- exit_point_list.Insert(1, location)
- if(length(exit_point_list) >= 5)
- exit_point_list.Cut(5)
-
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/proc/jaunt_steam(mobloc)
- var/datum/effect_system/steam_spread/steam = new /datum/effect_system/steam_spread()
- steam.set_up(10, 0, mobloc)
- steam.start()
-
-/obj/effect/proc_holder/spell/targeted/ethereal_jaunt/proc/play_sound(type,mob/living/target)
- switch(type)
- if("enter")
- playsound(get_turf(target), 'sound/magic/ethereal_enter.ogg', 50, TRUE, -1)
- if("exit")
- playsound(get_turf(target), 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
-
-/obj/effect/dummy/phased_mob/spell_jaunt
- movespeed = 2 //quite slow.
- var/reappearing = FALSE
-
-/obj/effect/dummy/phased_mob/spell_jaunt/phased_check(mob/living/user, direction)
- if(reappearing)
- return
- . = ..()
- if(!.)
- return
- if (locate(/obj/effect/blessing, .))
- to_chat(user, span_warning("Holy energies block your path!"))
- return null
diff --git a/code/modules/spells/spell_types/explosion.dm b/code/modules/spells/spell_types/explosion.dm
deleted file mode 100644
index 0a497290f3390..0000000000000
--- a/code/modules/spells/spell_types/explosion.dm
+++ /dev/null
@@ -1,22 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/explosion
- name = "Explosion"
- desc = "This spell explodes an area."
-
- school = SCHOOL_EVOCATION
-
- /// The devastation range of the resulting explosion.
- var/ex_severe = 1
- /// The heavy impact range of the resulting explosion.
- var/ex_heavy = 2
- /// The light impact range of the resulting explosion.
- var/ex_light = 3
- /// The flash range of the resulting explosion.
- var/ex_flash = 4
-
-/obj/effect/proc_holder/spell/targeted/explosion/cast(list/targets,mob/user = usr)
- for(var/mob/living/target in targets)
- if(target.can_block_magic())
- continue
- explosion(target, devastation_range = ex_severe, heavy_impact_range = ex_heavy, light_impact_range = ex_light, flash_range = ex_flash, explosion_cause = src)
-
- return
diff --git a/code/modules/spells/spell_types/forcewall.dm b/code/modules/spells/spell_types/forcewall.dm
deleted file mode 100644
index 43979fe80bf2b..0000000000000
--- a/code/modules/spells/spell_types/forcewall.dm
+++ /dev/null
@@ -1,40 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/forcewall
- name = "Forcewall"
- desc = "Create a magical barrier that only you can pass through."
- school = SCHOOL_TRANSMUTATION
- charge_max = 100
- clothes_req = FALSE
- invocation = "TARCOL MINTI ZHERI"
- invocation_type = INVOCATION_SHOUT
- sound = 'sound/magic/forcewall.ogg'
- action_icon_state = "shield"
- range = -1
- include_user = TRUE
- cooldown_min = 50 //12 deciseconds reduction per rank
- var/wall_type = /obj/effect/forcefield/wizard
-
-/obj/effect/proc_holder/spell/targeted/forcewall/cast(list/targets,mob/user = usr)
- new wall_type(get_turf(user),user)
- if(user.dir == SOUTH || user.dir == NORTH)
- new wall_type(get_step(user, EAST),user)
- new wall_type(get_step(user, WEST),user)
- else
- new wall_type(get_step(user, NORTH),user)
- new wall_type(get_step(user, SOUTH),user)
-
-
-/obj/effect/forcefield/wizard
- var/mob/wizard
-
-/obj/effect/forcefield/wizard/Initialize(mapload, mob/summoner)
- . = ..()
- wizard = summoner
-
-/obj/effect/forcefield/wizard/CanAllowThrough(atom/movable/mover, border_dir)
- . = ..()
- if(mover == wizard)
- return TRUE
- if(isliving(mover))
- var/mob/M = mover
- if(M.can_block_magic(charge_cost = 0))
- return TRUE
diff --git a/code/modules/spells/spell_types/genetic.dm b/code/modules/spells/spell_types/genetic.dm
deleted file mode 100644
index d6912753b7de9..0000000000000
--- a/code/modules/spells/spell_types/genetic.dm
+++ /dev/null
@@ -1,48 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/genetic
- name = "Genetic"
- desc = "This spell inflicts a set of mutations and disabilities upon the target."
-
- school = SCHOOL_TRANSMUTATION
-
- var/list/active_on = list()
- var/list/traits = list() //disabilities
- var/list/mutations = list() //mutation defines
- var/duration = 100 //deciseconds
- /*
- Disabilities
- 1st bit - ?
- 2nd bit - ?
- 3rd bit - ?
- 4th bit - ?
- 5th bit - ?
- 6th bit - ?
- */
-
-/obj/effect/proc_holder/spell/targeted/genetic/cast(list/targets,mob/user = usr)
- playMagSound()
- for(var/mob/living/carbon/target in targets)
- if(target.can_block_magic())
- to_chat(user, span_warning("The spell had no effect on [target]!"))
- continue
- if(!target.dna)
- continue
- for(var/A in mutations)
- target.dna.add_mutation(A)
- for(var/A in traits)
- ADD_TRAIT(target, A, GENETICS_SPELL)
- active_on += target
- if(duration < charge_max)
- addtimer(CALLBACK(src, .proc/remove, target), duration, TIMER_OVERRIDE|TIMER_UNIQUE)
-
-/obj/effect/proc_holder/spell/targeted/genetic/Destroy()
- . = ..()
- for(var/V in active_on)
- remove(V)
-
-/obj/effect/proc_holder/spell/targeted/genetic/proc/remove(mob/living/carbon/target)
- active_on -= target
- if(!QDELETED(target))
- for(var/A in mutations)
- target.dna.remove_mutation(A)
- for(var/A in traits)
- REMOVE_TRAIT(target, A, GENETICS_SPELL)
diff --git a/code/modules/spells/spell_types/godhand.dm b/code/modules/spells/spell_types/godhand.dm
deleted file mode 100644
index 93e3d632a8c22..0000000000000
--- a/code/modules/spells/spell_types/godhand.dm
+++ /dev/null
@@ -1,171 +0,0 @@
-/obj/item/melee/touch_attack
- name = "\improper outstretched hand"
- desc = "High Five?"
- var/catchphrase = "High Five!"
- var/on_use_sound = null
- var/obj/effect/proc_holder/spell/targeted/touch/attached_spell
- icon = 'icons/obj/items_and_weapons.dmi'
- lefthand_file = 'icons/mob/inhands/misc/touchspell_lefthand.dmi'
- righthand_file = 'icons/mob/inhands/misc/touchspell_righthand.dmi'
- icon_state = "latexballon"
- inhand_icon_state = null
- item_flags = NEEDS_PERMIT | ABSTRACT | DROPDEL
- w_class = WEIGHT_CLASS_HUGE
- force = 0
- throwforce = 0
- throw_range = 0
- throw_speed = 0
- var/charges = 1
-
-/obj/item/melee/touch_attack/Initialize(mapload)
- . = ..()
- ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT)
-
-/obj/item/melee/touch_attack/attack(mob/target, mob/living/carbon/user)
- if(!iscarbon(user)) //Look ma, no hands
- return
- if(!(user.mobility_flags & MOBILITY_USE))
- to_chat(user, span_warning("You can't reach out!"))
- return
- ..()
-
-/obj/item/melee/touch_attack/afterattack(atom/target, mob/user, proximity)
- . = ..()
- if(!proximity)
- return
- if(charges > 0)
- use_charge(user)
-
-/obj/item/melee/touch_attack/proc/use_charge(mob/living/user, whisper = FALSE)
- if(QDELETED(src))
- return
-
- if(catchphrase)
- if(whisper)
- user.say("#[catchphrase]", forced = "spell")
- else
- user.say(catchphrase, forced = "spell")
- playsound(get_turf(user), on_use_sound, 50, TRUE)
- if(--charges <= 0)
- qdel(src)
-
-/obj/item/melee/touch_attack/Destroy()
- if(attached_spell)
- attached_spell.on_hand_destroy(src)
- return ..()
-
-/obj/item/melee/touch_attack/disintegrate
- name = "\improper smiting touch"
- desc = "This hand of mine glows with an awesome power!"
- catchphrase = "EI NATH!!"
- on_use_sound = 'sound/magic/disintegrate.ogg'
- icon_state = "disintegrate"
- inhand_icon_state = "disintegrate"
-
-/obj/item/melee/touch_attack/disintegrate/afterattack(mob/living/target, mob/living/carbon/user, proximity)
- if(!proximity || target == user || !istype(target) || !iscarbon(user) || !(user.mobility_flags & MOBILITY_USE)) //exploding after touching yourself would be bad
- return
- if(!user.can_speak_vocal())
- to_chat(user, span_warning("You can't get the words out!"))
- return
- do_sparks(4, FALSE, target.loc)
- for(var/mob/living/L in view(src, 7))
- if(L != user)
- L.flash_act(affect_silicon = FALSE)
- if(target.can_block_magic())
- user.visible_message(span_warning("The feedback blows [user]'s arm off!"), \
- span_userdanger("The spell bounces from [target]'s skin back into your arm!"))
- user.flash_act()
- var/obj/item/bodypart/part = user.get_holding_bodypart_of_item(src)
- if(part)
- part.dismember()
- return ..()
- var/obj/item/clothing/suit/hooded/bloated_human/suit = target.get_item_by_slot(ITEM_SLOT_OCLOTHING)
- if(istype(suit))
- target.visible_message(span_danger("[target]'s [suit] explodes off of them into a puddle of gore!"))
- target.dropItemToGround(suit)
- qdel(suit)
- new /obj/effect/gibspawner(target.loc)
- return ..()
- target.gib()
- return ..()
-
-/obj/item/melee/touch_attack/fleshtostone
- name = "\improper petrifying touch"
- desc = "That's the bottom line, because flesh to stone said so!"
- catchphrase = "STAUN EI!!"
- on_use_sound = 'sound/magic/fleshtostone.ogg'
- icon_state = "fleshtostone"
- inhand_icon_state = "fleshtostone"
-
-/obj/item/melee/touch_attack/fleshtostone/afterattack(mob/living/target, mob/living/carbon/user, proximity)
- if(!proximity || target == user || !isliving(target) || !iscarbon(user)) //getting hard after touching yourself would also be bad
- return
- if(!(user.mobility_flags & MOBILITY_USE))
- to_chat(user, span_warning("You can't reach out!"))
- return
- if(!user.can_speak_vocal())
- to_chat(user, span_warning("You can't get the words out!"))
- return
- if(target.can_block_magic())
- to_chat(user, span_warning("The spell can't seem to affect [target]!"))
- to_chat(target, span_warning("You feel your flesh turn to stone for a moment, then revert back!"))
- return ..()
- target.Stun(40)
- target.petrify()
- return ..()
-
-
-/obj/item/melee/touch_attack/duffelbag
- name = "\improper burdening touch"
- desc = "Where is the bar from here?"
- catchphrase = "HU'SWCH H'ANS!!"
- on_use_sound = 'sound/magic/mm_hit.ogg'
- icon_state = "duffelcurse"
- inhand_icon_state = "duffelcurse"
-
-/obj/item/melee/touch_attack/duffelbag/afterattack(atom/target, mob/living/carbon/user, proximity)
- if(!proximity || target == user || !isliving(target) || !iscarbon(user)) //Roleplay involving touching is equally as bad
- return
- if(!(user.mobility_flags & MOBILITY_USE))
- to_chat(user, span_warning("You can't reach out!"))
- return
- if(!user.can_speak_vocal())
- to_chat(user, span_warning("You can't get the words out!"))
- return
- var/mob/living/carbon/duffelvictim = target
- var/elaborate_backstory = pick("spacewar origin story", "military background", "corporate connections", "life in the colonies", "anti-government activities", "upbringing on the space farm", "fond memories with your buddy Keith")
- if(duffelvictim.can_block_magic())
- to_chat(user, span_warning("The spell can't seem to affect [duffelvictim]!"))
- to_chat(duffelvictim, span_warning("You really don't feel like talking about your [elaborate_backstory] with complete strangers today."))
- return ..()
-
- duffelvictim.flash_act()
- duffelvictim.Immobilize(5 SECONDS)
- duffelvictim.apply_damage(80, STAMINA)
- duffelvictim.Knockdown(5 SECONDS)
-
- if(HAS_TRAIT(target, TRAIT_DUFFEL_CURSE_PROOF))
- to_chat(user, span_warning("The burden of [duffelvictim]'s duffel bag becomes too much, shoving them to the floor!"))
- to_chat(duffelvictim, span_warning("The weight of this bag becomes overburdening!"))
- return ..()
-
- var/obj/item/storage/backpack/duffelbag/cursed/conjuredduffel = new get_turf(target)
-
- duffelvictim.visible_message(span_danger("A growling duffel bag appears on [duffelvictim]!"), \
- span_danger("You feel something attaching itself to you, and a strong desire to discuss your [elaborate_backstory] at length!"))
-
- ADD_TRAIT(duffelvictim, TRAIT_DUFFEL_CURSE_PROOF, CURSED_ITEM_TRAIT(conjuredduffel.name))
- conjuredduffel.pickup(duffelvictim)
- conjuredduffel.forceMove(duffelvictim)
- if(duffelvictim.dropItemToGround(duffelvictim.back))
- duffelvictim.equip_to_slot_if_possible(conjuredduffel, ITEM_SLOT_BACK, TRUE, TRUE)
- else
- if(!duffelvictim.put_in_hands(conjuredduffel))
- duffelvictim.dropItemToGround(duffelvictim.get_inactive_held_item())
- if(!duffelvictim.put_in_hands(conjuredduffel))
- duffelvictim.dropItemToGround(duffelvictim.get_active_held_item())
- duffelvictim.put_in_hands(conjuredduffel)
- else
- return ..()
- return ..()
diff --git a/code/modules/spells/spell_types/infinite_guns.dm b/code/modules/spells/spell_types/infinite_guns.dm
deleted file mode 100644
index 3817e04198dc4..0000000000000
--- a/code/modules/spells/spell_types/infinite_guns.dm
+++ /dev/null
@@ -1,27 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/infinite_guns
- name = "Lesser Summon Guns"
- desc = "Why reload when you have infinite guns? Summons an unending stream of bolt action rifles that deal little damage, but will knock targets down. Requires both hands free to use. Learning this spell makes you unable to learn Arcane Barrage."
- invocation_type = INVOCATION_NONE
- include_user = TRUE
- range = -1
-
- school = SCHOOL_CONJURATION
- charge_max = 750
- clothes_req = TRUE
- cooldown_min = 10 //Gun wizard
- action_icon_state = "bolt_action"
- var/summon_path = /obj/item/gun/ballistic/rifle/enchanted
-
-/obj/effect/proc_holder/spell/targeted/infinite_guns/cast(list/targets, mob/user = usr)
- for(var/mob/living/carbon/C in targets)
- C.drop_all_held_items()
- var/GUN = new summon_path
- C.put_in_hands(GUN)
-
-/obj/effect/proc_holder/spell/targeted/infinite_guns/gun
-
-/obj/effect/proc_holder/spell/targeted/infinite_guns/arcane_barrage
- name = "Arcane Barrage"
- desc = "Fire a torrent of arcane energy at your foes with this (powerful) spell. Deals much more damage than Lesser Summon Guns, but won't knock targets down. Requires both hands free to use. Learning this spell makes you unable to learn Lesser Summon Gun."
- action_icon_state = "arcane_barrage"
- summon_path = /obj/item/gun/ballistic/rifle/enchanted/arcane_barrage
diff --git a/code/modules/spells/spell_types/inflict_handler.dm b/code/modules/spells/spell_types/inflict_handler.dm
deleted file mode 100644
index b717009de936c..0000000000000
--- a/code/modules/spells/spell_types/inflict_handler.dm
+++ /dev/null
@@ -1,54 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/inflict_handler
- name = "Inflict Handler"
- desc = "This spell blinds and/or destroys/damages/heals and/or knockdowns/stuns the target."
- school = SCHOOL_EVOCATION
- antimagic_flags = MAGIC_RESISTANCE
- var/amt_paralyze = 0
- var/amt_unconscious = 0
- var/amt_stun = 0
- var/inflict_status
- var/list/status_params = list()
- //set to negatives for healing
- var/amt_dam_fire = 0
- var/amt_dam_brute = 0
- var/amt_dam_oxy = 0
- var/amt_dam_tox = 0
- var/amt_eye_blind = 0
- var/amt_eye_blurry = 0
- var/destroys = "none" //can be "none", "gib" or "disintegrate"
- var/summon_type = null //this will put an obj at the target's location
-
-/obj/effect/proc_holder/spell/targeted/inflict_handler/cast(list/targets,mob/user = usr)
- for(var/mob/living/target in targets)
- playsound(target,sound, 50,TRUE)
- if(target.can_block_magic(antimagic_flags))
- to_chat(user, span_warning("The spell had no effect on [target]!"))
- return
- switch(destroys)
- if("gib")
- target.gib()
- if("disintegrate")
- target.dust()
-
- if(!target)
- continue
- //damage/healing
- target.adjustBruteLoss(amt_dam_brute)
- target.adjustFireLoss(amt_dam_fire)
- target.adjustToxLoss(amt_dam_tox)
- target.adjustOxyLoss(amt_dam_oxy)
- //disabling
- target.Paralyze(amt_paralyze)
- target.Unconscious(amt_unconscious)
- target.Stun(amt_stun)
-
- target.blind_eyes(amt_eye_blind)
- target.blur_eyes(amt_eye_blurry)
- //summoning
- if(summon_type)
- new summon_type(target.loc, target)
-
- if(inflict_status)
- var/list/stat_args = status_params.Copy()
- stat_args.Insert(1,inflict_status)
- target.apply_status_effect(arglist(stat_args))
diff --git a/code/modules/spells/spell_types/jaunt/_jaunt.dm b/code/modules/spells/spell_types/jaunt/_jaunt.dm
new file mode 100644
index 0000000000000..af311947366b6
--- /dev/null
+++ b/code/modules/spells/spell_types/jaunt/_jaunt.dm
@@ -0,0 +1,92 @@
+/**
+ * ## Jaunt spells
+ *
+ * A basic subtype for jaunt related spells.
+ * Jaunt spells put their caster in a dummy
+ * phased_mob effect that allows them to float
+ * around incorporeally.
+ *
+ * Doesn't actually implement any behavior on cast to
+ * enter or exit the jaunt - that must be done via subtypes.
+ *
+ * Use enter_jaunt() and exit_jaunt() as wrappers.
+ */
+/datum/action/cooldown/spell/jaunt
+ school = SCHOOL_TRANSMUTATION
+
+ invocation_type = INVOCATION_NONE
+
+ /// What dummy mob type do we put jaunters in on jaunt?
+ var/jaunt_type = /obj/effect/dummy/phased_mob
+
+/datum/action/cooldown/spell/jaunt/can_cast_spell(feedback = TRUE)
+ . = ..()
+ if(!.)
+ return FALSE
+ var/area/owner_area = get_area(owner)
+ var/turf/owner_turf = get_turf(owner)
+ if(!owner_area || !owner_turf)
+ return FALSE // nullspaced?
+
+ if(owner_area.area_flags & NOTELEPORT)
+ if(feedback)
+ to_chat(owner, span_danger("Some dull, universal force is stopping you from jaunting here."))
+ return FALSE
+
+ if(owner_turf?.turf_flags & NOJAUNT)
+ if(feedback)
+ to_chat(owner, span_danger("An otherwordly force is preventing you from jaunting here."))
+ return FALSE
+
+ return isliving(owner)
+
+
+/**
+ * Places the [jaunter] in a jaunt holder mob
+ * If [loc_override] is supplied,
+ * the jaunt will be moved to that turf to start at
+ *
+ * Returns the holder mob that was created
+ */
+/datum/action/cooldown/spell/jaunt/proc/enter_jaunt(mob/living/jaunter, turf/loc_override)
+ var/obj/effect/dummy/phased_mob/jaunt = new jaunt_type(loc_override || get_turf(jaunter), jaunter)
+ spell_requirements |= SPELL_CASTABLE_WHILE_PHASED
+ ADD_TRAIT(jaunter, TRAIT_MAGICALLY_PHASED, REF(src))
+
+ // This needs to happen at the end, after all the traits and stuff is handled
+ SEND_SIGNAL(jaunter, COMSIG_MOB_ENTER_JAUNT, src, jaunt)
+ return jaunt
+
+/**
+ * Ejects the [unjaunter] from jaunt
+ * If [loc_override] is supplied,
+ * the jaunt will be moved to that turf
+ * before ejecting the unjaunter
+ *
+ * Returns TRUE on successful exit, FALSE otherwise
+ */
+/datum/action/cooldown/spell/jaunt/proc/exit_jaunt(mob/living/unjaunter, turf/loc_override)
+ var/obj/effect/dummy/phased_mob/jaunt = unjaunter.loc
+ if(!istype(jaunt))
+ return FALSE
+
+ if(jaunt.jaunter != unjaunter)
+ CRASH("Jaunt spell attempted to exit_jaunt with an invalid unjaunter, somehow.")
+
+ if(loc_override)
+ jaunt.forceMove(loc_override)
+ jaunt.eject_jaunter()
+ spell_requirements &= ~SPELL_CASTABLE_WHILE_PHASED
+ REMOVE_TRAIT(unjaunter, TRAIT_MAGICALLY_PHASED, REF(src))
+
+ // Ditto - this needs to happen at the end, after all the traits and stuff is handled
+ SEND_SIGNAL(unjaunter, COMSIG_MOB_AFTER_EXIT_JAUNT, src)
+ return TRUE
+
+/// Simple helper to check if the passed mob is currently jaunting or not
+/datum/action/cooldown/spell/jaunt/proc/is_jaunting(mob/living/user)
+ return istype(user.loc, /obj/effect/dummy/phased_mob)
+
+/datum/action/cooldown/spell/jaunt/Remove(mob/living/remove_from)
+ exit_jaunt(remove_from)
+ return ..()
diff --git a/code/modules/spells/spell_types/jaunt/bloodcrawl.dm b/code/modules/spells/spell_types/jaunt/bloodcrawl.dm
new file mode 100644
index 0000000000000..365234c649a54
--- /dev/null
+++ b/code/modules/spells/spell_types/jaunt/bloodcrawl.dm
@@ -0,0 +1,315 @@
+/**
+ * ### Blood Crawl
+ *
+ * Lets the caster enter and exit pools of blood.
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl
+ name = "Blood Crawl"
+ desc = "Allows you to phase in and out of existance via pools of blood."
+ background_icon_state = "bg_demon"
+ icon_icon = 'icons/mob/actions/actions_minor_antag.dmi'
+ button_icon_state = "bloodcrawl"
+
+ spell_requirements = NONE
+
+ /// The time it takes to enter blood
+ var/enter_blood_time = 0 SECONDS
+ /// The time it takes to exit blood
+ var/exit_blood_time = 2 SECONDS
+ /// The radius around us that we look for blood in
+ var/blood_radius = 1
+ /// If TRUE, we equip "blood crawl" hands to the jaunter to prevent using items
+ var/equip_blood_hands = TRUE
+
+/datum/action/cooldown/spell/jaunt/bloodcrawl/cast(mob/living/cast_on)
+ . = ..()
+ for(var/obj/effect/decal/cleanable/blood_nearby in range(blood_radius, get_turf(cast_on)))
+ if(blood_nearby.can_bloodcrawl_in())
+ return do_bloodcrawl(blood_nearby, cast_on)
+
+ reset_spell_cooldown()
+ to_chat(cast_on, span_warning("There must be a nearby source of blood!"))
+
+/**
+ * Attempts to enter or exit the passed blood pool.
+ * Returns TRUE if we successfully entered or exited said pool, FALSE otherwise
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/proc/do_bloodcrawl(obj/effect/decal/cleanable/blood, mob/living/jaunter)
+ if(is_jaunting(jaunter))
+ . = try_exit_jaunt(blood, jaunter)
+ else
+ . = try_enter_jaunt(blood, jaunter)
+
+ if(!.)
+ reset_spell_cooldown()
+ to_chat(jaunter, span_warning("You are unable to blood crawl!"))
+
+/**
+ * Attempts to enter the passed blood pool.
+ * If forced is TRUE, it will override enter_blood_time.
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/proc/try_enter_jaunt(obj/effect/decal/cleanable/blood, mob/living/jaunter, forced = FALSE)
+ if(!forced)
+ if(enter_blood_time > 0 SECONDS)
+ blood.visible_message(span_warning("[jaunter] starts to sink into [blood]!"))
+ if(!do_after(jaunter, enter_blood_time, target = blood))
+ return FALSE
+
+ // The actual turf we enter
+ var/turf/jaunt_turf = get_turf(blood)
+
+ // Begin the jaunt
+ jaunter.notransform = TRUE
+ var/obj/effect/dummy/phased_mob/holder = enter_jaunt(jaunter, jaunt_turf)
+ if(!holder)
+ jaunter.notransform = FALSE
+ return FALSE
+
+ if(equip_blood_hands && iscarbon(jaunter))
+ jaunter.drop_all_held_items()
+ // Give them some bloody hands to prevent them from doing things
+ var/obj/item/bloodcrawl/left_hand = new(jaunter)
+ var/obj/item/bloodcrawl/right_hand = new(jaunter)
+ left_hand.icon_state = "bloodhand_right" // Icons swapped intentionally..
+ right_hand.icon_state = "bloodhand_left" // ..because perspective, or something
+ jaunter.put_in_hands(left_hand)
+ jaunter.put_in_hands(right_hand)
+
+ blood.visible_message(span_warning("[jaunter] sinks into [blood]!"))
+ playsound(jaunt_turf, 'sound/magic/enter_blood.ogg', 50, TRUE, -1)
+ jaunter.extinguish_mob()
+
+ jaunter.notransform = FALSE
+ return TRUE
+
+/**
+ * Attempts to Exit the passed blood pool.
+ * If forced is TRUE, it will override exit_blood_time, and if we're currently consuming someone.
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/proc/try_exit_jaunt(obj/effect/decal/cleanable/blood, mob/living/jaunter, forced = FALSE)
+ if(!forced)
+ if(jaunter.notransform)
+ to_chat(jaunter, span_warning("You cannot exit yet!!"))
+ return FALSE
+
+ if(exit_blood_time > 0 SECONDS)
+ blood.visible_message(span_warning("[blood] starts to bubble..."))
+ if(!do_after(jaunter, exit_blood_time, target = blood))
+ return FALSE
+
+ if(!exit_jaunt(jaunter, get_turf(blood)))
+ return FALSE
+
+ if(equip_blood_hands && iscarbon(jaunter))
+ for(var/obj/item/bloodcrawl/blood_hand in jaunter.held_items)
+ jaunter.temporarilyRemoveItemFromInventory(blood_hand, force = TRUE)
+ qdel(blood_hand)
+
+ blood.visible_message(span_boldwarning("[jaunter] rises out of [blood]!"))
+ return TRUE
+
+/datum/action/cooldown/spell/jaunt/bloodcrawl/exit_jaunt(mob/living/unjaunter, turf/loc_override)
+ . = ..()
+ if(!.)
+ return
+
+ exit_blood_effect(unjaunter)
+
+/// Adds an coloring effect to mobs which exit blood crawl.
+/datum/action/cooldown/spell/jaunt/bloodcrawl/proc/exit_blood_effect(mob/living/exited)
+ var/turf/landing_turf = get_turf(exited)
+ playsound(landing_turf, 'sound/magic/exit_blood.ogg', 50, TRUE, -1)
+
+ // Make the mob have the color of the blood pool it came out of
+ var/obj/effect/decal/cleanable/came_from = locate() in landing_turf
+ var/new_color = came_from?.get_blood_color()
+ if(!new_color)
+ return
+
+ exited.add_atom_colour(new_color, TEMPORARY_COLOUR_PRIORITY)
+ // ...but only for a few seconds
+ addtimer(CALLBACK(exited, /atom/.proc/remove_atom_colour, TEMPORARY_COLOUR_PRIORITY, new_color), 6 SECONDS)
+
+/**
+ * Slaughter demon's blood crawl
+ * Allows the blood crawler to consume people they are dragging.
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon
+ name = "Voracious Blood Crawl"
+ desc = "Allows you to phase in and out of existance via pools of blood. If you are dragging someone in critical or dead, \
+ they will be consumed by you, fully healing you."
+ /// The sound played when someone's consumed.
+ var/consume_sound = 'sound/magic/demon_consume.ogg'
+
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/try_enter_jaunt(obj/effect/decal/cleanable/blood, mob/living/jaunter)
+ // Save this before the actual jaunt
+ var/atom/coming_with = jaunter.pulling
+
+ // Does the actual jaunt
+ . = ..()
+ if(!.)
+ return
+
+ var/turf/jaunt_turf = get_turf(jaunter)
+ // if we're not pulling anyone, or we can't what we're pulling
+ if(!isliving(coming_with))
+ return
+
+ var/mob/living/victim = coming_with
+
+ if(victim.stat == CONSCIOUS)
+ jaunt_turf.visible_message(
+ span_warning("[victim] kicks free of [blood] just before entering it!"),
+ blind_message = span_notice("You hear splashing and struggling."),
+ )
+ return FALSE
+
+ if(SEND_SIGNAL(victim, COMSIG_LIVING_BLOOD_CRAWL_PRE_CONSUMED, src, jaunter, blood) & COMPONENT_STOP_CONSUMPTION)
+ return FALSE
+
+ victim.forceMove(jaunter)
+ victim.emote("scream")
+ jaunt_turf.visible_message(
+ span_boldwarning("[jaunter] drags [victim] into [blood]!"),
+ blind_message = span_notice("You hear a splash."),
+ )
+
+ jaunter.notransform = TRUE
+ consume_victim(victim, jaunter)
+ jaunter.notransform = FALSE
+
+ return TRUE
+
+/**
+ * Consumes the [victim] from the [jaunter], fully healing them
+ * and calling [proc/on_victim_consumed] if successful.
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/proc/consume_victim(mob/living/victim, mob/living/jaunter)
+ on_victim_start_consume(victim, jaunter)
+
+ for(var/i in 1 to 3)
+ playsound(get_turf(jaunter), consume_sound, 50, TRUE)
+ if(!do_after(jaunter, 3 SECONDS, victim))
+ to_chat(jaunter, span_danger("You lose your victim!"))
+ return FALSE
+ if(QDELETED(src))
+ return FALSE
+
+ if(SEND_SIGNAL(victim, COMSIG_LIVING_BLOOD_CRAWL_CONSUMED, src, jaunter) & COMPONENT_STOP_CONSUMPTION)
+ return FALSE
+
+ jaunter.revive(full_heal = TRUE, admin_revive = FALSE)
+
+ // No defib possible after laughter
+ victim.apply_damage(1000, BRUTE, wound_bonus = CANT_WOUND)
+ victim.death()
+ on_victim_consumed(victim, jaunter)
+
+/**
+ * Called when a victim starts to be consumed.
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/proc/on_victim_start_consume(mob/living/victim, mob/living/jaunter)
+ to_chat(jaunter, span_danger("You begin to feast on [victim]... You can not move while you are doing this."))
+
+/**
+ * Called when a victim is successfully consumed.
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/proc/on_victim_consumed(mob/living/victim, mob/living/jaunter)
+ to_chat(jaunter, span_danger("You devour [victim]. Your health is fully restored."))
+ qdel(victim)
+
+/**
+ * Laughter demon's blood crawl
+ * All mobs consumed are revived after the demon is killed.
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny
+ name = "Friendly Blood Crawl"
+ desc = "Allows you to phase in and out of existance via pools of blood. If you are dragging someone in critical or dead - I mean, \
+ sleeping, when entering a blood pool, they will be invited to a party and fully heal you!"
+ consume_sound = 'sound/misc/scary_horn.ogg'
+
+ // Keep the people we hug!
+ var/list/mob/living/consumed_mobs = list()
+
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/Destroy()
+ consumed_mobs.Cut()
+ return ..()
+
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/Grant(mob/grant_to)
+ . = ..()
+ if(owner)
+ RegisterSignal(owner, COMSIG_LIVING_DEATH, .proc/on_death)
+
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/Remove(mob/living/remove_from)
+ UnregisterSignal(remove_from, COMSIG_LIVING_DEATH)
+ return ..()
+
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/on_victim_start_consume(mob/living/victim, mob/living/jaunter)
+ to_chat(jaunter, span_clown("You invite [victim] to your party! You can not move while you are doing this."))
+
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/on_victim_consumed(mob/living/victim, mob/living/jaunter)
+ to_chat(jaunter, span_clown("[victim] joins your party! Your health is fully restored."))
+ consumed_mobs += victim
+ RegisterSignal(victim, COMSIG_MOB_STATCHANGE, .proc/on_victim_statchange)
+ RegisterSignal(victim, COMSIG_PARENT_QDELETING, .proc/on_victim_deleted)
+
+/**
+ * Signal proc for COMSIG_LIVING_DEATH and COMSIG_PARENT_QDELETING
+ *
+ * If our demon is deleted or destroyed, expel all of our consumed mobs
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/proc/on_death(datum/source)
+ SIGNAL_HANDLER
+
+ var/turf/release_turf = get_turf(source)
+ for(var/mob/living/friend as anything in consumed_mobs)
+
+ // Unregister the signals first
+ UnregisterSignal(friend, list(COMSIG_MOB_STATCHANGE, COMSIG_PARENT_QDELETING))
+
+ friend.forceMove(release_turf)
+ if(!friend.revive(full_heal = TRUE, admin_revive = TRUE))
+ continue
+ friend.grab_ghost(force = TRUE)
+ playsound(release_turf, consumed_mobs, 50, TRUE, -1)
+ to_chat(friend, span_clown("You leave [source]'s warm embrace, and feel ready to take on the world."))
+
+
+/**
+ * Handle signal from a consumed mob changing stat.
+ *
+ * A signal handler for if one of the laughter demon's consumed mobs has
+ * changed stat. If they're no longer dead (because they were dead when
+ * swallowed), eject them so they can't rip their way out from the inside.
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/proc/on_victim_statchange(mob/living/victim, new_stat)
+ SIGNAL_HANDLER
+
+ if(new_stat == DEAD)
+ return
+ // Someone we've eaten has spontaneously revived; maybe regen coma, maybe a changeling
+ victim.forceMove(get_turf(victim))
+ victim.visible_message(span_warning("[victim] falls out of the air, covered in blood, with a confused look on their face."))
+ exit_blood_effect(victim)
+
+ consumed_mobs -= victim
+ UnregisterSignal(victim, COMSIG_MOB_STATCHANGE)
+
+/**
+ * Handle signal from a consumed mob being deleted. Clears any references.
+ */
+/datum/action/cooldown/spell/jaunt/bloodcrawl/slaughter_demon/funny/proc/on_victim_deleted(datum/source)
+ SIGNAL_HANDLER
+
+ consumed_mobs -= source
+
+/// Bloodcrawl "hands", prevent the user from holding items in bloodcrawl
+/obj/item/bloodcrawl
+ name = "blood crawl"
+ desc = "You are unable to hold anything while in this form."
+ icon = 'icons/effects/blood.dmi'
+ item_flags = ABSTRACT | DROPDEL
+
+/obj/item/bloodcrawl/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_NODROP, ABSTRACT_ITEM_TRAIT)
diff --git a/code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm b/code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm
new file mode 100644
index 0000000000000..2c89ba21c7f84
--- /dev/null
+++ b/code/modules/spells/spell_types/jaunt/ethereal_jaunt.dm
@@ -0,0 +1,256 @@
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt
+ name = "Ethereal Jaunt"
+ desc = "This spell turns your form ethereal, temporarily making you invisible and able to pass through walls."
+ button_icon_state = "jaunt"
+ sound = 'sound/magic/ethereal_enter.ogg'
+
+ cooldown_time = 30 SECONDS
+ cooldown_reduction_per_rank = 5 SECONDS
+
+ jaunt_type = /obj/effect/dummy/phased_mob/spell_jaunt
+
+ var/exit_jaunt_sound = 'sound/magic/ethereal_exit.ogg'
+ /// For how long are we jaunting?
+ var/jaunt_duration = 5 SECONDS
+ /// For how long we become immobilized after exiting the jaunt.
+ var/jaunt_in_time = 0.5 SECONDS
+ /// For how long we become immobilized when using this spell.
+ var/jaunt_out_time = 0 SECONDS
+ /// Visual for jaunting
+ var/obj/effect/jaunt_in_type = /obj/effect/temp_visual/wizard
+ /// Visual for exiting the jaunt
+ var/obj/effect/jaunt_out_type = /obj/effect/temp_visual/wizard/out
+ /// List of valid exit points
+ var/list/exit_point_list
+
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/enter_jaunt(mob/living/jaunter)
+ . = ..()
+ if(!.)
+ return
+
+ var/turf/cast_turf = get_turf(.)
+ new jaunt_out_type(cast_turf, jaunter.dir)
+ jaunter.extinguish_mob()
+ do_steam_effects(cast_turf)
+
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/cast(mob/living/cast_on)
+ . = ..()
+ do_jaunt(cast_on)
+
+/**
+ * Begin the jaunt, and the entire jaunt chain.
+ * Puts cast_on in the phased mob holder here.
+ *
+ * Calls do_jaunt_out:
+ * - if jaunt_out_time is set to more than 0,
+ * Or immediately calls start_jaunt:
+ * - if jaunt_out_time = 0
+ */
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/do_jaunt(mob/living/cast_on)
+ // Makes sure they don't die or get jostled or something during the jaunt entry
+ // Honestly probably not necessary anymore, but better safe than sorry
+ cast_on.notransform = TRUE
+ var/obj/effect/dummy/phased_mob/holder = enter_jaunt(cast_on)
+ cast_on.notransform = FALSE
+
+ if(!holder)
+ CRASH("[type] attempted do_jaunt but failed to create a jaunt holder via enter_jaunt.")
+
+ if(jaunt_out_time > 0)
+ ADD_TRAIT(cast_on, TRAIT_IMMOBILIZED, REF(src))
+ addtimer(CALLBACK(src, .proc/do_jaunt_out, cast_on, holder), jaunt_out_time)
+ else
+ start_jaunt(cast_on, holder)
+
+/**
+ * The wind-up to the jaunt.
+ * Optional, only called if jaunt_out_time is set.
+ *
+ * Calls start_jaunt.
+ */
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/do_jaunt_out(mob/living/cast_on, obj/effect/dummy/phased_mob/spell_jaunt/holder)
+ if(QDELETED(cast_on) || QDELETED(holder) || QDELETED(src))
+ return
+
+ REMOVE_TRAIT(cast_on, TRAIT_IMMOBILIZED, REF(src))
+ start_jaunt(cast_on, holder)
+
+/**
+ * The actual process of starting the jaunt.
+ * Sets up the signals and exit points and allows
+ * the caster to actually start moving around.
+ *
+ * Calls stop_jaunt after the jaunt runs out.
+ */
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/start_jaunt(mob/living/cast_on, obj/effect/dummy/phased_mob/spell_jaunt/holder)
+ if(QDELETED(cast_on) || QDELETED(holder) || QDELETED(src))
+ return
+
+ LAZYINITLIST(exit_point_list)
+ RegisterSignal(holder, COMSIG_MOVABLE_MOVED, .proc/update_exit_point, target)
+ addtimer(CALLBACK(src, .proc/stop_jaunt, cast_on, holder, get_turf(holder)), jaunt_duration)
+
+/**
+ * The stopping of the jaunt.
+ * Unregisters and signals and places
+ * the jaunter on the turf they will exit at.
+ *
+ * Calls do_jaunt_in:
+ * - immediately, if jaunt_in_time >= 2.5 seconds
+ * - 2.5 seconds - jaunt_in_time seconds otherwise
+ */
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/stop_jaunt(mob/living/cast_on, obj/effect/dummy/phased_mob/spell_jaunt/holder, turf/start_point)
+ if(QDELETED(cast_on) || QDELETED(holder) || QDELETED(src))
+ return
+
+ UnregisterSignal(holder, COMSIG_MOVABLE_MOVED)
+ // The caster escaped our holder somehow?
+ if(cast_on.loc != holder)
+ qdel(holder)
+ return
+
+ // Pick an exit turf to deposit the jaunter
+ var/turf/found_exit
+ for(var/turf/possible_exit as anything in exit_point_list)
+ if(possible_exit.is_blocked_turf_ignore_climbable())
+ continue
+ found_exit = possible_exit
+ break
+
+ // No valid exit was found
+ if(!found_exit)
+ // It's possible no exit was found, because we literally didn't even move
+ if(get_turf(cast_on) != start_point)
+ to_chat(cast_on, span_danger("Unable to find an unobstructed space, you find yourself ripped back to where you started."))
+ // Either way, default to where we started
+ found_exit = start_point
+
+ exit_point_list = null
+ holder.forceMove(found_exit)
+ do_steam_effects(found_exit)
+ holder.reappearing = TRUE
+ if(exit_jaunt_sound)
+ playsound(found_exit, exit_jaunt_sound, 50, TRUE)
+
+ ADD_TRAIT(cast_on, TRAIT_IMMOBILIZED, REF(src))
+
+ if(2.5 SECONDS - jaunt_in_time <= 0)
+ do_jaunt_in(cast_on, holder, found_exit)
+ else
+ addtimer(CALLBACK(src, .proc/do_jaunt_in, cast_on, holder, found_exit), 2.5 SECONDS - jaunt_in_time)
+
+/**
+ * The wind-up (wind-out?) of exiting the jaunt.
+ * Optional, only called if jaunt_in_time is above 2.5 seconds.
+ *
+ * Calls end_jaunt.
+ */
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/do_jaunt_in(mob/living/cast_on, obj/effect/dummy/phased_mob/spell_jaunt/holder, turf/final_point)
+ if(QDELETED(cast_on) || QDELETED(holder) || QDELETED(src))
+ return
+
+ new jaunt_in_type(final_point, holder.dir)
+ cast_on.setDir(holder.dir)
+
+ if(jaunt_in_time > 0)
+ addtimer(CALLBACK(src, .proc/end_jaunt, cast_on, holder, final_point), jaunt_in_time)
+ else
+ end_jaunt(cast_on, holder, final_point)
+
+/**
+ * Finally, the actual veritable end of the jaunt chains.
+ * Deletes the phase holder, ejecting the caster at final_point.
+ *
+ * If the final_point is dense for some reason,
+ * tries to put the caster in an adjacent turf.
+ */
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/end_jaunt(mob/living/cast_on, obj/effect/dummy/phased_mob/spell_jaunt/holder, turf/final_point)
+ if(QDELETED(cast_on) || QDELETED(holder) || QDELETED(src))
+ return
+ cast_on.notransform = TRUE
+ exit_jaunt(cast_on)
+ cast_on.notransform = FALSE
+
+ REMOVE_TRAIT(cast_on, TRAIT_IMMOBILIZED, REF(src))
+
+ if(final_point.density)
+ var/list/aside_turfs = get_adjacent_open_turfs(final_point)
+ if(length(aside_turfs))
+ cast_on.forceMove(pick(aside_turfs))
+
+/**
+ * Updates the exit point of the jaunt
+ *
+ * Called when the jaunting mob holder moves, this updates the backup exit-jaunt
+ * location, in case the jaunt ends with the mob still in a wall. Five
+ * spots are kept in the list, in case the last few changed since we passed
+ * by (doors closing, engineers building walls, etc)
+ */
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/update_exit_point(mob/living/source)
+ SIGNAL_HANDLER
+
+ var/turf/location = get_turf(source)
+ if(location.is_blocked_turf_ignore_climbable())
+ return
+ exit_point_list.Insert(1, location)
+ if(length(exit_point_list) >= 5)
+ exit_point_list.Cut(5)
+
+/// Does some steam effects from the jaunt at passed loc.
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/proc/do_steam_effects(turf/loc)
+ var/datum/effect_system/steam_spread/steam = new()
+ steam.set_up(10, FALSE, loc)
+ steam.start()
+
+
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift
+ name = "Phase Shift"
+ desc = "This spell allows you to pass through walls."
+ background_icon_state = "bg_demon"
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "phaseshift"
+
+ cooldown_time = 25 SECONDS
+ spell_requirements = NONE
+
+ jaunt_duration = 5 SECONDS
+ jaunt_in_time = 0.6 SECONDS
+ jaunt_out_time = 0.6 SECONDS
+ jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith
+ jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out
+
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/do_steam_effects(mobloc)
+ return
+
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/angelic
+ name = "Purified Phase Shift"
+ jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith/angelic
+ jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out/angelic
+
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/mystic
+ name = "Mystic Phase Shift"
+ jaunt_in_type = /obj/effect/temp_visual/dir_setting/wraith/mystic
+ jaunt_out_type = /obj/effect/temp_visual/dir_setting/wraith/out/mystic
+
+/datum/action/cooldown/spell/jaunt/ethereal_jaunt/shift/golem
+ name = "Runic Phase Shift"
+ cooldown_time = 80 SECONDS
+ jaunt_in_type = /obj/effect/temp_visual/dir_setting/cult/phase
+ jaunt_out_type = /obj/effect/temp_visual/dir_setting/cult/phase/out
+
+
+/// The dummy that holds people jaunting. Maybe one day we can replace it.
+/obj/effect/dummy/phased_mob/spell_jaunt
+ movespeed = 2 //quite slow.
+ /// Whether we're currently reappearing - we can't move if so
+ var/reappearing = FALSE
+
+/obj/effect/dummy/phased_mob/spell_jaunt/phased_check(mob/living/user, direction)
+ if(reappearing)
+ return
+ . = ..()
+ if(!.)
+ return
+ if (locate(/obj/effect/blessing) in .)
+ to_chat(user, span_warning("Holy energies block your path!"))
+ return null
diff --git a/code/modules/spells/spell_types/jaunt/shadow_walk.dm b/code/modules/spells/spell_types/jaunt/shadow_walk.dm
new file mode 100644
index 0000000000000..64405faf99377
--- /dev/null
+++ b/code/modules/spells/spell_types/jaunt/shadow_walk.dm
@@ -0,0 +1,82 @@
+/datum/action/cooldown/spell/jaunt/shadow_walk
+ name = "Shadow Walk"
+ desc = "Grants unlimited movement in darkness."
+ background_icon_state = "bg_alien"
+ icon_icon = 'icons/mob/actions/actions_minor_antag.dmi'
+ button_icon_state = "ninja_cloak"
+
+ spell_requirements = NONE
+ jaunt_type = /obj/effect/dummy/phased_mob/shadow
+
+/datum/action/cooldown/spell/jaunt/shadow_walk/cast(mob/living/cast_on)
+ . = ..()
+ if(is_jaunting(cast_on))
+ exit_jaunt(cast_on)
+ return
+
+ var/turf/cast_turf = get_turf(cast_on)
+ if(cast_turf.get_lumcount() >= SHADOW_SPECIES_LIGHT_THRESHOLD)
+ to_chat(cast_on, span_warning("It isn't dark enough here!"))
+ return
+
+ playsound(cast_turf, 'sound/magic/ethereal_enter.ogg', 50, TRUE, -1)
+ cast_on.visible_message(span_boldwarning("[cast_on] melts into the shadows!"))
+ cast_on.SetAllImmobility(0)
+ cast_on.setStaminaLoss(0, FALSE)
+ enter_jaunt(cast_on)
+
+/obj/effect/dummy/phased_mob/shadow
+ name = "shadows"
+ /// The amount that shadow heals us per SSobj tick (times delta_time)
+ var/healing_rate = 1.5
+
+/obj/effect/dummy/phased_mob/shadow/Initialize(mapload)
+ . = ..()
+ START_PROCESSING(SSobj, src)
+
+/obj/effect/dummy/phased_mob/shadow/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ return ..()
+
+/obj/effect/dummy/phased_mob/shadow/process(delta_time)
+ var/turf/T = get_turf(src)
+ var/light_amount = T.get_lumcount()
+ if(!jaunter || jaunter.loc != src)
+ qdel(src)
+ return
+
+ if(light_amount < 0.2 && !QDELETED(jaunter) && isliving(jaunter)) //heal in the dark
+ var/mob/living/living_jaunter = jaunter
+ living_jaunter.heal_overall_damage((healing_rate * delta_time), (healing_rate * delta_time), 0, BODYTYPE_ORGANIC)
+
+ check_light_level()
+
+/obj/effect/dummy/phased_mob/shadow/relaymove(mob/living/user, direction)
+ var/turf/oldloc = loc
+ . = ..()
+ if(loc != oldloc)
+ check_light_level()
+
+/obj/effect/dummy/phased_mob/shadow/phased_check(mob/living/user, direction)
+ . = ..()
+ if(. && isspaceturf(.))
+ to_chat(user, span_warning("It really would not be wise to go into space."))
+ return FALSE
+
+/obj/effect/dummy/phased_mob/shadow/proc/check_light_level()
+ var/turf/T = get_turf(src)
+ var/light_amount = T.get_lumcount()
+ if(light_amount > 0.2) // jaunt ends
+ eject_jaunter(TRUE)
+
+/obj/effect/dummy/phased_mob/shadow/eject_jaunter(forced_out = FALSE)
+ var/turf/reveal_turf = get_turf(src)
+
+ if(istype(reveal_turf))
+ if(forced_out)
+ reveal_turf.visible_message(span_boldwarning("[jaunter] is revealed by the light!"))
+ else
+ reveal_turf.visible_message(span_boldwarning("[jaunter] emerges from the darkness!"))
+ playsound(reveal_turf, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
+
+ return ..()
diff --git a/code/modules/spells/spell_types/knock.dm b/code/modules/spells/spell_types/knock.dm
deleted file mode 100644
index 921bbe5a9e05b..0000000000000
--- a/code/modules/spells/spell_types/knock.dm
+++ /dev/null
@@ -1,31 +0,0 @@
-/obj/effect/proc_holder/spell/aoe_turf/knock
- name = "Knock"
- desc = "This spell opens nearby doors and closets."
-
- school = SCHOOL_TRANSMUTATION
- charge_max = 100
- clothes_req = FALSE
- invocation = "AULIE OXIN FIERA"
- invocation_type = INVOCATION_WHISPER
- range = 3
- cooldown_min = 20 //20 deciseconds reduction per rank
-
- action_icon_state = "knock"
-
-/obj/effect/proc_holder/spell/aoe_turf/knock/cast(list/targets,mob/user = usr)
- SEND_SOUND(user, sound('sound/magic/knock.ogg'))
- for(var/turf/T in targets)
- for(var/obj/machinery/door/door in T.contents)
- INVOKE_ASYNC(src, .proc/open_door, door)
- for(var/obj/structure/closet/C in T.contents)
- INVOKE_ASYNC(src, .proc/open_closet, C)
-
-/obj/effect/proc_holder/spell/aoe_turf/knock/proc/open_door(obj/machinery/door/door)
- if(istype(door, /obj/machinery/door/airlock))
- var/obj/machinery/door/airlock/A = door
- A.locked = FALSE
- door.open()
-
-/obj/effect/proc_holder/spell/aoe_turf/knock/proc/open_closet(obj/structure/closet/C)
- C.locked = FALSE
- C.open()
diff --git a/code/modules/spells/spell_types/lichdom.dm b/code/modules/spells/spell_types/lichdom.dm
deleted file mode 100644
index 1b5e24abc88bc..0000000000000
--- a/code/modules/spells/spell_types/lichdom.dm
+++ /dev/null
@@ -1,74 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/lichdom
- name = "Bind Soul"
- desc = "A spell that binds your soul to an item in your hands. \
- Binding your soul to an item will turn you into an immortal Lich. \
- So long as the item remains intact, you will revive from death, \
- no matter the circumstances."
- action_icon = 'icons/mob/actions/actions_spells.dmi'
- action_icon_state = "skeleton"
- centcom_cancast = FALSE
- invocation = "NECREM IMORTIUM!"
- invocation_type = INVOCATION_SHOUT
- school = SCHOOL_NECROMANCY
- level_max = 0 // Cannot be improved (yet)
- range = -1
- charge_max = 1 SECONDS
- cooldown_min = 1 SECONDS
- clothes_req = FALSE
- include_user = TRUE
-
-/obj/effect/proc_holder/spell/targeted/lichdom/cast(list/targets, mob/user = usr)
- for(var/mob/living/caster in targets)
-
- if(HAS_TRAIT(caster, TRAIT_NO_SOUL))
- to_chat(caster, span_warning("You don't have a soul to bind!"))
- return
-
- var/obj/item/marked_item = caster.get_active_held_item()
- if(marked_item.item_flags & ABSTRACT)
- return
- if(HAS_TRAIT(marked_item, TRAIT_NODROP))
- to_chat(caster, span_warning("[marked_item] is stuck to your hand - it wouldn't be a wise idea to place your soul into it."))
- return
- // I ensouled the nuke disk once.
- // But it's a really mean tactic,
- // so we probably should disallow it.
- if(SEND_SIGNAL(marked_item, COMSIG_ITEM_IMBUE_SOUL, user) & COMPONENT_BLOCK_IMBUE)
- to_chat(caster, span_warning("[marked_item] is not suitable for emplacement of your fragile soul."))
- return
-
- playsound(user, 'sound/effects/pope_entry.ogg', 100)
-
- to_chat(caster, span_green("You begin to focus your very being into [marked_item]..."))
- if(!do_after(caster, 5 SECONDS, target = marked_item, timed_action_flags = IGNORE_HELD_ITEM))
- to_chat(caster, span_warning("Your soul snaps back to your body as you stop ensouling [marked_item]!"))
- return
-
- marked_item.AddComponent(/datum/component/phylactery, caster.mind)
-
- caster.set_species(/datum/species/skeleton)
- to_chat(caster, span_userdanger("With a hideous feeling of emptiness you watch in horrified fascination \
- as skin sloughs off bone! Blood boils, nerves disintegrate, eyes boil in their sockets! \
- As your organs crumble to dust in your fleshless chest you come to terms with your choice. \
- You're a lich!"))
-
- if(iscarbon(caster))
- var/mob/living/carbon/carbon_caster = caster
- var/obj/item/organ/internal/brain/lich_brain = carbon_caster.getorganslot(ORGAN_SLOT_BRAIN)
- if(lich_brain) // This prevents MMIs being used to stop lich revives
- lich_brain.organ_flags &= ~ORGAN_VITAL
- lich_brain.decoy_override = TRUE
-
- if(ishuman(caster))
- var/mob/living/carbon/human/human_caster = caster
- human_caster.dropItemToGround(human_caster.w_uniform)
- human_caster.dropItemToGround(human_caster.wear_suit)
- human_caster.dropItemToGround(human_caster.head)
- human_caster.equip_to_slot_or_del(new /obj/item/clothing/suit/wizrobe/black(human_caster), ITEM_SLOT_OCLOTHING)
- human_caster.equip_to_slot_or_del(new /obj/item/clothing/head/wizard/black(human_caster), ITEM_SLOT_HEAD)
- human_caster.equip_to_slot_or_del(new /obj/item/clothing/under/color/black(human_caster), ITEM_SLOT_ICLOTHING)
-
- // You only get one phylactery.
- caster.mind.RemoveSpell(src)
- // And no soul. You just sold it
- ADD_TRAIT(caster, TRAIT_NO_SOUL, LICH_TRAIT)
diff --git a/code/modules/spells/spell_types/lightning.dm b/code/modules/spells/spell_types/lightning.dm
deleted file mode 100644
index b197deb5bab6d..0000000000000
--- a/code/modules/spells/spell_types/lightning.dm
+++ /dev/null
@@ -1,87 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/tesla
- name = "Tesla Blast"
- desc = "Charge up a tesla arc and release it at a random nearby target! You can move freely while it charges. The arc jumps between targets and can knock them down."
- charge_type = "recharge"
- charge_max = 300
- clothes_req = TRUE
- invocation = "UN'LTD P'WAH!"
- invocation_type = INVOCATION_SHOUT
- school = SCHOOL_EVOCATION
- range = 7
- cooldown_min = 30
- selection_type = "view"
- random_target = TRUE
- var/ready = FALSE
- var/static/mutable_appearance/halo
- var/sound/Snd // so far only way i can think of to stop a sound, thank MSO for the idea.
-
- action_icon_state = "lightning"
-
-/obj/effect/proc_holder/spell/targeted/tesla/Click()
- if(!ready && cast_check())
- StartChargeup()
- return TRUE
-
-/obj/effect/proc_holder/spell/targeted/tesla/proc/StartChargeup(mob/user = usr)
- ready = TRUE
- to_chat(user, span_notice("You start gathering the power."))
- Snd = new/sound('sound/magic/lightning_chargeup.ogg',channel = 7)
- halo = halo || mutable_appearance('icons/effects/effects.dmi', "electricity", EFFECTS_LAYER)
- user.add_overlay(halo)
- playsound(get_turf(user), Snd, 50, FALSE)
- if(do_after(user, 10 SECONDS, timed_action_flags = (IGNORE_USER_LOC_CHANGE|IGNORE_HELD_ITEM)))
- if(ready && cast_check(skipcharge=1))
- choose_targets()
- else
- revert_cast(user, 0)
- else
- revert_cast(user, 0)
-
-/obj/effect/proc_holder/spell/targeted/tesla/proc/Reset(mob/user = usr)
- ready = FALSE
- user.cut_overlay(halo)
-
-/obj/effect/proc_holder/spell/targeted/tesla/revert_cast(mob/user = usr, message = 1)
- if(message)
- to_chat(user, span_notice("No target found in range."))
- Reset(user)
- ..()
-
-/obj/effect/proc_holder/spell/targeted/tesla/cast(list/targets, mob/user = usr)
- ready = FALSE
- var/mob/living/carbon/target = targets[1]
- Snd=sound(null, repeat = 0, wait = 1, channel = Snd.channel) //byond, why you suck?
- playsound(get_turf(user),Snd,50,FALSE)// Sorry MrPerson, but the other ways just didn't do it the way i needed to work, this is the only way.
- if(get_dist(user,target)>range)
- to_chat(user, span_warning("[target.p_theyre(TRUE)] too far away!"))
- Reset(user)
- return
-
- playsound(get_turf(user), 'sound/magic/lightningbolt.ogg', 50, TRUE)
- user.Beam(target,icon_state="lightning[rand(1,12)]", time = 5)
-
- Bolt(user,target,30,5,user)
- Reset(user)
-
-/obj/effect/proc_holder/spell/targeted/tesla/proc/Bolt(mob/origin,mob/target,bolt_energy,bounces,mob/user = usr)
- origin.Beam(target,icon_state="lightning[rand(1,12)]", time = 5)
- var/mob/living/carbon/current = target
- if(current.can_block_magic())
- playsound(get_turf(current), 'sound/magic/lightningshock.ogg', 50, TRUE, -1)
- current.visible_message(span_warning("[current] absorbs the spell, remaining unharmed!"), span_userdanger("You absorb the spell, remaining unharmed!"))
- else if(bounces < 1)
- current.electrocute_act(bolt_energy,"Lightning Bolt",flags = SHOCK_NOGLOVES)
- playsound(get_turf(current), 'sound/magic/lightningshock.ogg', 50, TRUE, -1)
- else
- current.electrocute_act(bolt_energy,"Lightning Bolt",flags = SHOCK_NOGLOVES)
- playsound(get_turf(current), 'sound/magic/lightningshock.ogg', 50, TRUE, -1)
- var/list/possible_targets = new
- for(var/mob/living/M in view(range,target))
- if(user == M || target == M && los_check(current,M)) // || origin == M ? Not sure double shockings is good or not
- continue
- possible_targets += M
- if(!possible_targets.len)
- return
- var/mob/living/next = pick(possible_targets)
- if(next)
- Bolt(current,next,max((bolt_energy-5),5),bounces-1,user)
diff --git a/code/modules/spells/spell_types/list_target/_list_target.dm b/code/modules/spells/spell_types/list_target/_list_target.dm
new file mode 100644
index 0000000000000..d595552e98795
--- /dev/null
+++ b/code/modules/spells/spell_types/list_target/_list_target.dm
@@ -0,0 +1,41 @@
+/**
+ * ## List Target spells
+ *
+ * These spells will prompt the user with a tgui list
+ * of all nearby targets that they select on to cast.
+ *
+ * To add effects on cast, override "cast(atom/cast_on)".
+ * The cast_on atom is the atom that was selected by the list.
+ */
+/datum/action/cooldown/spell/list_target
+ /// The message displayed as the title of the tgui target input list.
+ var/choose_target_message = "Choose a target."
+ /// Radius around the caster that living targets are picked to choose from
+ var/target_radius = 7
+
+/datum/action/cooldown/spell/list_target/PreActivate(atom/caster)
+ var/list/list_targets = get_list_targets(caster, target_radius)
+ if(!length(list_targets))
+ caster.balloon_alert(caster, "no targets nearby!")
+ return FALSE
+
+ var/atom/chosen = tgui_input_list(caster, choose_target_message, name, sort_names(list_targets))
+ if(QDELETED(src) || QDELETED(caster) || QDELETED(chosen) || !can_cast_spell())
+ return FALSE
+
+ if(get_dist(chosen, caster) > target_radius)
+ caster.balloon_alert(caster, "they're too far!")
+ return FALSE
+
+ return Activate(chosen)
+
+/// Get a list of living targets in radius of the center to put in the target list.
+/datum/action/cooldown/spell/list_target/proc/get_list_targets(atom/center, target_radius = 7)
+ var/list/things = list()
+ for(var/mob/living/nearby_living in view(target_radius, center))
+ if(nearby_living == owner || nearby_living == center)
+ continue
+
+ things += nearby_living
+
+ return things
diff --git a/code/modules/spells/spell_types/list_target/telepathy.dm b/code/modules/spells/spell_types/list_target/telepathy.dm
new file mode 100644
index 0000000000000..e67fd72334215
--- /dev/null
+++ b/code/modules/spells/spell_types/list_target/telepathy.dm
@@ -0,0 +1,52 @@
+
+/datum/action/cooldown/spell/list_target/telepathy
+ name = "Telepathy"
+ desc = "Telepathically transmits a message to the target."
+ icon_icon = 'icons/mob/actions/actions_revenant.dmi'
+ button_icon_state = "r_transmit"
+
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+ antimagic_flags = MAGIC_RESISTANCE_MIND
+
+ choose_target_message = "Choose a target to whisper to."
+
+ /// The message we send to the next person via telepathy.
+ var/message
+ /// The span surrounding the telepathy message
+ var/telepathy_span = "notice"
+ /// The bolded span surrounding the telepathy message
+ var/bold_telepathy_span = "boldnotice"
+
+/datum/action/cooldown/spell/list_target/telepathy/before_cast(atom/cast_on)
+ . = ..()
+ if(. & SPELL_CANCEL_CAST)
+ return
+
+ message = tgui_input_text(owner, "What do you wish to whisper to [cast_on]?", "[src]")
+ if(QDELETED(src) || QDELETED(owner) || QDELETED(cast_on) || !can_cast_spell())
+ return . | SPELL_CANCEL_CAST
+
+ if(!message)
+ reset_spell_cooldown()
+ return . | SPELL_CANCEL_CAST
+
+/datum/action/cooldown/spell/list_target/telepathy/cast(mob/living/cast_on)
+ . = ..()
+ log_directed_talk(owner, cast_on, message, LOG_SAY, name)
+
+ var/formatted_message = "[message]"
+
+ to_chat(owner, "You transmit to [cast_on]: [formatted_message]")
+ if(!cast_on.can_block_magic(antimagic_flags, charge_cost = 0)) //hear no evil
+ to_chat(cast_on, "You hear something behind you talking... [formatted_message]")
+
+ for(var/mob/dead/ghost as anything in GLOB.dead_mob_list)
+ if(!isobserver(ghost))
+ continue
+
+ var/from_link = FOLLOW_LINK(ghost, owner)
+ var/from_mob_name = "[owner] [src]:"
+ var/to_link = FOLLOW_LINK(ghost, cast_on)
+ var/to_mob_name = span_name("[cast_on]")
+
+ to_chat(ghost, "[from_link] [from_mob_name] [formatted_message] [to_link] [to_mob_name]")
diff --git a/code/modules/spells/spell_types/curse.dm b/code/modules/spells/spell_types/madness_curse.dm
similarity index 95%
rename from code/modules/spells/spell_types/curse.dm
rename to code/modules/spells/spell_types/madness_curse.dm
index 473f5ce2f355c..330f8aa6ff17d 100644
--- a/code/modules/spells/spell_types/curse.dm
+++ b/code/modules/spells/spell_types/madness_curse.dm
@@ -22,7 +22,7 @@ GLOBAL_VAR_INIT(curse_of_madness_triggered, FALSE)
give_madness(to_curse, message)
/proc/give_madness(mob/living/carbon/human/to_curse, message)
- to_curse.playsound_local(to_curse, 'sound/magic/curse.ogg', 40, 1)
+ to_curse.playsound_local(get_turf(to_curse), 'sound/magic/curse.ogg', 40, 1)
to_chat(to_curse, span_reallybig(span_hypnophrase(message)))
to_chat(to_curse, span_warning("Your mind shatters!"))
switch(rand(1, 10))
diff --git a/code/modules/spells/spell_types/mime.dm b/code/modules/spells/spell_types/mime.dm
deleted file mode 100644
index 311c767171c2c..0000000000000
--- a/code/modules/spells/spell_types/mime.dm
+++ /dev/null
@@ -1,260 +0,0 @@
-/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_wall
- name = "Invisible Wall"
- desc = "The mime's performance transmutates a wall into physical reality."
- school = SCHOOL_MIME
- panel = "Mime"
- summon_type = list(/obj/effect/forcefield/mime)
- invocation_type = INVOCATION_EMOTE
- invocation_emote_self = "You form a wall in front of yourself."
- summon_lifespan = 300
- charge_max = 300
- clothes_req = FALSE
- antimagic_flags = NONE
- range = 0
- cast_sound = null
- human_req = TRUE
-
- action_icon = 'icons/mob/actions/actions_mime.dmi'
- action_icon_state = "invisible_wall"
- action_background_icon_state = "bg_mime"
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_wall/Click()
- if(usr?.mind)
- if(!usr.mind.miming)
- to_chat(usr, span_warning("You must dedicate yourself to silence first!"))
- return
- invocation = "[usr.real_name] looks as if a wall is in front of [usr.p_them()]."
- else
- invocation_type ="none"
- ..()
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_chair
- name = "Invisible Chair"
- desc = "The mime's performance transmutates a chair into physical reality."
- school = SCHOOL_MIME
- panel = "Mime"
- summon_type = list(/obj/structure/chair/mime)
- invocation_type = INVOCATION_EMOTE
- invocation_emote_self = "You conjure an invisible chair and sit down."
- summon_lifespan = 250
- charge_max = 300
- clothes_req = FALSE
- antimagic_flags = NONE
- range = 0
- cast_sound = null
- human_req = TRUE
-
- action_icon = 'icons/mob/actions/actions_mime.dmi'
- action_icon_state = "invisible_chair"
- action_background_icon_state = "bg_mime"
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_chair/Click()
- if(usr?.mind)
- if(!usr.mind.miming)
- to_chat(usr, span_warning("You must dedicate yourself to silence first!"))
- return
- invocation = "[usr.real_name] pulls out an invisible chair and sits down."
- else
- invocation_type ="none"
- ..()
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_chair/cast(list/targets,mob/user = usr)
- ..()
- var/turf/T = user.loc
- for (var/obj/structure/chair/A in T)
- if (is_type_in_list(A, summon_type))
- A.setDir(user.dir)
- A.buckle_mob(user)
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_box
- name = "Invisible Box"
- desc = "The mime's performance transmutates a box into physical reality."
- school = SCHOOL_MIME
- panel = "Mime"
- summon_type = list(/obj/item/storage/box/mime)
- invocation_type = INVOCATION_EMOTE
- invocation_emote_self = "You conjure up an invisible box, large enough to store a few things."
- summon_lifespan = 500
- charge_max = 300
- clothes_req = FALSE
- antimagic_flags = NONE
- range = 0
- cast_sound = null
- human_req = TRUE
-
- action_icon = 'icons/mob/actions/actions_mime.dmi'
- action_icon_state = "invisible_box"
- action_background_icon_state = "bg_mime"
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_box/cast(list/targets,mob/user = usr)
- ..()
- var/turf/T = user.loc
- for (var/obj/item/storage/box/mime/B in T)
- user.put_in_hands(B)
- B.alpha = 255
- addtimer(CALLBACK(B, /obj/item/storage/box/mime/.proc/emptyStorage, FALSE), (summon_lifespan - 1))
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/mime_box/Click()
- if(usr?.mind)
- if(!usr.mind.miming)
- to_chat(usr, span_warning("You must dedicate yourself to silence first!"))
- return
- invocation = "[usr.real_name] moves [usr.p_their()] hands in the shape of a cube, pressing a box out of the air."
- else
- invocation_type ="none"
- ..()
-
-
-/obj/effect/proc_holder/spell/targeted/mime/speak
- name = "Speech"
- desc = "Make or break a vow of silence."
- school = SCHOOL_MIME
- panel = "Mime"
- clothes_req = FALSE
- human_req = TRUE
- antimagic_flags = NONE
- charge_max = 3000
- range = -1
- include_user = TRUE
-
- action_icon = 'icons/mob/actions/actions_mime.dmi'
- action_icon_state = "mime_speech"
- action_background_icon_state = "bg_mime"
-
-/obj/effect/proc_holder/spell/targeted/mime/speak/Click()
- if(!usr)
- return
- if(!ishuman(usr))
- return
- var/mob/living/carbon/human/H = usr
- if(H.mind.miming)
- still_recharging_msg = span_warning("You can't break your vow of silence that fast!")
- else
- still_recharging_msg = span_warning("You'll have to wait before you can give your vow of silence again!")
- ..()
-
-/obj/effect/proc_holder/spell/targeted/mime/speak/cast(list/targets,mob/user = usr)
- for(var/mob/living/carbon/human/H in targets)
- H.mind.miming=!H.mind.miming
- if(H.mind.miming)
- to_chat(H, span_notice("You make a vow of silence."))
- SEND_SIGNAL(H, COMSIG_CLEAR_MOOD_EVENT, "vow")
- else
- SEND_SIGNAL(H, COMSIG_ADD_MOOD_EVENT, "vow", /datum/mood_event/broken_vow)
- to_chat(H, span_notice("You break your vow of silence."))
-
-// These spells can only be gotten from the "Guide for Advanced Mimery series" for Mime Traitors.
-
-/obj/effect/proc_holder/spell/targeted/forcewall/mime
- name = "Invisible Blockade"
- desc = "Form an invisible three tile wide blockade."
- school = SCHOOL_MIME
- panel = "Mime"
- wall_type = /obj/effect/forcefield/mime/advanced
- invocation_type = INVOCATION_EMOTE
- invocation_emote_self = "You form a blockade in front of yourself."
- charge_max = 600
- sound = null
- clothes_req = FALSE
- antimagic_flags = NONE
- range = -1
- include_user = TRUE
-
- action_icon = 'icons/mob/actions/actions_mime.dmi'
- action_icon_state = "invisible_blockade"
- action_background_icon_state = "bg_mime"
-
-/obj/effect/proc_holder/spell/targeted/forcewall/mime/Click()
- if(usr?.mind)
- if(!usr.mind.miming)
- to_chat(usr, span_warning("You must dedicate yourself to silence first!"))
- return
- invocation = "[usr.real_name] looks as if a blockade is in front of [usr.p_them()]."
- else
- invocation_type ="none"
- ..()
-
-/obj/effect/proc_holder/spell/aimed/finger_guns
- name = "Finger Guns"
- desc = "Shoot up to three mimed bullets from your fingers that damage and mute their targets. Can't be used if you have something in your hands."
- school = SCHOOL_MIME
- panel = "Mime"
- charge_max = 300
- clothes_req = FALSE
- antimagic_flags = NONE
- invocation_type = INVOCATION_EMOTE
- invocation_emote_self = span_danger("You fire your finger gun!")
- range = 20
- projectile_type = /obj/projectile/bullet/mime
- projectile_amount = 3
- sound = null
- active_msg = "You draw your fingers!"
- deactive_msg = "You put your fingers at ease. Another time."
- active = FALSE
-
- action_icon = 'icons/mob/actions/actions_mime.dmi'
- action_icon_state = "finger_guns0"
- action_background_icon_state = "bg_mime"
- base_icon_state = "finger_guns"
-
-
-/obj/effect/proc_holder/spell/aimed/finger_guns/Click()
- var/mob/living/carbon/human/owner = usr
- if(owner.incapacitated())
- to_chat(owner, span_warning("You can't properly point your fingers while incapacitated."))
- return
- if(owner.get_active_held_item())
- to_chat(owner, span_warning("You can't properly fire your finger guns with something in your hand."))
- return
- if(usr?.mind)
- if(!usr.mind.miming)
- to_chat(usr, span_warning("You must dedicate yourself to silence first!"))
- return
- invocation = "[usr.real_name] fires [usr.p_their()] finger gun!"
- else
- invocation_type ="none"
- ..()
-
-/obj/effect/proc_holder/spell/aimed/finger_guns/InterceptClickOn(mob/living/caller, params, atom/target)
- if(caller.get_active_held_item())
- to_chat(caller, span_warning("You can't properly fire your finger guns with something in your hand."))
- return
- if(caller.incapacitated())
- to_chat(caller, span_warning("You can't properly point your fingers while incapacitated."))
- if(charge_type == "recharge")
- var/refund_percent = current_amount/projectile_amount
- charge_counter = charge_max * refund_percent
- start_recharge()
- remove_ranged_ability()
- on_deactivation(caller)
- ..()
-
-/obj/item/book/granter/spell/mimery_blockade
- spell = /obj/effect/proc_holder/spell/targeted/forcewall/mime
- spellname = "Invisible Blockade"
- name = "Guide to Advanced Mimery Vol 1"
- desc = "The pages don't make any sound when turned."
- icon_state ="bookmime"
- remarks = list("...")
-
-/obj/item/book/granter/spell/mimery_blockade/attack_self(mob/user)
- . = ..()
- if(!.)
- return
- if(!locate(/obj/effect/proc_holder/spell/targeted/mime/speak) in user.mind.spell_list)
- user.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/mime/speak)
-
-/obj/item/book/granter/spell/mimery_guns
- spell = /obj/effect/proc_holder/spell/aimed/finger_guns
- spellname = "Finger Guns"
- name = "Guide to Advanced Mimery Vol 2"
- desc = "There aren't any words written..."
- icon_state ="bookmime"
- remarks = list("...")
-
-/obj/item/book/granter/spell/mimery_guns/attack_self(mob/user)
- . = ..()
- if(!.)
- return
- if(!locate(/obj/effect/proc_holder/spell/targeted/mime/speak) in user.mind.spell_list)
- user.mind.AddSpell(new /obj/effect/proc_holder/spell/targeted/mime/speak)
diff --git a/code/modules/spells/spell_types/personality_commune.dm b/code/modules/spells/spell_types/personality_commune.dm
deleted file mode 100644
index d776bb0aef1ef..0000000000000
--- a/code/modules/spells/spell_types/personality_commune.dm
+++ /dev/null
@@ -1,39 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/personality_commune
- name = "Personality Commune"
- desc = "Sends thoughts to your alternate consciousness."
- charge_max = 0
- clothes_req = FALSE
- range = -1
- include_user = TRUE
- action_icon_state = "telepathy"
- action_background_icon_state = "bg_spell"
- // Bidaly reminder that spells are not really "owned" by anyone
- /// Weakref to the trauma that owns this spell
- var/datum/weakref/trauma_ref
- var/flufftext = "You hear an echoing voice in the back of your head..."
-
-/obj/effect/proc_holder/spell/targeted/personality_commune/New(datum/brain_trauma/severe/split_personality/T)
- . = ..()
- trauma_ref = WEAKREF(T)
-
-/obj/effect/proc_holder/spell/targeted/personality_commune/Destroy()
- trauma_ref = null
- return ..()
-
-// Pillaged and adapted from telepathy code
-/obj/effect/proc_holder/spell/targeted/personality_commune/cast(list/targets, mob/user)
- var/datum/brain_trauma/severe/split_personality/trauma = trauma_ref?.resolve()
- if(!istype(trauma))
- to_chat(user, span_warning("Something is wrong; Either due a bug or admemes, you are trying to cast this spell without a split personality!"))
- return
- var/msg = tgui_input_text(usr, "What would you like to tell your other self?", "Commune")
- if(!msg)
- charge_counter = charge_max
- return
- to_chat(user, span_boldnotice("You concentrate and send thoughts to your other self: [msg]"))
- to_chat(trauma.owner, span_boldnotice("[flufftext][msg]"))
- log_directed_talk(user, trauma.owner, msg, LOG_SAY ,"[name]")
- for(var/ded in GLOB.dead_mob_list)
- if(!isobserver(ded))
- continue
- to_chat(ded, "[FOLLOW_LINK(ded, user)] [span_boldnotice("[user] [name]:")] [span_notice("\"[msg]\" to")] [span_name("[trauma]")]")
diff --git a/code/modules/spells/spell_types/pointed/_pointed.dm b/code/modules/spells/spell_types/pointed/_pointed.dm
new file mode 100644
index 0000000000000..4f5bbf2349e50
--- /dev/null
+++ b/code/modules/spells/spell_types/pointed/_pointed.dm
@@ -0,0 +1,181 @@
+/**
+ * ## Pointed spells
+ *
+ * These spells override the caster's click,
+ * allowing them to cast the spell on whatever is clicked on.
+ *
+ * To add effects on cast, override "cast(atom/cast_on)".
+ * The cast_on atom is the person who was clicked on.
+ */
+/datum/action/cooldown/spell/pointed
+ click_to_activate = TRUE
+
+ /// The base icon state of the spell's button icon, used for editing the icon "on" and "off"
+ var/base_icon_state
+ /// Message showing to the spell owner upon activating pointed spell.
+ var/active_msg
+ /// Message showing to the spell owner upon deactivating pointed spell.
+ var/deactive_msg
+ /// The casting range of our spell
+ var/cast_range = 7
+ /// Variable dictating if the spell will use turf based aim assist
+ var/aim_assist = TRUE
+
+/datum/action/cooldown/spell/pointed/New(Target)
+ . = ..()
+ if(!active_msg)
+ active_msg = "You prepare to use [src] on a target..."
+ if(!deactive_msg)
+ deactive_msg = "You dispel [src]."
+
+/datum/action/cooldown/spell/pointed/set_click_ability(mob/on_who)
+ . = ..()
+ if(!.)
+ return
+
+ on_activation(on_who)
+
+// Note: Destroy() calls Remove(), Remove() calls unset_click_ability() if our spell is active.
+/datum/action/cooldown/spell/pointed/unset_click_ability(mob/on_who, refund_cooldown = TRUE)
+ . = ..()
+ if(!.)
+ return
+
+ on_deactivation(on_who, refund_cooldown = refund_cooldown)
+
+/datum/action/cooldown/spell/pointed/before_cast(atom/cast_on)
+ . = ..()
+ if(. & SPELL_CANCEL_CAST)
+ on_deactivation(owner, refund_cooldown = FALSE)
+
+/// Called when the spell is activated / the click ability is set to our spell
+/datum/action/cooldown/spell/pointed/proc/on_activation(mob/on_who)
+ SHOULD_CALL_PARENT(TRUE)
+
+ to_chat(on_who, span_notice("[active_msg] Left-click to cast the spell on a target!"))
+ if(base_icon_state)
+ button_icon_state = "[base_icon_state]1"
+ UpdateButtons()
+ return TRUE
+
+/// Called when the spell is deactivated / the click ability is unset from our spell
+/datum/action/cooldown/spell/pointed/proc/on_deactivation(mob/on_who, refund_cooldown = TRUE)
+ SHOULD_CALL_PARENT(TRUE)
+
+ if(refund_cooldown)
+ // Only send the "deactivation" message if they're willingly disabling the ability
+ to_chat(on_who, span_notice("[deactive_msg]"))
+ if(base_icon_state)
+ button_icon_state = "[base_icon_state]0"
+ UpdateButtons()
+ return TRUE
+
+/datum/action/cooldown/spell/pointed/InterceptClickOn(mob/living/caller, params, atom/click_target)
+
+ var/atom/aim_assist_target
+ if(aim_assist && isturf(click_target))
+ // Find any human in the list. We aren't picky, it's aim assist after all
+ aim_assist_target = locate(/mob/living/carbon/human) in click_target
+ if(!aim_assist_target)
+ // If we didn't find a human, we settle for any living at all
+ aim_assist_target = locate(/mob/living) in click_target
+
+ return ..(caller, params, aim_assist_target || click_target)
+
+/datum/action/cooldown/spell/pointed/is_valid_target(atom/cast_on)
+ if(cast_on == owner)
+ to_chat(owner, span_warning("You cannot cast [src] on yourself!"))
+ return FALSE
+
+ if(get_dist(owner, cast_on) > cast_range)
+ to_chat(owner, span_warning("[cast_on.p_theyre(TRUE)] too far away!"))
+ return FALSE
+
+ return TRUE
+
+/**
+ * ### Pointed projectile spells
+ *
+ * Pointed spells that, instead of casting a spell directly on the target that's clicked,
+ * will instead fire a projectile pointed at the target's direction.
+ */
+/datum/action/cooldown/spell/pointed/projectile
+ /// What projectile we create when we shoot our spell.
+ var/obj/projectile/magic/projectile_type = /obj/projectile/magic/teleport
+ /// How many projectiles we can fire per cast. Not all at once, per click, kinda like charges
+ var/projectile_amount = 1
+ /// How many projectiles we have yet to fire, based on projectile_amount
+ var/current_amount = 0
+ /// How many projectiles we fire every fire_projectile() call.
+ /// Unwise to change without overriding or extending ready_projectile.
+ var/projectiles_per_fire = 1
+
+/datum/action/cooldown/spell/pointed/projectile/New(Target)
+ . = ..()
+ if(projectile_amount > 1)
+ unset_after_click = FALSE
+
+/datum/action/cooldown/spell/pointed/projectile/is_valid_target(atom/cast_on)
+ return TRUE
+
+/datum/action/cooldown/spell/pointed/projectile/on_activation(mob/on_who)
+ . = ..()
+ if(!.)
+ return
+
+ current_amount = projectile_amount
+
+/datum/action/cooldown/spell/pointed/projectile/on_deactivation(mob/on_who, refund_cooldown = TRUE)
+ . = ..()
+ if(projectile_amount > 1 && current_amount)
+ StartCooldown(cooldown_time * ((projectile_amount - current_amount) / projectile_amount))
+ current_amount = 0
+
+// cast_on is a turf, or atom target, that we clicked on to fire at.
+/datum/action/cooldown/spell/pointed/projectile/cast(atom/cast_on)
+ . = ..()
+ if(!isturf(owner.loc))
+ return FALSE
+
+ var/turf/caster_turf = get_turf(owner)
+ // Get the tile infront of the caster, based on their direction
+ var/turf/caster_front_turf = get_step(owner, owner.dir)
+
+ fire_projectile(cast_on)
+ owner.newtonian_move(get_dir(caster_front_turf, caster_turf))
+ if(current_amount <= 0)
+ unset_click_ability(owner, refund_cooldown = FALSE)
+
+ return TRUE
+
+/datum/action/cooldown/spell/pointed/projectile/after_cast(atom/cast_on)
+ . = ..()
+ if(current_amount > 0)
+ // We still have projectiles to cast!
+ // Reset our cooldown and let them fire away
+ reset_spell_cooldown()
+
+/datum/action/cooldown/spell/pointed/projectile/proc/fire_projectile(atom/target)
+ current_amount--
+ for(var/i in 1 to projectiles_per_fire)
+ var/obj/projectile/to_fire = new projectile_type()
+ ready_projectile(to_fire, target, owner, i)
+ to_fire.fire()
+ return TRUE
+
+/datum/action/cooldown/spell/pointed/projectile/proc/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration)
+ to_fire.firer = owner
+ to_fire.fired_from = get_turf(owner)
+ to_fire.preparePixelProjectile(target, owner)
+ RegisterSignal(to_fire, COMSIG_PROJECTILE_ON_HIT, .proc/on_cast_hit)
+
+ if(istype(to_fire, /obj/projectile/magic))
+ var/obj/projectile/magic/magic_to_fire = to_fire
+ magic_to_fire.antimagic_flags = antimagic_flags
+
+/// Signal proc for whenever the projectile we fire hits someone.
+/// Pretty much relays to the spell when the projectile actually hits something.
+/datum/action/cooldown/spell/pointed/projectile/proc/on_cast_hit(atom/source, mob/firer, atom/hit, angle)
+ SIGNAL_HANDLER
+
+ SEND_SIGNAL(src, COMSIG_SPELL_PROJECTILE_HIT, hit, firer, source)
diff --git a/code/modules/spells/spell_types/pointed/abyssal_gaze.dm b/code/modules/spells/spell_types/pointed/abyssal_gaze.dm
new file mode 100644
index 0000000000000..0cf4d3130a425
--- /dev/null
+++ b/code/modules/spells/spell_types/pointed/abyssal_gaze.dm
@@ -0,0 +1,54 @@
+
+/datum/action/cooldown/spell/pointed/abyssal_gaze
+ name = "Abyssal Gaze"
+ desc = "This spell instills a deep terror in your target, temporarily chilling and blinding it."
+ ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi'
+ background_icon_state = "bg_demon"
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "abyssal_gaze"
+
+ school = SCHOOL_EVOCATION
+ cooldown_time = 75 SECONDS
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+ antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_HOLY
+
+ cast_range = 5
+ active_msg = "You prepare to instill a deep terror in a target..."
+
+ /// The duration of the blind on our target
+ var/blind_duration = 4 SECONDS
+ /// The amount of temperature we take from our target
+ var/amount_to_cool = 200
+
+/datum/action/cooldown/spell/pointed/abyssal_gaze/is_valid_target(atom/cast_on)
+ return iscarbon(target)
+
+/datum/action/cooldown/spell/pointed/abyssal_gaze/cast(mob/living/carbon/cast_on)
+ . = ..()
+ if(cast_on.can_block_magic(antimagic_flags))
+ to_chat(owner, span_warning("The spell had no effect!"))
+ to_chat(cast_on, span_warning("You feel a freezing darkness closing in on you, but it rapidly dissipates."))
+ return FALSE
+
+ to_chat(cast_on, span_userdanger("A freezing darkness surrounds you..."))
+ cast_on.playsound_local(get_turf(cast_on), 'sound/hallucinations/i_see_you1.ogg', 50, 1)
+ owner.playsound_local(get_turf(owner), 'sound/effects/ghost2.ogg', 50, 1)
+ cast_on.become_blind(ABYSSAL_GAZE_BLIND)
+ addtimer(CALLBACK(src, .proc/cure_blindness, cast_on), blind_duration)
+ if(ishuman(cast_on))
+ var/mob/living/carbon/human/human_cast_on = cast_on
+ human_cast_on.adjust_coretemperature(-amount_to_cool)
+ cast_on.adjust_bodytemperature(-amount_to_cool)
+
+/**
+ * cure_blidness: Cures Abyssal Gaze blindness from the target
+ *
+ * Arguments:
+ * * target The mob that is being cured of the blindness.
+ */
+/datum/action/cooldown/spell/pointed/abyssal_gaze/proc/cure_blindness(mob/living/carbon/cast_on)
+ if(QDELETED(cast_on) || !istype(cast_on))
+ return
+
+ cast_on.cure_blind(ABYSSAL_GAZE_BLIND)
diff --git a/code/modules/spells/spell_types/pointed/barnyard.dm b/code/modules/spells/spell_types/pointed/barnyard.dm
index 4177ca48af715..b6fce6521555a 100644
--- a/code/modules/spells/spell_types/pointed/barnyard.dm
+++ b/code/modules/spells/spell_types/pointed/barnyard.dm
@@ -1,53 +1,55 @@
-/obj/effect/proc_holder/spell/pointed/barnyardcurse
+/datum/action/cooldown/spell/pointed/barnyardcurse
name = "Curse of the Barnyard"
desc = "This spell dooms an unlucky soul to possess the speech and facial attributes of a barnyard animal."
+ button_icon_state = "barn"
+ ranged_mousepointer = 'icons/effects/mouse_pointers/barn_target.dmi'
+
school = SCHOOL_TRANSMUTATION
- charge_type = "recharge"
- charge_max = 150
- charge_counter = 0
- clothes_req = FALSE
- stat_allowed = FALSE
+ cooldown_time = 15 SECONDS
+ cooldown_reduction_per_rank = 3 SECONDS
+
invocation = "KN'A FTAGHU, PUCK 'BTHNK!"
invocation_type = INVOCATION_SHOUT
- range = 7
- cooldown_min = 30
- ranged_mousepointer = 'icons/effects/mouse_pointers/barn_target.dmi'
- action_icon_state = "barn"
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
active_msg = "You prepare to curse a target..."
- deactive_msg = "You dispel the curse..."
- /// List of mobs which are allowed to be a target of the spell
- var/static/list/compatible_mobs_typecache = typecacheof(list(/mob/living/carbon/human))
+ deactive_msg = "You dispel the curse."
-/obj/effect/proc_holder/spell/pointed/barnyardcurse/cast(list/targets, mob/user)
- if(!targets.len)
- to_chat(user, span_warning("No target found in range!"))
+/datum/action/cooldown/spell/pointed/barnyardcurse/is_valid_target(atom/cast_on)
+ . = ..()
+ if(!.)
return FALSE
- if(!can_target(targets[1], user))
+ if(!ishuman(cast_on))
return FALSE
- var/mob/living/carbon/target = targets[1]
- if(target.can_block_magic())
- to_chat(user, span_warning("The spell had no effect!"))
- target.visible_message(span_danger("[target]'s face bursts into flames, which instantly burst outward, leaving [target] unharmed!"), \
- span_danger("Your face starts burning up, but the flames are repulsed by your anti-magic protection!"))
- return FALSE
+ var/mob/living/carbon/human/human_target = cast_on
+ if(!human_target.wear_mask)
+ return TRUE
- var/choice = pick(GLOB.cursed_animal_masks)
- var/obj/item/clothing/mask/magichead = new choice(get_turf(target))
+ return !(human_target.wear_mask.type in GLOB.cursed_animal_masks)
- target.visible_message(span_danger("[target]'s face bursts into flames, and a barnyard animal's head takes its place!"), \
- span_danger("Your face burns up, and shortly after the fire you realise you have the face of a barnyard animal!"))
- if(!target.dropItemToGround(target.wear_mask))
- qdel(target.wear_mask)
- target.equip_to_slot_if_possible(magichead, ITEM_SLOT_MASK, 1, 1)
- target.flash_act()
-
-/obj/effect/proc_holder/spell/pointed/barnyardcurse/can_target(atom/target, mob/user, silent)
+/datum/action/cooldown/spell/pointed/barnyardcurse/cast(mob/living/carbon/human/cast_on)
. = ..()
- if(!.)
- return FALSE
- if(!is_type_in_typecache(target, compatible_mobs_typecache))
- if(!silent)
- to_chat(user, span_warning("You are unable to curse [target]!"))
+ if(cast_on.can_block_magic(antimagic_flags))
+ cast_on.visible_message(
+ span_danger("[cast_on]'s face bursts into flames, which instantly burst outward, leaving [cast_on.p_them()] unharmed!"),
+ span_danger("Your face starts burning up, but the flames are repulsed by your anti-magic protection!"),
+ )
+ to_chat(owner, span_warning("The spell had no effect!"))
return FALSE
+
+ var/chosen_type = pick(GLOB.cursed_animal_masks)
+ var/obj/item/clothing/mask/animal/cursed_mask = new chosen_type(get_turf(target))
+
+ cast_on.visible_message(
+ span_danger("[target]'s face bursts into flames, and a barnyard animal's head takes its place!"),
+ span_userdanger("Your face burns up, and shortly after the fire you realise you have the face of a [cursed_mask.animal_type]!"),
+ )
+
+ // Can't drop? Nuke it
+ if(!cast_on.dropItemToGround(cast_on.wear_mask))
+ qdel(cast_on.wear_mask)
+
+ cast_on.equip_to_slot_if_possible(cursed_mask, ITEM_SLOT_MASK, TRUE, TRUE)
+ cast_on.flash_act()
return TRUE
diff --git a/code/modules/spells/spell_types/pointed/blind.dm b/code/modules/spells/spell_types/pointed/blind.dm
index de4d3ff2b8012..cd044e5cb4013 100644
--- a/code/modules/spells/spell_types/pointed/blind.dm
+++ b/code/modules/spells/spell_types/pointed/blind.dm
@@ -1,35 +1,51 @@
-/obj/effect/proc_holder/spell/pointed/trigger/blind
+/datum/action/cooldown/spell/pointed/blind
name = "Blind"
desc = "This spell temporarily blinds a single target."
+ button_icon_state = "blind"
+ ranged_mousepointer = 'icons/effects/mouse_pointers/blind_target.dmi'
+
+ sound = 'sound/magic/blind.ogg'
school = SCHOOL_TRANSMUTATION
- charge_max = 300
- clothes_req = FALSE
+ cooldown_time = 30 SECONDS
+ cooldown_reduction_per_rank = 6.25 SECONDS
+
invocation = "STI KALY"
invocation_type = INVOCATION_WHISPER
- message = "Your eyes cry out in pain!"
- cooldown_min = 50 //12 deciseconds reduction per rank
- starting_spells = list("/obj/effect/proc_holder/spell/targeted/inflict_handler/blind", "/obj/effect/proc_holder/spell/targeted/genetic/blind")
- ranged_mousepointer = 'icons/effects/mouse_pointers/blind_target.dmi'
- action_icon_state = "blind"
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
active_msg = "You prepare to blind a target..."
-/obj/effect/proc_holder/spell/targeted/inflict_handler/blind
- amt_eye_blind = 10
- amt_eye_blurry = 20
- sound = 'sound/magic/blind.ogg'
+ /// The amount of blind to apply
+ var/eye_blind_amount = 10
+ /// The amount of blurriness to apply
+ var/eye_blurry_amount = 20
+ /// The duration of the blind mutation placed on the person
+ var/blind_mutation_duration = 30 SECONDS
-/obj/effect/proc_holder/spell/targeted/genetic/blind
- mutations = list(/datum/mutation/human/blind)
- duration = 300
- charge_max = 400 // needs to be higher than the duration or it'll be permanent
- sound = 'sound/magic/blind.ogg'
-
-/obj/effect/proc_holder/spell/pointed/trigger/blind/can_target(atom/target, mob/user, silent)
+/datum/action/cooldown/spell/pointed/blind/is_valid_target(atom/cast_on)
. = ..()
if(!.)
return FALSE
- if(!isliving(target))
- if(!silent)
- to_chat(user, span_warning("You can only blind living beings!"))
+ if(!ishuman(cast_on))
return FALSE
+
+ var/mob/living/carbon/human/human_target = cast_on
+ return !human_target.is_blind()
+
+/datum/action/cooldown/spell/pointed/blind/cast(mob/living/carbon/human/cast_on)
+ . = ..()
+ if(cast_on.can_block_magic(antimagic_flags))
+ to_chat(cast_on, span_notice("Your eye itches, but it passes momentarily."))
+ to_chat(owner, span_warning("The spell had no effect!"))
+ return FALSE
+
+ to_chat(cast_on, span_warning("Your eyes cry out in pain!"))
+ cast_on.blind_eyes(eye_blind_amount)
+ cast_on.blur_eyes(eye_blurry_amount)
+ if(cast_on.dna && blind_mutation_duration > 0 SECONDS)
+ cast_on.dna.add_mutation(/datum/mutation/human/blind)
+ addtimer(CALLBACK(src, .proc/fix_eyes, cast_on), blind_mutation_duration)
return TRUE
+
+/datum/action/cooldown/spell/pointed/blind/proc/fix_eyes(mob/living/carbon/human/cast_on)
+ cast_on.dna?.remove_mutation(/datum/mutation/human/blind)
diff --git a/code/modules/spells/spell_types/pointed/dominate.dm b/code/modules/spells/spell_types/pointed/dominate.dm
new file mode 100644
index 0000000000000..f5b8aaa9c8c5a
--- /dev/null
+++ b/code/modules/spells/spell_types/pointed/dominate.dm
@@ -0,0 +1,49 @@
+/datum/action/cooldown/spell/pointed/dominate
+ name = "Dominate"
+ desc = "This spell dominates the mind of a lesser creature to the will of Nar'Sie, \
+ allying it only to her direct followers."
+ background_icon_state = "bg_demon"
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "dominate"
+ ranged_mousepointer = 'icons/effects/mouse_pointers/cult_target.dmi'
+
+ school = SCHOOL_EVOCATION
+ cooldown_time = 1 MINUTES
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+ // An UNHOLY, MAGIC SPELL that INFLUECNES THE MIND - all things work here, logically
+ antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_HOLY|MAGIC_RESISTANCE_MIND
+
+ cast_range = 7
+ active_msg = "You prepare to dominate the mind of a target..."
+
+/datum/action/cooldown/spell/pointed/dominate/is_valid_target(atom/cast_on)
+ if(!isanimal(cast_on))
+ return FALSE
+
+ var/mob/living/simple_animal/animal = cast_on
+ if(animal.mind)
+ return FALSE
+ if(animal.stat == DEAD)
+ return FALSE
+ if(animal.sentience_type != SENTIENCE_ORGANIC)
+ return FALSE
+ if("cult" in animal.faction)
+ return FALSE
+ if(HAS_TRAIT(animal, TRAIT_HOLY))
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/spell/pointed/dominate/cast(mob/living/simple_animal/cast_on)
+ . = ..()
+ if(cast_on.can_block_magic(antimagic_flags))
+ to_chat(cast_on, span_warning("Your feel someone attempting to subject your mind to terrible machinations!"))
+ to_chat(owner, span_warning("[cast_on] resists your domination!"))
+ return FALSE
+
+ var/turf/cast_turf = get_turf(cast_on)
+ cast_on.add_atom_colour("#990000", FIXED_COLOUR_PRIORITY)
+ cast_on.faction |= "cult"
+ playsound(cast_turf, 'sound/effects/ghost.ogg', 100, TRUE)
+ new /obj/effect/temp_visual/cult/sac(cast_turf)
diff --git a/code/modules/spells/spell_types/pointed/finger_guns.dm b/code/modules/spells/spell_types/pointed/finger_guns.dm
new file mode 100644
index 0000000000000..9c495d27d755e
--- /dev/null
+++ b/code/modules/spells/spell_types/pointed/finger_guns.dm
@@ -0,0 +1,48 @@
+/datum/action/cooldown/spell/pointed/projectile/finger_guns
+ name = "Finger Guns"
+ desc = "Shoot up to three mimed bullets from your fingers that damage and mute their targets. \
+ Can't be used if you have something in your hands."
+ background_icon_state = "bg_mime"
+ icon_icon = 'icons/mob/actions/actions_mime.dmi'
+ button_icon_state = "finger_guns0"
+ panel = "Mime"
+ sound = null
+
+ school = SCHOOL_MIME
+ cooldown_time = 30 SECONDS
+
+ invocation = ""
+ invocation_type = INVOCATION_EMOTE
+ invocation_self_message = span_danger("You fire your finger gun!")
+
+ spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIME_VOW
+ antimagic_flags = NONE
+ spell_max_level = 1
+
+ base_icon_state = "finger_guns"
+ active_msg = "You draw your fingers!"
+ deactive_msg = "You put your fingers at ease. Another time."
+ cast_range = 20
+ projectile_type = /obj/projectile/bullet/mime
+ projectile_amount = 3
+
+/datum/action/cooldown/spell/pointed/projectile/finger_guns/can_invoke(feedback = TRUE)
+ if(invocation_type == INVOCATION_EMOTE)
+ if(!ishuman(owner))
+ return FALSE
+
+ var/mob/living/carbon/human/human_owner = owner
+ if(human_owner.incapacitated())
+ if(feedback)
+ to_chat(owner, span_warning("You can't properly point your fingers while incapacitated."))
+ return FALSE
+ if(human_owner.get_active_held_item())
+ if(feedback)
+ to_chat(owner, span_warning("You can't properly fire your finger guns with something in your hand."))
+ return FALSE
+
+ return ..()
+
+/datum/action/cooldown/spell/pointed/projectile/finger_guns/before_cast(atom/cast_on)
+ . = ..()
+ invocation = span_notice("[cast_on] fires [cast_on.p_their()] finger gun!")
diff --git a/code/modules/spells/spell_types/pointed/fireball.dm b/code/modules/spells/spell_types/pointed/fireball.dm
new file mode 100644
index 0000000000000..47fd05c0f4680
--- /dev/null
+++ b/code/modules/spells/spell_types/pointed/fireball.dm
@@ -0,0 +1,23 @@
+/datum/action/cooldown/spell/pointed/projectile/fireball
+ name = "Fireball"
+ desc = "This spell fires an explosive fireball at a target."
+ button_icon_state = "fireball0"
+
+ sound = 'sound/magic/fireball.ogg'
+ school = SCHOOL_EVOCATION
+ cooldown_time = 6 SECONDS
+ cooldown_reduction_per_rank = 1 SECONDS // 1 second reduction per rank
+
+ invocation = "ONI SOMA!"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+ base_icon_state = "fireball"
+ active_msg = "You prepare to cast your fireball spell!"
+ deactive_msg = "You extinguish your fireball... for now."
+ cast_range = 8
+ projectile_type = /obj/projectile/magic/fireball
+
+/datum/action/cooldown/spell/pointed/projectile/fireball/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration)
+ . = ..()
+ to_fire.range = (6 + 2 * spell_level)
diff --git a/code/modules/spells/spell_types/pointed/lightning_bolt.dm b/code/modules/spells/spell_types/pointed/lightning_bolt.dm
new file mode 100644
index 0000000000000..e88e35235718f
--- /dev/null
+++ b/code/modules/spells/spell_types/pointed/lightning_bolt.dm
@@ -0,0 +1,43 @@
+/datum/action/cooldown/spell/pointed/projectile/lightningbolt
+ name = "Lightning Bolt"
+ desc = "Fire a lightning bolt at your foes! It will jump between targets, but can't knock them down."
+ button_icon_state = "lightning0"
+
+ sound = 'sound/magic/lightningbolt.ogg'
+ school = SCHOOL_EVOCATION
+ cooldown_time = 10 SECONDS
+ cooldown_reduction_per_rank = 2 SECONDS
+
+ invocation = "P'WAH, UNLIM'TED P'WAH!"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+ base_icon_state = "lightning"
+ active_msg = "You energize your hands with arcane lightning!"
+ deactive_msg = "You let the energy flow out of your hands back into yourself..."
+ projectile_type = /obj/projectile/magic/aoe/lightning
+
+ /// The range the bolt itself (different to the range of the projectile)
+ var/bolt_range = 15
+ /// The power of the bolt itself
+ var/bolt_power = 20000
+ /// The flags the bolt itself takes when zapping someone
+ var/bolt_flags = ZAP_MOB_DAMAGE
+
+/datum/action/cooldown/spell/pointed/projectile/lightningbolt/Grant(mob/grant_to)
+ . = ..()
+ ADD_TRAIT(owner, TRAIT_TESLA_SHOCKIMMUNE, type)
+
+/datum/action/cooldown/spell/pointed/projectile/lightningbolt/Remove(mob/living/remove_from)
+ REMOVE_TRAIT(remove_from, TRAIT_TESLA_SHOCKIMMUNE, type)
+ return ..()
+
+/datum/action/cooldown/spell/pointed/projectile/lightningbolt/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration)
+ . = ..()
+ if(!istype(to_fire, /obj/projectile/magic/aoe/lightning))
+ return
+
+ var/obj/projectile/magic/aoe/lightning/bolt = to_fire
+ bolt.zap_range = bolt_range
+ bolt.zap_power = bolt_power
+ bolt.zap_flags = bolt_flags
diff --git a/code/modules/spells/spell_types/pointed/mind_transfer.dm b/code/modules/spells/spell_types/pointed/mind_transfer.dm
index 888190f0112a4..bf31d402a4c22 100644
--- a/code/modules/spells/spell_types/pointed/mind_transfer.dm
+++ b/code/modules/spells/spell_types/pointed/mind_transfer.dm
@@ -1,104 +1,125 @@
-/obj/effect/proc_holder/spell/pointed/mind_transfer
- name = "Mind Transfer"
+/datum/action/cooldown/spell/pointed/mind_transfer
+ name = "Mind Swap"
desc = "This spell allows the user to switch bodies with a target next to him."
+ button_icon_state = "mindswap"
+ ranged_mousepointer = 'icons/effects/mouse_pointers/mindswap_target.dmi'
+
school = SCHOOL_TRANSMUTATION
- charge_max = 600
- clothes_req = FALSE
+ cooldown_time = 60 SECONDS
+ cooldown_reduction_per_rank = 10 SECONDS
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_MIND|SPELL_CASTABLE_AS_BRAIN
+ antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_MIND
+
invocation = "GIN'YU CAPAN"
invocation_type = INVOCATION_WHISPER
- range = 1
- cooldown_min = 200 //100 deciseconds reduction per rank
- ranged_mousepointer = 'icons/effects/mouse_pointers/mindswap_target.dmi'
- action_icon_state = "mindswap"
+
active_msg = "You prepare to swap minds with a target..."
- antimagic_flags = MAGIC_RESISTANCE|MAGIC_RESISTANCE_MIND
+ deactive_msg = "You dispel mind swap."
+ cast_range = 1
+
+ /// If TRUE, we cannot mindswap into mobs with minds if they do not currently have a key / player.
+ var/target_requires_key = TRUE
/// For how long is the caster stunned for after the spell
var/unconscious_amount_caster = 40 SECONDS
/// For how long is the victim stunned for after the spell
var/unconscious_amount_victim = 40 SECONDS
+ /// List of mobs we cannot mindswap into.
+ var/static/list/mob/living/blacklisted_mobs = typecacheof(list(
+ /mob/living/brain,
+ /mob/living/silicon/pai,
+ /mob/living/simple_animal/hostile/imp/slaughter,
+ /mob/living/simple_animal/hostile/megafauna,
+ ))
-/obj/effect/proc_holder/spell/pointed/mind_transfer/cast(list/targets, mob/living/user, silent = FALSE)
- if(!targets.len)
- if(!silent)
- to_chat(user, span_warning("No mind found!"))
- return FALSE
- if(targets.len > 1)
- if(!silent)
- to_chat(user, span_warning("Too many minds! You're not a hive damnit!"))
+/datum/action/cooldown/spell/pointed/mind_transfer/can_cast_spell(feedback = TRUE)
+ . = ..()
+ if(!.)
return FALSE
- if(!can_target(targets[1], user, silent))
+ if(!isliving(owner))
return FALSE
-
- var/mob/living/victim = targets[1] //The target of the spell whos body will be transferred to.
- if(istype(victim, /mob/living/simple_animal/hostile/guardian))
- var/mob/living/simple_animal/hostile/guardian/stand = victim
- if(stand.summoner)
- victim = stand.summoner
- var/datum/mind/VM = victim.mind
- if(victim.can_block_magic(antimagic_flags) || VM.has_antag_datum(/datum/antagonist/wizard) || VM.has_antag_datum(/datum/antagonist/cult) || VM.has_antag_datum(/datum/antagonist/changeling) || VM.has_antag_datum(/datum/antagonist/rev) || victim.key[1] == "@")
- if(!silent)
- to_chat(user, span_warning("[victim.p_their(TRUE)] mind is resisting your spell!"))
+ if(owner.suiciding)
+ if(feedback)
+ to_chat(owner, span_warning("You're killing yourself! You can't concentrate enough to do this!"))
return FALSE
-
- //You should not be able to enter one of the most powerful side-antags as a fucking wizard.
- if(istype(victim,/mob/living/simple_animal/hostile/imp/slaughter))
- to_chat(user, span_warning("The devilish contract doesn't include the 'mind swappable' package, please try again another lifetime."))
- return
-
- //MIND TRANSFER BEGIN
- var/mob/dead/observer/ghost = victim.ghostize()
- user.mind.transfer_to(victim)
-
- ghost.mind.transfer_to(user)
- if(ghost.key)
- user.key = ghost.key //have to transfer the key since the mind was not active
- qdel(ghost)
- //MIND TRANSFER END
-
- //Here we knock both mobs out for a time.
- user.Unconscious(unconscious_amount_caster)
- victim.Unconscious(unconscious_amount_victim)
- SEND_SOUND(user, sound('sound/magic/mandswap.ogg'))
- SEND_SOUND(victim, sound('sound/magic/mandswap.ogg')) // only the caster and victim hear the sounds, that way no one knows for sure if the swap happened
return TRUE
-/obj/effect/proc_holder/spell/pointed/mind_transfer/can_target(atom/target, mob/user, silent)
+/datum/action/cooldown/spell/pointed/mind_transfer/is_valid_target(atom/cast_on)
. = ..()
if(!.)
return FALSE
- if(!isliving(target))
- if(!silent)
- to_chat(user, span_warning("You can only swap minds with living beings!"))
+
+ if(!isliving(cast_on))
+ to_chat(owner, span_warning("You can only swap minds with living beings!"))
return FALSE
- if(user == target)
- if(!silent)
- to_chat(user, span_warning("You can't swap minds with yourself!"))
+ if(is_type_in_typecache(cast_on, blacklisted_mobs))
+ to_chat(owner, span_warning("This creature is too [pick("powerful", "strange", "arcane", "obscene")] to control!"))
return FALSE
+ if(isguardian(cast_on))
+ var/mob/living/simple_animal/hostile/guardian/stand = cast_on
+ if(stand.summoner && stand.summoner == owner)
+ to_chat(owner, span_warning("Swapping minds with your own guardian would just put you back into your own head!"))
+ return FALSE
- var/mob/living/victim = target
- var/t_He = victim.p_they(TRUE)
-
- if(ismegafauna(victim))
- if(!silent)
- to_chat(user, span_warning("This creature is too powerful to control!"))
- return FALSE
- if(victim.stat == DEAD)
- if(!silent)
- to_chat(user, span_warning("You don't particularly want to be dead!"))
+ var/mob/living/living_target = cast_on
+ if(living_target.stat == DEAD)
+ to_chat(owner, span_warning("You don't particularly want to be dead!"))
return FALSE
- if(!victim.key || !victim.mind)
- if(!silent)
- to_chat(user, span_warning("[t_He] appear[victim.p_s()] to be catatonic! Not even magic can affect [victim.p_their()] vacant mind."))
+ if(!living_target.mind)
+ to_chat(owner, span_warning("[living_target.p_theyve(TRUE)] doesn't appear to have a mind to swap into!"))
return FALSE
- if(user.suiciding)
- if(!silent)
- to_chat(user, span_warning("You're killing yourself! You can't concentrate enough to do this!"))
+ if(!living_target.key && target_requires_key)
+ to_chat(owner, span_warning("[living_target.p_theyve(TRUE)] appear[living_target.p_s()] to be catatonic! \
+ Not even magic can affect [living_target.p_their()] vacant mind."))
return FALSE
- if(istype(victim, /mob/living/simple_animal/hostile/guardian))
- var/mob/living/simple_animal/hostile/guardian/stand = victim
+
+ return TRUE
+
+/datum/action/cooldown/spell/pointed/mind_transfer/cast(mob/living/cast_on)
+ . = ..()
+ swap_minds(owner, cast_on)
+
+/datum/action/cooldown/spell/pointed/mind_transfer/proc/swap_minds(mob/living/caster, mob/living/cast_on)
+
+ var/mob/living/to_swap = cast_on
+ if(isguardian(cast_on))
+ var/mob/living/simple_animal/hostile/guardian/stand = cast_on
if(stand.summoner)
- if(stand.summoner == user)
- if(!silent)
- to_chat(user, span_warning("Swapping minds with your own guardian would just put you back into your own head!"))
- return FALSE
+ to_swap = stand.summoner
+
+ var/datum/mind/mind_to_swap = to_swap.mind
+ if(to_swap.can_block_magic(antimagic_flags) \
+ || mind_to_swap.has_antag_datum(/datum/antagonist/wizard) \
+ || mind_to_swap.has_antag_datum(/datum/antagonist/cult) \
+ || mind_to_swap.has_antag_datum(/datum/antagonist/changeling) \
+ || mind_to_swap.has_antag_datum(/datum/antagonist/rev) \
+ || mind_to_swap.key?[1] == "@" \
+ )
+ to_chat(caster, span_warning("[to_swap.p_their(TRUE)] mind is resisting your spell!"))
+ return FALSE
+
+ // MIND TRANSFER BEGIN
+
+ var/datum/mind/caster_mind = caster.mind
+ var/datum/mind/to_swap_mind = to_swap.mind
+
+ var/to_swap_key = to_swap.key
+
+ caster_mind.transfer_to(to_swap)
+ to_swap_mind.transfer_to(caster)
+
+ // Just in case the swappee's key wasn't grabbed by transfer_to...
+ if(to_swap_key)
+ caster.key = to_swap_key
+
+ // MIND TRANSFER END
+
+ // Now we knock both mobs out for a time.
+ caster.Unconscious(unconscious_amount_caster)
+ to_swap.Unconscious(unconscious_amount_victim)
+
+ // Only the caster and victim hear the sounds,
+ // that way no one knows for sure if the swap happened
+ SEND_SOUND(caster, sound('sound/magic/mandswap.ogg'))
+ SEND_SOUND(to_swap, sound('sound/magic/mandswap.ogg'))
+
return TRUE
diff --git a/code/modules/spells/spell_types/pointed/pointed.dm b/code/modules/spells/spell_types/pointed/pointed.dm
deleted file mode 100644
index 94676ba64acd2..0000000000000
--- a/code/modules/spells/spell_types/pointed/pointed.dm
+++ /dev/null
@@ -1,104 +0,0 @@
-/obj/effect/proc_holder/spell/pointed
- name = "pointed spell"
- ranged_mousepointer = 'icons/effects/mouse_pointers/throw_target.dmi'
- action_icon_state = "projectile"
- /// Message showing to the spell owner upon deactivating pointed spell.
- var/deactive_msg = "You dispel the magic..."
- /// Message showing to the spell owner upon activating pointed spell.
- var/active_msg = "You prepare to use the spell on a target..."
- /// Variable dictating if the user is allowed to cast a spell on himself.
- var/self_castable = FALSE
- /// Variable dictating if the spell will use turf based aim assist
- var/aim_assist = TRUE
-
-/obj/effect/proc_holder/spell/pointed/Click()
- var/mob/living/user = usr
- if(!istype(user))
- return
- var/msg
- if(!can_cast(user))
- msg = span_warning("You can no longer cast [name]!")
- remove_ranged_ability(msg)
- return
- if(active)
- msg = span_notice("[deactive_msg]")
- remove_ranged_ability(msg)
- else
- msg = span_notice("[active_msg] Left-click to activate spell on a target!")
- add_ranged_ability(user, msg, TRUE)
-
-/obj/effect/proc_holder/spell/pointed/on_lose(mob/living/user)
- remove_ranged_ability()
-
-/obj/effect/proc_holder/spell/pointed/remove_ranged_ability(msg)
- . = ..()
- on_deactivation(ranged_ability_user)
-
-/obj/effect/proc_holder/spell/pointed/add_ranged_ability(mob/living/user, msg, forced)
- . = ..()
- on_activation(user)
-
-/**
- * on_activation: What happens upon pointed spell activation.
- *
- * Arguments:
- * * user The mob interacting owning the spell.
- */
-/obj/effect/proc_holder/spell/pointed/proc/on_activation(mob/user)
- return
-
-/**
- * on_activation: What happens upon pointed spell deactivation.
- *
- * Arguments:
- * * user The mob interacting owning the spell.
- */
-/obj/effect/proc_holder/spell/pointed/proc/on_deactivation(mob/user)
- return
-
-/obj/effect/proc_holder/spell/pointed/update_icon()
- if(!action)
- return
-
- . = ..()
- action.button_icon_state = "[action_icon_state][active ? 1 : null]"
- action.UpdateButtons()
-
-/obj/effect/proc_holder/spell/pointed/InterceptClickOn(mob/living/caller, params, atom/target)
- if(..())
- return TRUE
- if(aim_assist && isturf(target))
- var/list/possible_targets = list()
- for(var/A in target)
- if(intercept_check(caller, A, TRUE))
- possible_targets += A
- if(possible_targets.len == 1)
- target = possible_targets[1]
- if(!intercept_check(caller, target))
- return TRUE
- if(!cast_check(FALSE, caller))
- return TRUE
- perform(list(target), user = caller)
- remove_ranged_ability()
- return TRUE // Do not do any underlying actions after the spell cast
-
-/**
- * intercept_check: Specific spell checks for InterceptClickOn() targets.
- *
- * Arguments:
- * * user The mob using the ranged spell via intercept.
- * * target The atom that is being targeted by the spell via intercept.
- * * silent If the checks should produce not any feedback messages for the user.
- */
-/obj/effect/proc_holder/spell/pointed/proc/intercept_check(mob/user, atom/target, silent = FALSE)
- if(!self_castable && target == user)
- if(!silent)
- to_chat(user, span_warning("You cannot cast the spell on yourself!"))
- return FALSE
- if(!(target in view_or_range(range, user, selection_type)))
- if(!silent)
- to_chat(user, span_warning("[target.p_theyre(TRUE)] too far away!"))
- return FALSE
- if(!can_target(target, user, silent))
- return FALSE
- return TRUE
diff --git a/code/modules/spells/spell_types/pointed/spell_cards.dm b/code/modules/spells/spell_types/pointed/spell_cards.dm
new file mode 100644
index 0000000000000..4b6af520517cc
--- /dev/null
+++ b/code/modules/spells/spell_types/pointed/spell_cards.dm
@@ -0,0 +1,83 @@
+
+/datum/action/cooldown/spell/pointed/projectile/spell_cards
+ name = "Spell Cards"
+ desc = "Blazing hot rapid-fire homing cards. Send your foes to the shadow realm with their mystical power!"
+ button_icon_state = "spellcard0"
+ click_cd_override = 1
+
+ school = SCHOOL_EVOCATION
+ cooldown_time = 5 SECONDS
+ cooldown_reduction_per_rank = 1 SECONDS
+
+ invocation = "Sigi'lu M'Fan 'Tasia!"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+ base_icon_state = "spellcard"
+ cast_range = 40
+ projectile_type = /obj/projectile/magic/spellcard
+ projectile_amount = 5
+ projectiles_per_fire = 7
+
+ /// A weakref to the mob we're currently targeting with the lockon component.
+ var/datum/weakref/current_target_weakref
+ /// The turn rate of the spell cards in flight. (They track onto locked on targets)
+ var/projectile_turnrate = 10
+ /// The homing spread of the spell cards in flight.
+ var/projectile_pixel_homing_spread = 32
+ /// The initial spread of the spell cards when fired.
+ var/projectile_initial_spread_amount = 30
+ /// The location spread of the spell cards when fired.
+ var/projectile_location_spread_amount = 12
+ /// A ref to our lockon component, which is created and destroyed on activation and deactivation.
+ var/datum/component/lockon_aiming/lockon_component
+
+/datum/action/cooldown/spell/pointed/projectile/spell_cards/Destroy()
+ QDEL_NULL(lockon_component)
+ return ..()
+
+/datum/action/cooldown/spell/pointed/projectile/spell_cards/on_activation(mob/on_who)
+ . = ..()
+ if(!.)
+ return
+
+ QDEL_NULL(lockon_component)
+ lockon_component = owner.AddComponent( \
+ /datum/component/lockon_aiming, \
+ range = 5, \
+ typecache = GLOB.typecache_living, \
+ amount = 1, \
+ when_locked = CALLBACK(src, .proc/on_lockon_component))
+
+/datum/action/cooldown/spell/pointed/projectile/spell_cards/proc/on_lockon_component(list/locked_weakrefs)
+ if(!length(locked_weakrefs))
+ current_target_weakref = null
+ return
+ current_target_weakref = locked_weakrefs[1]
+ var/atom/real_target = current_target_weakref.resolve()
+ if(real_target)
+ owner.face_atom(real_target)
+
+/datum/action/cooldown/spell/pointed/projectile/spell_cards/on_deactivation(mob/on_who, refund_cooldown = TRUE)
+ . = ..()
+ QDEL_NULL(lockon_component)
+
+/datum/action/cooldown/spell/pointed/projectile/spell_cards/ready_projectile(obj/projectile/to_fire, atom/target, mob/user, iteration)
+ . = ..()
+ if(current_target_weakref)
+ var/atom/real_target = current_target_weakref?.resolve()
+ if(real_target && get_dist(real_target, user) < 7)
+ to_fire.homing_turn_speed = projectile_turnrate
+ to_fire.homing_inaccuracy_min = projectile_pixel_homing_spread
+ to_fire.homing_inaccuracy_max = projectile_pixel_homing_spread
+ to_fire.set_homing_target(real_target)
+
+ var/rand_spr = rand()
+ var/total_angle = projectile_initial_spread_amount * 2
+ var/adjusted_angle = total_angle - ((projectile_initial_spread_amount / projectiles_per_fire) * 0.5)
+ var/one_fire_angle = adjusted_angle / projectiles_per_fire
+ var/current_angle = iteration * one_fire_angle * rand_spr - (projectile_initial_spread_amount / 2)
+
+ to_fire.pixel_x = rand(-projectile_location_spread_amount, projectile_location_spread_amount)
+ to_fire.pixel_y = rand(-projectile_location_spread_amount, projectile_location_spread_amount)
+ to_fire.preparePixelProjectile(target, user, null, current_angle)
diff --git a/code/modules/spells/spell_types/projectile.dm b/code/modules/spells/spell_types/projectile.dm
deleted file mode 100644
index 8d3b0b7f014a4..0000000000000
--- a/code/modules/spells/spell_types/projectile.dm
+++ /dev/null
@@ -1,112 +0,0 @@
-/obj/projectile/magic/spell
- name = "custom spell projectile"
- var/trigger_range = 0 //How far we do we need to be to hit
- var/linger = FALSE //Can't hit anything but the intended target
-
- var/trail = FALSE //if it leaves a trail
- var/trail_lifespan = 0 //deciseconds
- var/trail_icon = 'icons/obj/wizard.dmi'
- var/trail_icon_state = "trail"
-
-//todo unify this and magic/aoe under common path
-/obj/projectile/magic/spell/Range()
- if(trigger_range > 1)
- for(var/mob/living/L in range(trigger_range, get_turf(src)))
- if(can_hit_target(L, ignore_loc = TRUE))
- return Bump(L)
- . = ..()
-
-/obj/projectile/magic/spell/Moved(atom/OldLoc, Dir)
- . = ..()
- if(trail)
- create_trail()
-
-/obj/projectile/magic/spell/proc/create_trail()
- if(!trajectory)
- return
- var/datum/point/vector/previous = trajectory.return_vector_after_increments(1,-1)
- var/obj/effect/overlay/trail = new /obj/effect/overlay(previous.return_turf())
- trail.pixel_x = previous.return_px()
- trail.pixel_y = previous.return_py()
- trail.icon = trail_icon
- trail.icon_state = trail_icon_state
- //might be changed to temp overlay
- trail.set_density(FALSE)
- trail.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
- QDEL_IN(trail, trail_lifespan)
-
-/obj/projectile/magic/spell/can_hit_target(atom/target, list/passthrough, direct_target = FALSE, ignore_loc = FALSE)
- if(linger && target != original)
- return FALSE
- return ..()
-
-//NEEDS MAJOR CODE CLEANUP.
-
-/obj/effect/proc_holder/spell/targeted/projectile
- name = "Projectile"
- desc = "This spell summons projectiles which try to hit the targets."
- antimagic_flags = MAGIC_RESISTANCE
- var/proj_type = /obj/projectile/magic/spell //IMPORTANT use only subtypes of this
- var/update_projectile = FALSE //So you want to admin abuse magic bullets ? This is for you
- //Below only apply if update_projectile is true
- var/proj_icon = 'icons/obj/guns/projectiles.dmi'
- var/proj_icon_state = "spell"
- var/proj_name = "a spell projectile"
- var/proj_trail = FALSE //if it leaves a trail
- var/proj_trail_lifespan = 0 //deciseconds
- var/proj_trail_icon = 'icons/obj/wizard.dmi'
- var/proj_trail_icon_state = "trail"
- var/proj_lingering = FALSE //if it lingers or disappears upon hitting an obstacle
- var/proj_homing = TRUE //if it follows the target
- var/proj_insubstantial = FALSE //if it can pass through dense objects or not
- var/proj_trigger_range = 0 //the range from target at which the projectile triggers cast(target)
- var/proj_lifespan = 15 //in deciseconds * proj_step_delay
- var/proj_step_delay = 1 //lower = faster
- var/list/ignore_factions = list() //Faction types that will be ignored
-
-/obj/effect/proc_holder/spell/targeted/projectile/proc/fire_projectile(atom/target, mob/user)
- var/obj/projectile/magic/spell/projectile = new proj_type()
-
- if(update_projectile)
- //Generally these should already be set on the projectile, this is mostly here for varedited spells.
- projectile.icon = proj_icon
- projectile.icon_state = proj_icon_state
- projectile.name = proj_name
- if(proj_insubstantial)
- projectile.movement_type |= PHASING
- if(proj_homing)
- projectile.homing = TRUE
- projectile.homing_turn_speed = 360 //Perfect tracking
- if(proj_lingering)
- projectile.linger = TRUE
- projectile.trigger_range = proj_trigger_range
- projectile.ignored_factions = ignore_factions
- projectile.range = proj_lifespan
- projectile.speed = proj_step_delay
- projectile.trail = proj_trail
- projectile.trail_lifespan = proj_trail_lifespan
- projectile.trail_icon = proj_trail_icon
- projectile.trail_icon_state = proj_trail_icon_state
-
- projectile.preparePixelProjectile(target,user)
- if(projectile.homing)
- projectile.set_homing_target(target)
- projectile.fire()
-
-/obj/effect/proc_holder/spell/targeted/projectile/cast(list/targets, mob/user = usr)
- playMagSound()
- for(var/atom/target in targets)
- fire_projectile(target, user)
-
-//This one just pops one projectile in direction user is facing, irrelevant of max_targets etc
-/obj/effect/proc_holder/spell/targeted/projectile/dumbfire
- name = "Dumbfire projectile"
-
-/obj/effect/proc_holder/spell/targeted/projectile/dumbfire/choose_targets(mob/user = usr)
- var/turf/T = get_turf(user)
- for(var/i in 1 to range-1)
- var/turf/new_turf = get_step(T, user.dir)
- if(new_turf.density)
- break
- T = new_turf
- perform(list(T),user = user)
diff --git a/code/modules/spells/spell_types/projectile/_basic_projectile.dm b/code/modules/spells/spell_types/projectile/_basic_projectile.dm
new file mode 100644
index 0000000000000..f9bd303f56f1d
--- /dev/null
+++ b/code/modules/spells/spell_types/projectile/_basic_projectile.dm
@@ -0,0 +1,29 @@
+/**
+ * ## Basic Projectile spell
+ *
+ * Simply fires specified projectile type the direction the caster is facing.
+ *
+ * Behavior could / should probably be unified with pointed projectile spells
+ * and aoe projectile spells in the future.
+ */
+/datum/action/cooldown/spell/basic_projectile
+ /// How far we try to fire the basic projectile. Blocked by dense objects.
+ var/projectile_range = 7
+ /// The projectile type fired at all people around us
+ var/obj/projectile/projectile_type = /obj/projectile/magic/aoe/magic_missile
+
+/datum/action/cooldown/spell/basic_projectile/cast(atom/cast_on)
+ . = ..()
+ var/turf/target_turf = get_turf(cast_on)
+ for(var/i in 1 to projectile_range - 1)
+ var/turf/next_turf = get_step(target_turf, cast_on.dir)
+ if(next_turf.density)
+ break
+ target_turf = next_turf
+
+ fire_projectile(target_turf, cast_on)
+
+/datum/action/cooldown/spell/basic_projectile/proc/fire_projectile(atom/target, atom/caster)
+ var/obj/projectile/to_fire = new projectile_type()
+ to_fire.preparePixelProjectile(target, caster)
+ to_fire.fire()
diff --git a/code/modules/spells/spell_types/projectile/juggernaut.dm b/code/modules/spells/spell_types/projectile/juggernaut.dm
new file mode 100644
index 0000000000000..443c9cf62e5cd
--- /dev/null
+++ b/code/modules/spells/spell_types/projectile/juggernaut.dm
@@ -0,0 +1,12 @@
+/datum/action/cooldown/spell/basic_projectile/juggernaut
+ name = "Gauntlet Echo"
+ desc = "Channels energy into your gauntlet - firing its essence forward in a slow moving, yet devastating, attack."
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "cultfist"
+ background_icon_state = "bg_demon"
+ sound = 'sound/weapons/resonator_blast.ogg'
+
+ cooldown_time = 35 SECONDS
+ spell_requirements = NONE
+
+ projectile_type = /obj/projectile/magic/aoe/juggernaut
diff --git a/code/modules/spells/spell_types/rightandwrong.dm b/code/modules/spells/spell_types/right_and_wrong.dm
similarity index 96%
rename from code/modules/spells/spell_types/rightandwrong.dm
rename to code/modules/spells/spell_types/right_and_wrong.dm
index 0db7cbe8cf47d..211a4a9414ff0 100644
--- a/code/modules/spells/spell_types/rightandwrong.dm
+++ b/code/modules/spells/spell_types/right_and_wrong.dm
@@ -57,15 +57,15 @@ GLOBAL_LIST_INIT(summoned_guns, list(
//if you add anything that isn't covered by the typepaths below, add it to summon_magic_objective_types
GLOBAL_LIST_INIT(summoned_magic, list(
- /obj/item/book/granter/spell/fireball,
- /obj/item/book/granter/spell/smoke,
- /obj/item/book/granter/spell/blind,
- /obj/item/book/granter/spell/mindswap,
- /obj/item/book/granter/spell/forcewall,
- /obj/item/book/granter/spell/knock,
- /obj/item/book/granter/spell/barnyard,
- /obj/item/book/granter/spell/charge,
- /obj/item/book/granter/spell/summonitem,
+ /obj/item/book/granter/action/spell/fireball,
+ /obj/item/book/granter/action/spell/smoke,
+ /obj/item/book/granter/action/spell/blind,
+ /obj/item/book/granter/action/spell/mindswap,
+ /obj/item/book/granter/action/spell/forcewall,
+ /obj/item/book/granter/action/spell/knock,
+ /obj/item/book/granter/action/spell/barnyard,
+ /obj/item/book/granter/action/spell/charge,
+ /obj/item/book/granter/action/spell/summonitem,
/obj/item/gun/magic/wand/nothing,
/obj/item/gun/magic/wand/death,
/obj/item/gun/magic/wand/resurrection,
diff --git a/code/modules/spells/spell_types/santa.dm b/code/modules/spells/spell_types/santa.dm
deleted file mode 100644
index 6a41c95cbdb34..0000000000000
--- a/code/modules/spells/spell_types/santa.dm
+++ /dev/null
@@ -1,24 +0,0 @@
-//Santa spells!
-/obj/effect/proc_holder/spell/aoe_turf/conjure/presents
- name = "Conjure Presents!"
- desc = "This spell lets you reach into S-space and retrieve presents! Yay!"
- school = SCHOOL_CONJURATION
- charge_max = 600
- clothes_req = FALSE
- invocation = "HO HO HO"
- invocation_type = INVOCATION_SHOUT
- range = 3
- cooldown_min = 50
- antimagic_flags = NONE
-
- summon_type = list("/obj/item/a_gift")
- summon_lifespan = 0
- summon_amt = 5
-
-/obj/effect/proc_holder/spell/targeted/area_teleport/teleport/santa
- name = "Santa Teleport"
-
- invocation = "HO HO HO"
- clothes_req = FALSE
- say_destination = FALSE // Santa moves in mysterious ways
- antimagic_flags = NONE
diff --git a/code/modules/spells/spell_types/self/basic_heal.dm b/code/modules/spells/spell_types/self/basic_heal.dm
new file mode 100644
index 0000000000000..a4acba2d88451
--- /dev/null
+++ b/code/modules/spells/spell_types/self/basic_heal.dm
@@ -0,0 +1,27 @@
+// This spell exists mainly for debugging purposes, and also to show how casting works
+/datum/action/cooldown/spell/basic_heal
+ name = "Lesser Heal"
+ desc = "Heals a small amount of brute and burn damage to the caster."
+
+ sound = 'sound/magic/staff_healing.ogg'
+ school = SCHOOL_RESTORATION
+ cooldown_time = 10 SECONDS
+ cooldown_reduction_per_rank = 1.25 SECONDS
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_HUMAN
+
+ invocation = "Victus sano!"
+ invocation_type = INVOCATION_WHISPER
+
+ /// Amount of brute to heal to the spell caster on cast
+ var/brute_to_heal = 10
+ /// Amount of burn to heal to the spell caster on cast
+ var/burn_to_heal = 10
+
+/datum/action/cooldown/spell/basic_heal/cast(mob/living/cast_on)
+ . = ..()
+ cast_on.visible_message(
+ span_warning("A wreath of gentle light passes over [cast_on]!"),
+ span_notice("You wreath yourself in healing light!"),
+ )
+ cast_on.adjustBruteLoss(-brute_to_heal, FALSE)
+ cast_on.adjustFireLoss(-burn_to_heal)
diff --git a/code/modules/spells/spell_types/self/charge.dm b/code/modules/spells/spell_types/self/charge.dm
new file mode 100644
index 0000000000000..87d7ae287d337
--- /dev/null
+++ b/code/modules/spells/spell_types/self/charge.dm
@@ -0,0 +1,58 @@
+/datum/action/cooldown/spell/charge
+ name = "Charge"
+ desc = "This spell can be used to recharge a variety of things in your hands, \
+ from magical artifacts to electrical components. A creative wizard can even use it \
+ to grant magical power to a fellow magic user."
+ button_icon_state = "charge"
+
+ sound = 'sound/magic/charge.ogg'
+ school = SCHOOL_TRANSMUTATION
+ cooldown_time = 60 SECONDS
+ cooldown_reduction_per_rank = 5 SECONDS
+
+ invocation = "DIRI CEL"
+ invocation_type = INVOCATION_WHISPER
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+/datum/action/cooldown/spell/charge/is_valid_target(atom/cast_on)
+ return isliving(cast_on)
+
+/datum/action/cooldown/spell/charge/cast(mob/living/cast_on)
+ . = ..()
+
+ // Charge people we're pulling first and foremost
+ if(isliving(cast_on.pulling))
+ var/mob/living/pulled_living = cast_on.pulling
+ var/pulled_has_spells = FALSE
+
+ for(var/datum/action/cooldown/spell/spell in pulled_living.actions)
+ spell.reset_spell_cooldown()
+ pulled_has_spells = TRUE
+
+ if(pulled_has_spells)
+ to_chat(pulled_living, span_notice("You feel raw magic flowing through you. It feels good!"))
+ to_chat(cast_on, span_notice("[pulled_living] suddenly feels very warm!"))
+ return
+
+ to_chat(pulled_living, span_notice("You feel very strange for a moment, but then it passes."))
+
+ // Then charge their main hand item, then charge their offhand item
+ var/obj/item/to_charge = cast_on.get_active_held_item() || cast_on.get_inactive_held_item()
+ if(!to_charge)
+ to_chat(cast_on, span_notice("You feel magical power surging through your hands, but the feeling rapidly fades."))
+ return
+
+ var/charge_return = SEND_SIGNAL(to_charge, COMSIG_ITEM_MAGICALLY_CHARGED, src, cast_on)
+
+ if(QDELETED(to_charge))
+ to_chat(cast_on, span_warning("[src] seems to react adversely with [to_charge]!"))
+ return
+
+ if(charge_return & COMPONENT_ITEM_BURNT_OUT)
+ to_chat(cast_on, span_warning("[to_charge] seems to react negatively to [src], becoming uncomfortably warm!"))
+
+ else if(charge_return & COMPONENT_ITEM_CHARGED)
+ to_chat(cast_on, span_notice("[to_charge] suddenly feels very warm!"))
+
+ else
+ to_chat(cast_on, span_notice("[to_charge] doesn't seem to be react to [src]."))
diff --git a/code/modules/spells/spell_types/self/disable_tech.dm b/code/modules/spells/spell_types/self/disable_tech.dm
new file mode 100644
index 0000000000000..543daa467791e
--- /dev/null
+++ b/code/modules/spells/spell_types/self/disable_tech.dm
@@ -0,0 +1,30 @@
+/datum/action/cooldown/spell/emp
+ name = "Emplosion"
+ desc = "This spell emplodes an area."
+ button_icon_state = "emp"
+ sound = 'sound/weapons/zapbang.ogg'
+
+ school = SCHOOL_EVOCATION
+
+ /// The heavy radius of the EMP
+ var/emp_heavy = 2
+ /// The light radius of the EMP
+ var/emp_light = 3
+
+/datum/action/cooldown/spell/emp/cast(atom/cast_on)
+ . = ..()
+ empulse(get_turf(cast_on), emp_heavy, emp_light)
+
+/datum/action/cooldown/spell/emp/disable_tech
+ name = "Disable Tech"
+ desc = "This spell disables all weapons, cameras and most other technology in range."
+ sound = 'sound/magic/disable_tech.ogg'
+
+ cooldown_time = 40 SECONDS
+ cooldown_reduction_per_rank = 5 SECONDS
+
+ invocation = "NEC CANTIO"
+ invocation_type = INVOCATION_SHOUT
+
+ emp_heavy = 6
+ emp_light = 10
diff --git a/code/modules/spells/spell_types/self/forcewall.dm b/code/modules/spells/spell_types/self/forcewall.dm
new file mode 100644
index 0000000000000..e037c1ae689d9
--- /dev/null
+++ b/code/modules/spells/spell_types/self/forcewall.dm
@@ -0,0 +1,66 @@
+/datum/action/cooldown/spell/forcewall
+ name = "Forcewall"
+ desc = "Create a magical barrier that only you can pass through."
+ button_icon_state = "shield"
+
+ sound = 'sound/magic/forcewall.ogg'
+ school = SCHOOL_TRANSMUTATION
+ cooldown_time = 10 SECONDS
+ cooldown_reduction_per_rank = 1.25 SECONDS
+
+ invocation = "TARCOL MINTI ZHERI"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+ /// The typepath to the wall we create on cast.
+ var/wall_type = /obj/effect/forcefield/wizard
+
+/datum/action/cooldown/spell/forcewall/cast(atom/cast_on)
+ . = ..()
+ new wall_type(get_turf(owner), owner)
+
+ if(owner.dir == SOUTH || owner.dir == NORTH)
+ new wall_type(get_step(owner, EAST), owner, antimagic_flags)
+ new wall_type(get_step(owner, WEST), owner, antimagic_flags)
+
+ else
+ new wall_type(get_step(owner, NORTH), owner, antimagic_flags)
+ new wall_type(get_step(owner, SOUTH), owner, antimagic_flags)
+
+/datum/action/cooldown/spell/forcewall/cult
+ name = "Shield"
+ desc = "This spell creates a temporary forcefield to shield yourself and allies from incoming fire."
+ background_icon_state = "bg_demon"
+ icon_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "cultforcewall"
+
+ cooldown_time = 40 SECONDS
+ invocation_type = INVOCATION_NONE
+
+ wall_type = /obj/effect/forcefield/cult
+
+/datum/action/cooldown/spell/forcewall/mime
+ name = "Invisible Blockade"
+ desc = "Form an invisible three tile wide blockade."
+ background_icon_state = "bg_mime"
+ icon_icon = 'icons/mob/actions/actions_mime.dmi'
+ button_icon_state = "invisible_blockade"
+ panel = "Mime"
+ sound = null
+
+ school = SCHOOL_MIME
+ cooldown_time = 1 MINUTES
+ cooldown_reduction_per_rank = 0 SECONDS
+ spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIME_VOW
+ antimagic_flags = NONE
+
+ invocation = ""
+ invocation_type = INVOCATION_EMOTE
+ invocation_self_message = span_notice("You form a blockade in front of yourself.")
+ spell_max_level = 1
+
+ wall_type = /obj/effect/forcefield/mime/advanced
+
+/datum/action/cooldown/spell/forcewall/mime/before_cast(atom/cast_on)
+ . = ..()
+ invocation = span_notice("[cast_on] looks as if a blockade is in front of [cast_on.p_them()].")
diff --git a/code/modules/spells/spell_types/self/lichdom.dm b/code/modules/spells/spell_types/self/lichdom.dm
new file mode 100644
index 0000000000000..69325f9df97ab
--- /dev/null
+++ b/code/modules/spells/spell_types/self/lichdom.dm
@@ -0,0 +1,83 @@
+/datum/action/cooldown/spell/lichdom
+ name = "Bind Soul"
+ desc = "A spell that binds your soul to an item in your hands. \
+ Binding your soul to an item will turn you into an immortal Lich. \
+ So long as the item remains intact, you will revive from death, \
+ no matter the circumstances."
+ icon_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "skeleton"
+
+ school = SCHOOL_NECROMANCY
+ cooldown_time = 1 SECONDS
+
+ invocation = "NECREM IMORTIUM!"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_OFF_CENTCOM|SPELL_REQUIRES_MIND
+ spell_max_level = 1
+
+/datum/action/cooldown/spell/lichdom/can_cast_spell(feedback = TRUE)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ // We call this here so we can get feedback if they try to cast it when they shouldn't.
+ if(!is_valid_target(owner))
+ if(feedback)
+ to_chat(owner, span_warning("You don't have a soul to bind!"))
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/spell/lichdom/is_valid_target(atom/cast_on)
+ return isliving(cast_on) && !HAS_TRAIT(owner, TRAIT_NO_SOUL)
+
+/datum/action/cooldown/spell/lichdom/cast(mob/living/cast_on)
+ var/obj/item/marked_item = cast_on.get_active_held_item()
+ if(!marked_item || marked_item.item_flags & ABSTRACT)
+ return
+ if(HAS_TRAIT(marked_item, TRAIT_NODROP))
+ to_chat(cast_on, span_warning("[marked_item] is stuck to your hand - it wouldn't be a wise idea to place your soul into it."))
+ return
+ // I ensouled the nuke disk once.
+ // But it's a really mean tactic, so we probably should disallow it.
+ if(SEND_SIGNAL(marked_item, COMSIG_ITEM_IMBUE_SOUL, src, cast_on) & COMPONENT_BLOCK_IMBUE)
+ to_chat(cast_on, span_warning("[marked_item] is not suitable for emplacement of your fragile soul."))
+ return
+
+ . = ..()
+ playsound(cast_on, 'sound/effects/pope_entry.ogg', 100)
+
+ to_chat(cast_on, span_green("You begin to focus your very being into [marked_item]..."))
+ if(!do_after(cast_on, 5 SECONDS, target = marked_item, timed_action_flags = IGNORE_HELD_ITEM))
+ to_chat(cast_on, span_warning("Your soul snaps back to your body as you stop ensouling [marked_item]!"))
+ return
+
+ marked_item.AddComponent(/datum/component/phylactery, cast_on.mind)
+
+ cast_on.set_species(/datum/species/skeleton)
+ to_chat(cast_on, span_userdanger("With a hideous feeling of emptiness you watch in horrified fascination \
+ as skin sloughs off bone! Blood boils, nerves disintegrate, eyes boil in their sockets! \
+ As your organs crumble to dust in your fleshless chest you come to terms with your choice. \
+ You're a lich!"))
+
+ if(iscarbon(cast_on))
+ var/mob/living/carbon/carbon_cast_on = cast_on
+ var/obj/item/organ/internal/brain/lich_brain = carbon_cast_on.getorganslot(ORGAN_SLOT_BRAIN)
+ if(lich_brain) // This prevents MMIs being used to stop lich revives
+ lich_brain.organ_flags &= ~ORGAN_VITAL
+ lich_brain.decoy_override = TRUE
+
+ if(ishuman(cast_on))
+ var/mob/living/carbon/human/human_cast_on = cast_on
+ human_cast_on.dropItemToGround(human_cast_on.w_uniform)
+ human_cast_on.dropItemToGround(human_cast_on.wear_suit)
+ human_cast_on.dropItemToGround(human_cast_on.head)
+ human_cast_on.equip_to_slot_or_del(new /obj/item/clothing/suit/wizrobe/black(human_cast_on), ITEM_SLOT_OCLOTHING)
+ human_cast_on.equip_to_slot_or_del(new /obj/item/clothing/head/wizard/black(human_cast_on), ITEM_SLOT_HEAD)
+ human_cast_on.equip_to_slot_or_del(new /obj/item/clothing/under/color/black(human_cast_on), ITEM_SLOT_ICLOTHING)
+
+
+ // No soul. You just sold it
+ ADD_TRAIT(cast_on, TRAIT_NO_SOUL, LICH_TRAIT)
+ // You only get one phylactery.
+ qdel(src)
diff --git a/code/modules/spells/spell_types/self/lightning.dm b/code/modules/spells/spell_types/self/lightning.dm
new file mode 100644
index 0000000000000..7423fb8a374a6
--- /dev/null
+++ b/code/modules/spells/spell_types/self/lightning.dm
@@ -0,0 +1,128 @@
+/datum/action/cooldown/spell/tesla
+ name = "Tesla Blast"
+ desc = "Charge up a tesla arc and release it at random nearby targets! \
+ You can move freely while it charges. The arc jumps between targets and can knock them down."
+ button_icon_state = "lightning"
+
+ cooldown_time = 30 SECONDS
+ cooldown_reduction_per_rank = 6.75 SECONDS
+
+ invocation = "UN'LTD P'WAH!"
+ invocation_type = INVOCATION_SHOUT
+ school = SCHOOL_EVOCATION
+
+ /// Whether we're currently channelling a tesla blast or not
+ var/currently_channeling = FALSE
+ /// How long it takes to channel the zap.
+ var/channel_time = 10 SECONDS
+ /// The radius around (either the caster or people shocked) to which the tesla blast can reach
+ var/shock_radius = 7
+ /// The halo that appears around the caster while charging the spell
+ var/static/mutable_appearance/halo
+ /// The sound played while charging the spell
+ /// Quote: "the only way i can think of to stop a sound, thank MSO for the idea."
+ var/sound/charge_sound
+
+/datum/action/cooldown/spell/tesla/Remove(mob/living/remove_from)
+ reset_tesla(remove_from)
+ return ..()
+
+/datum/action/cooldown/spell/tesla/set_statpanel_format()
+ . = ..()
+ if(!islist(.))
+ return
+
+ if(currently_channeling)
+ .[PANEL_DISPLAY_STATUS] = "CHANNELING"
+
+/datum/action/cooldown/spell/tesla/can_cast_spell(feedback = TRUE)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(currently_channeling)
+ if(feedback)
+ to_chat(owner, span_warning("You're already channeling [src]!"))
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/spell/tesla/before_cast(atom/cast_on)
+ . = ..()
+ if(. & SPELL_CANCEL_CAST)
+ return
+
+ to_chat(cast_on, span_notice("You start gathering power..."))
+ charge_sound = new /sound('sound/magic/lightning_chargeup.ogg', channel = 7)
+ halo ||= mutable_appearance('icons/effects/effects.dmi', "electricity", EFFECTS_LAYER)
+ cast_on.add_overlay(halo)
+ playsound(get_turf(cast_on), charge_sound, 50, FALSE)
+
+ currently_channeling = TRUE
+ if(!do_after(cast_on, channel_time, timed_action_flags = (IGNORE_USER_LOC_CHANGE|IGNORE_HELD_ITEM)))
+ reset_tesla(cast_on)
+ return . | SPELL_CANCEL_CAST
+
+ return TRUE
+
+/datum/action/cooldown/spell/tesla/reset_spell_cooldown()
+ reset_tesla(owner)
+ return ..()
+
+/// Resets the tesla effect.
+/datum/action/cooldown/spell/tesla/proc/reset_tesla(atom/to_reset)
+ to_reset.cut_overlay(halo)
+ currently_channeling = FALSE
+
+/datum/action/cooldown/spell/tesla/cast(atom/cast_on)
+ . = ..()
+
+ // byond, why you suck?
+ charge_sound = sound(null, repeat = 0, wait = 1, channel = charge_sound.channel)
+ // Sorry MrPerson, but the other ways just didn't do it the way i needed to work, this is the only way.
+ playsound(get_turf(cast_on), charge_sound, 50, FALSE)
+
+ var/mob/living/carbon/to_zap_first = get_target(cast_on)
+ if(QDELETED(to_zap_first))
+ cast_on.balloon_alert(cast_on, "no targets nearby!")
+ reset_spell_cooldown()
+ return FALSE
+
+ playsound(get_turf(cast_on), 'sound/magic/lightningbolt.ogg', 50, TRUE)
+ zap_target(cast_on, to_zap_first)
+ reset_tesla(cast_on)
+ return TRUE
+
+/// Zaps a target, the bolt originating from origin.
+/datum/action/cooldown/spell/tesla/proc/zap_target(atom/origin, mob/living/carbon/to_zap, bolt_energy = 30, bounces = 5)
+ origin.Beam(to_zap, icon_state = "lightning[rand(1,12)]", time = 0.5 SECONDS)
+ playsound(get_turf(to_zap), 'sound/magic/lightningshock.ogg', 50, TRUE, -1)
+
+ if(to_zap.can_block_magic(antimagic_flags))
+ to_zap.visible_message(
+ span_warning("[to_zap] absorbs the spell, remaining unharmed!"),
+ span_userdanger("You absorb the spell, remaining unharmed!"),
+ )
+
+ else
+ to_zap.electrocute_act(bolt_energy, "Lightning Bolt", flags = SHOCK_NOGLOVES)
+
+ if(bounces >= 1)
+ var/mob/living/carbon/to_zap_next = get_target(to_zap)
+ if(!QDELETED(to_zap_next))
+ zap_target(to_zap, to_zap_next, max((bolt_energy - 5), 5), bounces - 1)
+
+/// Get a target in view of us to zap next. Returns a carbon, or null if none were found.
+/datum/action/cooldown/spell/tesla/proc/get_target(atom/center)
+ var/list/possibles = list()
+ for(var/mob/living/carbon/to_check in view(shock_radius, center))
+ if(to_check == center || to_check == owner)
+ continue
+ if(!length(get_path_to(center, to_check, max_distance = shock_radius, simulated_only = FALSE)))
+ continue
+
+ possibles += to_check
+
+ if(!length(possibles))
+ return null
+
+ return pick(possibles)
diff --git a/code/modules/spells/spell_types/self/mime_vow.dm b/code/modules/spells/spell_types/self/mime_vow.dm
new file mode 100644
index 0000000000000..553c9394f57f4
--- /dev/null
+++ b/code/modules/spells/spell_types/self/mime_vow.dm
@@ -0,0 +1,24 @@
+/datum/action/cooldown/spell/vow_of_silence
+ name = "Speech"
+ desc = "Make (or break) a vow of silence."
+ background_icon_state = "bg_mime"
+ icon_icon = 'icons/mob/actions/actions_mime.dmi'
+ button_icon_state = "mime_speech"
+ panel = "Mime"
+
+ school = SCHOOL_MIME
+ cooldown_time = 5 MINUTES
+
+ spell_requirements = SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_MIND
+ spell_max_level = 1
+
+/datum/action/cooldown/spell/vow_of_silence/cast(mob/living/carbon/human/cast_on)
+ . = ..()
+ cast_on.mind.miming = !cast_on.mind.miming
+ if(cast_on.mind.miming)
+ to_chat(cast_on, span_notice("You make a vow of silence."))
+ SEND_SIGNAL(cast_on, COMSIG_CLEAR_MOOD_EVENT, "vow")
+ else
+ to_chat(cast_on, span_notice("You break your vow of silence."))
+ SEND_SIGNAL(cast_on, COMSIG_ADD_MOOD_EVENT, "vow", /datum/mood_event/broken_vow)
+ cast_on.update_action_buttons_icon()
diff --git a/code/modules/spells/spell_types/self/mutate.dm b/code/modules/spells/spell_types/self/mutate.dm
new file mode 100644
index 0000000000000..0cc578809d655
--- /dev/null
+++ b/code/modules/spells/spell_types/self/mutate.dm
@@ -0,0 +1,49 @@
+/// A spell type that adds mutations to the caster temporarily.
+/datum/action/cooldown/spell/apply_mutations
+ button_icon_state = "mutate"
+ sound = 'sound/magic/mutate.ogg'
+
+ school = SCHOOL_TRANSMUTATION
+
+ /// A list of all mutations we add on cast
+ var/list/mutations_to_add = list()
+ /// The duration the mutations will last afetr cast (keep this above the minimum cooldown)
+ var/mutation_duration = 10 SECONDS
+
+/datum/action/cooldown/spell/apply_mutations/New(Target)
+ . = ..()
+ spell_requirements |= SPELL_REQUIRES_HUMAN // The spell involves mutations, so it always require human / dna
+
+/datum/action/cooldown/spell/apply_mutations/Remove(mob/living/remove_from)
+ remove_mutations(remove_from)
+ return ..()
+
+/datum/action/cooldown/spell/apply_mutations/is_valid_target(atom/cast_on)
+ var/mob/living/carbon/human/human_caster = cast_on // Requires human anyways
+ return !!human_caster.dna
+
+/datum/action/cooldown/spell/apply_mutations/cast(mob/living/carbon/human/cast_on)
+ . = ..()
+ for(var/mutation in mutations_to_add)
+ cast_on.dna.add_mutation(mutation)
+ addtimer(CALLBACK(src, .proc/remove_mutations, cast_on), mutation_duration, TIMER_DELETE_ME)
+
+/// Removes the mutations we added from casting our spell
+/datum/action/cooldown/spell/apply_mutations/proc/remove_mutations(mob/living/carbon/human/cast_on)
+ if(QDELETED(cast_on) || !is_valid_target(cast_on))
+ return
+
+ for(var/mutation in mutations_to_add)
+ cast_on.dna.remove_mutation(mutation)
+
+/datum/action/cooldown/spell/apply_mutations/mutate
+ name = "Mutate"
+ desc = "This spell causes you to turn into a hulk and gain laser vision for a short while."
+ cooldown_time = 40 SECONDS
+ cooldown_reduction_per_rank = 2.5 SECONDS
+
+ invocation = "BIRUZ BENNAR"
+ invocation_type = INVOCATION_SHOUT
+
+ mutations_to_add = list(/datum/mutation/human/laser_eyes, /datum/mutation/human/hulk)
+ mutation_duration = 30 SECONDS
diff --git a/code/modules/spells/spell_types/self/night_vision.dm b/code/modules/spells/spell_types/self/night_vision.dm
new file mode 100644
index 0000000000000..e7211aeb7a20e
--- /dev/null
+++ b/code/modules/spells/spell_types/self/night_vision.dm
@@ -0,0 +1,40 @@
+
+//Toggle Night Vision
+/datum/action/cooldown/spell/night_vision
+ name = "Toggle Nightvision"
+ desc = "Toggle your nightvision mode."
+
+ cooldown_time = 1 SECONDS
+ spell_requirements = NONE
+
+ /// The span the "toggle" message uses when sent to the user
+ var/toggle_span = "notice"
+
+/datum/action/cooldown/spell/night_vision/New(Target)
+ . = ..()
+ name = "[name] \[ON\]"
+
+/datum/action/cooldown/spell/night_vision/is_valid_target(atom/cast_on)
+ return isliving(cast_on)
+
+/datum/action/cooldown/spell/night_vision/cast(mob/living/cast_on)
+ . = ..()
+ to_chat(cast_on, "You toggle your night vision.")
+
+ var/next_mode_text = ""
+ switch(cast_on.lighting_alpha)
+ if (LIGHTING_PLANE_ALPHA_VISIBLE)
+ cast_on.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE
+ next_mode_text = "More"
+ if (LIGHTING_PLANE_ALPHA_MOSTLY_VISIBLE)
+ cast_on.lighting_alpha = LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE
+ next_mode_text = "Full"
+ if (LIGHTING_PLANE_ALPHA_MOSTLY_INVISIBLE)
+ cast_on.lighting_alpha = LIGHTING_PLANE_ALPHA_INVISIBLE
+ next_mode_text = "OFF"
+ else
+ cast_on.lighting_alpha = LIGHTING_PLANE_ALPHA_VISIBLE
+ next_mode_text = "ON"
+
+ cast_on.update_sight()
+ name = "[initial(name)] \[[next_mode_text]\]"
diff --git a/code/modules/spells/spell_types/self/personality_commune.dm b/code/modules/spells/spell_types/self/personality_commune.dm
new file mode 100644
index 0000000000000..67e794c966832
--- /dev/null
+++ b/code/modules/spells/spell_types/self/personality_commune.dm
@@ -0,0 +1,54 @@
+// This can probably be changed to use mind linker at some point
+/datum/action/cooldown/spell/personality_commune
+ name = "Personality Commune"
+ desc = "Sends thoughts to your alternate consciousness."
+ button_icon_state = "telepathy"
+ cooldown_time = 0 SECONDS
+ spell_requirements = NONE
+
+ /// Fluff text shown when a message is sent to the pair
+ var/fluff_text = span_boldnotice("You hear an echoing voice in the back of your head...")
+ /// The message to send to the corresponding person on cast
+ var/to_send
+
+/datum/action/cooldown/spell/personality_commune/New(Target)
+ . = ..()
+ if(!istype(target, /datum/brain_trauma/severe/split_personality))
+ stack_trace("[type] was created on a target that isn't a /datum/brain_trauma/severe/split_personality, this doesn't work.")
+ qdel(src)
+
+/datum/action/cooldown/spell/personality_commune/is_valid_target(atom/cast_on)
+ return isliving(cast_on)
+
+/datum/action/cooldown/spell/personality_commune/before_cast(atom/cast_on)
+ . = ..()
+ if(. & SPELL_CANCEL_CAST)
+ return
+
+ var/datum/brain_trauma/severe/split_personality/trauma = target
+ if(!istype(trauma)) // hypothetically impossible but you never know
+ return . | SPELL_CANCEL_CAST
+
+ to_send = tgui_input_text(cast_on, "What would you like to tell your other self?", "Commune")
+ if(QDELETED(src) || QDELETED(trauma)|| QDELETED(cast_on) || QDELETED(trauma.owner) || !can_cast_spell())
+ return . | SPELL_CANCEL_CAST
+ if(!to_send)
+ reset_cooldown()
+ return . | SPELL_CANCEL_CAST
+
+ return TRUE
+
+// Pillaged and adapted from telepathy code
+/datum/action/cooldown/spell/personality_commune/cast(mob/living/cast_on)
+ . = ..()
+ var/datum/brain_trauma/severe/split_personality/trauma = target
+
+ var/user_message = span_boldnotice("You concentrate and send thoughts to your other self:")
+ var/user_message_body = span_notice("[to_send]")
+ to_chat(cast_on, "[user_message] [user_message_body]")
+ to_chat(trauma.owner, "[fluff_text] [user_message_body]")
+ log_directed_talk(cast_on, trauma.owner, to_send, LOG_SAY, "[name]")
+ for(var/dead_mob in GLOB.dead_mob_list)
+ if(!isobserver(dead_mob))
+ continue
+ to_chat(dead_mob, "[FOLLOW_LINK(dead_mob, cast_on)] [span_boldnotice("[cast_on] [name]:")] [span_notice("\"[to_send]\" to")] [span_name("[trauma]")]")
diff --git a/code/modules/spells/spell_types/rod_form.dm b/code/modules/spells/spell_types/self/rod_form.dm
similarity index 71%
rename from code/modules/spells/spell_types/rod_form.dm
rename to code/modules/spells/spell_types/self/rod_form.dm
index fd833029b5551..467271e5433b9 100644
--- a/code/modules/spells/spell_types/rod_form.dm
+++ b/code/modules/spells/spell_types/self/rod_form.dm
@@ -1,46 +1,61 @@
/// The base distance a wizard rod will go without upgrades.
#define BASE_WIZ_ROD_RANGE 13
-/obj/effect/proc_holder/spell/targeted/rod_form
+/datum/action/cooldown/spell/rod_form
name = "Rod Form"
- desc = "Take on the form of an immovable rod, destroying all in your path. Purchasing this spell multiple times will also increase the rod's damage and travel range."
- clothes_req = TRUE
- human_req = FALSE
- charge_max = 250
- cooldown_min = 100
- range = -1
+ desc = "Take on the form of an immovable rod, destroying all in your path. \
+ Purchasing this spell multiple times will also increase the rod's damage and travel range."
+ button_icon_state = "immrod"
+
school = SCHOOL_TRANSMUTATION
- include_user = TRUE
+ cooldown_time = 25 SECONDS
+ cooldown_reduction_per_rank = 3.75 SECONDS
+
invocation = "CLANG!"
invocation_type = INVOCATION_SHOUT
- action_icon_state = "immrod"
+ spell_requirements = SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_OFF_CENTCOM
+
/// The extra distance we travel per additional spell level.
var/distance_per_spell_rank = 3
/// The extra damage we deal per additional spell level.
var/damage_per_spell_rank = 20
+ /// The max distance the rod goes on cast
+ var/rod_max_distance = BASE_WIZ_ROD_RANGE
+ /// The damage bonus applied to the rod on cast
+ var/rod_damage_bonus = 0
-/obj/effect/proc_holder/spell/targeted/rod_form/cast(list/targets, mob/user = usr)
- var/area/our_area = get_area(user)
- if(istype(our_area, /area/centcom/wizard_station))
- to_chat(user, span_warning("You know better than to trash Wizard Federation property. Best wait until you leave to use [src]."))
- return
+/datum/action/cooldown/spell/rod_form/cast(atom/cast_on)
+ . = ..()
+ // The destination turf of the rod - just a bit over the max range we calculated, for safety
+ var/turf/distant_turf = get_ranged_target_turf(get_turf(cast_on), cast_on.dir, (rod_max_distance + 2))
+
+ new /obj/effect/immovablerod/wizard(
+ get_turf(cast_on),
+ distant_turf,
+ null,
+ FALSE,
+ cast_on,
+ rod_max_distance,
+ rod_damage_bonus,
+ )
+
+/datum/action/cooldown/spell/rod_form/level_spell(bypass_cap = FALSE)
+ . = ..()
+ if(!.)
+ return FALSE
- // You travel farther when you upgrade the spell.
- var/rod_max_distance = BASE_WIZ_ROD_RANGE + (spell_level * distance_per_spell_rank)
- // You do more damage when you upgrade the spell.
- var/rod_damage_bonus = (spell_level * damage_per_spell_rank)
-
- for(var/mob/living/caster in targets)
- new /obj/effect/immovablerod/wizard(
- get_turf(caster),
- get_ranged_target_turf(get_turf(caster), caster.dir, (rod_max_distance + 2)), // Just a bit over the distance we got
- null,
- FALSE,
- caster,
- rod_max_distance,
- rod_damage_bonus,
- )
- ADD_TRAIT(caster, TRAIT_ROD_FORM, MAGIC_TRAIT)
+ rod_max_distance += distance_per_spell_rank
+ rod_damage_bonus += damage_per_spell_rank
+ return TRUE
+
+/datum/action/cooldown/spell/rod_form/delevel_spell()
+ . = ..()
+ if(!.)
+ return FALSE
+
+ rod_max_distance -= distance_per_spell_rank
+ rod_damage_bonus -= damage_per_spell_rank
+ return TRUE
/// Wizard Version of the Immovable Rod.
/obj/effect/immovablerod/wizard
@@ -125,6 +140,7 @@
wizard.forceMove(src)
wizard.notransform = TRUE
wizard.status_flags |= GODMODE
+ ADD_TRAIT(wizard, TRAIT_MAGICALLY_PHASED, REF(src))
/**
* Eject our current wizard, removing them from the rod
@@ -139,6 +155,6 @@
wizard.notransform = FALSE
wizard.forceMove(get_turf(src))
our_wizard = null
- REMOVE_TRAIT(wizard, TRAIT_ROD_FORM, MAGIC_TRAIT)
+ REMOVE_TRAIT(wizard, TRAIT_MAGICALLY_PHASED, REF(src))
#undef BASE_WIZ_ROD_RANGE
diff --git a/code/modules/spells/spell_types/self/smoke.dm b/code/modules/spells/spell_types/self/smoke.dm
new file mode 100644
index 0000000000000..b2c7e924f191e
--- /dev/null
+++ b/code/modules/spells/spell_types/self/smoke.dm
@@ -0,0 +1,37 @@
+/// Basic smoke spell.
+/datum/action/cooldown/spell/smoke
+ name = "Smoke"
+ desc = "This spell spawns a cloud of smoke at your location. \
+ People within will begin to choke and drop their items."
+ button_icon_state = "smoke"
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 12 SECONDS
+ cooldown_reduction_per_rank = 2.5 SECONDS
+
+ invocation_type = INVOCATION_NONE
+
+ smoke_type = /datum/effect_system/fluid_spread/smoke/bad
+ smoke_amt = 4
+
+/// Chaplain smoke.
+/datum/action/cooldown/spell/smoke/lesser
+ name = "Holy Smoke"
+ desc = "This spell spawns a small cloud of smoke at your location."
+
+ school = SCHOOL_HOLY
+ cooldown_time = 36 SECONDS
+ spell_requirements = NONE
+
+ smoke_type = /datum/effect_system/fluid_spread/smoke
+ smoke_amt = 2
+
+/// Unused smoke that makes people sleep. Used to be for cult?
+/datum/action/cooldown/spell/smoke/disable
+ name = "Paralysing Smoke"
+ desc = "This spell spawns a cloud of paralysing smoke."
+ background_icon_state = "bg_cult"
+
+ cooldown_time = 20 SECONDS
+
+ smoke_type = /datum/effect_system/fluid_spread/smoke/sleeping
diff --git a/code/modules/spells/spell_types/self/soultap.dm b/code/modules/spells/spell_types/self/soultap.dm
new file mode 100644
index 0000000000000..57932ad8288b9
--- /dev/null
+++ b/code/modules/spells/spell_types/self/soultap.dm
@@ -0,0 +1,63 @@
+
+/**
+ * SOUL TAP!
+ *
+ * Trades 20 max health for a refresh on all of your spells.
+ * I was considering making it depend on the cooldowns of your spells, but I want to support "Big spell wizard" with this loadout.
+ * The two spells that sound most problematic with this is mindswap and lichdom,
+ * but soul tap requires clothes for mindswap and lichdom takes your soul.
+ */
+/datum/action/cooldown/spell/tap
+ name = "Soul Tap"
+ desc = "Fuel your spells using your own soul!"
+ button_icon_state = "soultap"
+
+ // I could see why this wouldn't be necromancy, but messing with souls or whatever. Ectomancy?
+ school = SCHOOL_NECROMANCY
+ cooldown_time = 1 SECONDS
+ invocation = "AT ANY COST!"
+ invocation_type = INVOCATION_SHOUT
+ spell_max_level = 1
+
+ /// The amount of health we take on tap
+ var/tap_health_taken = 20
+
+/datum/action/cooldown/spell/tap/can_cast_spell(feedback = TRUE)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ // We call this here so we can get feedback if they try to cast it when they shouldn't.
+ if(!is_valid_target(owner))
+ if(feedback)
+ to_chat(owner, span_warning("You have no soul to tap into!"))
+ return FALSE
+
+ return TRUE
+
+/datum/action/cooldown/spell/tap/is_valid_target(atom/cast_on)
+ return isliving(cast_on) && !HAS_TRAIT(owner, TRAIT_NO_SOUL)
+
+/datum/action/cooldown/spell/tap/cast(mob/living/cast_on)
+ . = ..()
+ cast_on.maxHealth -= tap_health_taken
+ cast_on.health = min(cast_on.health, cast_on.maxHealth)
+
+ for(var/datum/action/cooldown/spell/spell in cast_on.actions)
+ spell.reset_spell_cooldown()
+
+ // If the tap took all of our life, we die and lose our soul!
+ if(cast_on.maxHealth <= 0)
+ to_chat(cast_on, span_userdanger("Your weakened soul is completely consumed by the tap!"))
+ ADD_TRAIT(cast_on, TRAIT_NO_SOUL, MAGIC_TRAIT)
+
+ cast_on.visible_message(span_danger("[cast_on] suddenly dies!"), ignored_mobs = cast_on)
+ cast_on.death()
+
+ // If the next tap will kill us, give us a heads-up
+ else if(cast_on.maxHealth - tap_health_taken <= 0)
+ to_chat(cast_on, span_bolddanger("Your body feels incredibly drained, and the burning is hard to ignore!"))
+
+ // Otherwise just give them some feedback
+ else
+ to_chat(cast_on, span_danger("Your body feels drained and there is a burning pain in your chest."))
diff --git a/code/modules/spells/spell_types/self/spacetime_distortion.dm b/code/modules/spells/spell_types/self/spacetime_distortion.dm
new file mode 100644
index 0000000000000..d71cb6713bc8b
--- /dev/null
+++ b/code/modules/spells/spell_types/self/spacetime_distortion.dm
@@ -0,0 +1,168 @@
+// This could probably be an aoe spell but it's a little cursed, so I'm not touching it
+/datum/action/cooldown/spell/spacetime_dist
+ name = "Spacetime Distortion"
+ desc = "Entangle the strings of space-time in an area around you, \
+ randomizing the layout and making proper movement impossible. The strings vibrate..."
+ sound = 'sound/effects/magic.ogg'
+ button_icon_state = "spacetime"
+
+ school = SCHOOL_EVOCATION
+ cooldown_time = 30 SECONDS
+ spell_requirements = SPELL_REQUIRES_WIZARD_GARB|SPELL_REQUIRES_NO_ANTIMAGIC|SPELL_REQUIRES_OFF_CENTCOM
+ spell_max_level = 1
+
+ /// Weather we're ready to cast again yet or not
+ var/ready = TRUE
+ /// The radius of the scramble around the caster
+ var/scramble_radius = 7
+ /// The duration of the scramble
+ var/duration = 15 SECONDS
+ /// A lazylist of all scramble effects this spell has created.
+ var/list/effects
+
+/datum/action/cooldown/spell/spacetime_dist/Destroy()
+ QDEL_LAZYLIST(effects)
+ return ..()
+
+/datum/action/cooldown/spell/spacetime_dist/can_cast_spell(feedback = TRUE)
+ return ..() && ready
+
+/datum/action/cooldown/spell/spacetime_dist/set_statpanel_format()
+ . = ..()
+ if(!islist(.))
+ return
+
+ if(!ready)
+ .[PANEL_DISPLAY_STATUS] = "NOT READY"
+
+/datum/action/cooldown/spell/spacetime_dist/cast(atom/cast_on)
+ . = ..()
+ var/list/turf/to_switcharoo = get_targets_to_scramble(cast_on)
+ if(!length(to_switcharoo))
+ to_chat(cast_on, span_warning("For whatever reason, the strings nearby aren't keen on being tangled."))
+ reset_spell_cooldown()
+ return
+
+ ready = FALSE
+
+ for(var/turf/swap_a as anything in to_switcharoo)
+ var/turf/swap_b = to_switcharoo[swap_a]
+ var/obj/effect/cross_action/spacetime_dist/effect_a = new /obj/effect/cross_action/spacetime_dist(swap_a, antimagic_flags)
+ var/obj/effect/cross_action/spacetime_dist/effect_b = new /obj/effect/cross_action/spacetime_dist(swap_b, antimagic_flags)
+ effect_a.linked_dist = effect_b
+ effect_a.add_overlay(swap_b.photograph())
+ effect_b.linked_dist = effect_a
+ effect_b.add_overlay(swap_a.photograph())
+ effect_b.set_light(4, 30, "#c9fff5")
+ LAZYADD(effects, effect_a)
+ LAZYADD(effects, effect_b)
+
+/datum/action/cooldown/spell/spacetime_dist/after_cast()
+ . = ..()
+ addtimer(CALLBACK(src, .proc/clean_turfs), duration)
+
+/// Callback which cleans up our effects list after the duration expires.
+/datum/action/cooldown/spell/spacetime_dist/proc/clean_turfs()
+ QDEL_LAZYLIST(effects)
+ ready = TRUE
+
+/**
+ * Gets a list of turfs around the center atom to scramble.
+ *
+ * Returns an assoc list of [turf] to [turf]. These pairs are what turfs are
+ * swapped between one another when the cast is done.
+ */
+/datum/action/cooldown/spell/spacetime_dist/proc/get_targets_to_scramble(atom/center)
+ // Get turfs around the center
+ var/list/turfs = spiral_range_turfs(scramble_radius, center)
+ if(!length(turfs))
+ return
+
+ var/list/turf_steps = list()
+
+ // Go through the turfs we got and pair them up
+ // This is where we determine what to swap where
+ var/num_to_scramble = round(length(turfs) * 0.5)
+ for(var/i in 1 to num_to_scramble)
+ turf_steps[pick_n_take(turfs)] = pick_n_take(turfs)
+
+ // If there's any turfs unlinked with a friend,
+ // just randomly swap it with any turf in the area
+ if(length(turfs))
+ var/turf/loner = pick(turfs)
+ var/area/caster_area = get_area(center)
+ turf_steps[loner] = get_turf(pick(caster_area.contents))
+
+ return turf_steps
+
+
+/obj/effect/cross_action
+ name = "cross me"
+ desc = "for crossing"
+ anchored = TRUE
+
+/obj/effect/cross_action/spacetime_dist
+ name = "spacetime distortion"
+ desc = "A distortion in spacetime. You can hear faint music..."
+ icon_state = ""
+ /// A flags which save people from being thrown about
+ var/antimagic_flags = MAGIC_RESISTANCE
+ var/obj/effect/cross_action/spacetime_dist/linked_dist
+ var/busy = FALSE
+ var/sound
+ var/walks_left = 50 //prevents the game from hanging in extreme cases (such as minigun fire)
+
+/obj/effect/cross_action/singularity_act()
+ return
+
+/obj/effect/cross_action/singularity_pull()
+ return
+
+/obj/effect/cross_action/spacetime_dist/Initialize(mapload, flags = MAGIC_RESISTANCE)
+ . = ..()
+ setDir(pick(GLOB.cardinals))
+ var/static/list/loc_connections = list(
+ COMSIG_ATOM_ENTERED = .proc/on_entered,
+ )
+ AddElement(/datum/element/connect_loc, loc_connections)
+ antimagic_flags = flags
+
+/obj/effect/cross_action/spacetime_dist/proc/walk_link(atom/movable/AM)
+ if(ismob(AM))
+ var/mob/M = AM
+ if(M.can_block_magic(antimagic_flags, charge_cost = 0))
+ return
+ if(linked_dist && walks_left > 0)
+ flick("purplesparkles", src)
+ linked_dist.get_walker(AM)
+ walks_left--
+
+/obj/effect/cross_action/spacetime_dist/proc/get_walker(atom/movable/AM)
+ busy = TRUE
+ flick("purplesparkles", src)
+ AM.forceMove(get_turf(src))
+ playsound(get_turf(src),sound,70,FALSE)
+ busy = FALSE
+
+/obj/effect/cross_action/spacetime_dist/proc/on_entered(datum/source, atom/movable/AM)
+ SIGNAL_HANDLER
+ if(!busy)
+ walk_link(AM)
+
+/obj/effect/cross_action/spacetime_dist/attackby(obj/item/W, mob/user, params)
+ if(user.temporarilyRemoveItemFromInventory(W))
+ walk_link(W)
+ else
+ walk_link(user)
+
+//ATTACK HAND IGNORING PARENT RETURN VALUE
+/obj/effect/cross_action/spacetime_dist/attack_hand(mob/user, list/modifiers)
+ walk_link(user)
+
+/obj/effect/cross_action/spacetime_dist/attack_paw(mob/user, list/modifiers)
+ walk_link(user)
+
+/obj/effect/cross_action/spacetime_dist/Destroy()
+ busy = TRUE
+ linked_dist = null
+ return ..()
diff --git a/code/modules/spells/spell_types/self/stop_time.dm b/code/modules/spells/spell_types/self/stop_time.dm
new file mode 100644
index 0000000000000..cab47375eb3a4
--- /dev/null
+++ b/code/modules/spells/spell_types/self/stop_time.dm
@@ -0,0 +1,30 @@
+/datum/action/cooldown/spell/timestop
+ name = "Stop Time"
+ desc = "This spell stops time for everyone except for you, \
+ allowing you to move freely while your enemies and even projectiles are frozen."
+ button_icon_state = "time"
+
+ school = SCHOOL_FORBIDDEN // Fucking with time is not appreciated by anyone
+ cooldown_time = 50 SECONDS
+ cooldown_reduction_per_rank = 10 SECONDS
+
+ invocation = "TOKI YO TOMARE!"
+ invocation_type = INVOCATION_SHOUT
+
+ /// The radius / range of the time stop.
+ var/timestop_range = 2
+ /// The duration of the time stop.
+ var/timestop_duration = 10 SECONDS
+
+/datum/action/cooldown/spell/timestop/Grant(mob/grant_to)
+ . = ..()
+ if(owner)
+ ADD_TRAIT(owner, TRAIT_TIME_STOP_IMMUNE, REF(src))
+
+/datum/action/cooldown/spell/timestop/Remove(mob/remove_from)
+ REMOVE_TRAIT(remove_from, TRAIT_TIME_STOP_IMMUNE, REF(src))
+ return ..()
+
+/datum/action/cooldown/spell/timestop/cast(atom/cast_on)
+ . = ..()
+ new /obj/effect/timestop/magic(get_turf(cast_on), timestop_range, timestop_duration, list(cast_on))
diff --git a/code/modules/spells/spell_types/self/summonitem.dm b/code/modules/spells/spell_types/self/summonitem.dm
new file mode 100644
index 0000000000000..761c2c7efada7
--- /dev/null
+++ b/code/modules/spells/spell_types/self/summonitem.dm
@@ -0,0 +1,154 @@
+/datum/action/cooldown/spell/summonitem
+ name = "Instant Summons"
+ desc = "This spell can be used to recall a previously marked item to your hand from anywhere in the universe."
+ button_icon_state = "summons"
+
+ school = SCHOOL_TRANSMUTATION
+ cooldown_time = 10 SECONDS
+
+ invocation = "GAR YOK"
+ invocation_type = INVOCATION_WHISPER
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+ spell_max_level = 1 //cannot be improved
+
+ ///The obj marked for recall
+ var/obj/marked_item
+
+/datum/action/cooldown/spell/summonitem/is_valid_target(atom/cast_on)
+ return isliving(cast_on)
+
+/// Set the passed object as our marked item
+/datum/action/cooldown/spell/summonitem/proc/mark_item(obj/to_mark)
+ name = "Recall [to_mark]"
+ marked_item = to_mark
+ RegisterSignal(marked_item, COMSIG_PARENT_QDELETING, .proc/on_marked_item_deleted)
+
+/// Unset our current marked item
+/datum/action/cooldown/spell/summonitem/proc/unmark_item()
+ name = initial(name)
+ UnregisterSignal(marked_item, COMSIG_PARENT_QDELETING)
+ marked_item = null
+
+/// Signal proc for COMSIG_PARENT_QDELETING on our marked item, unmarks our item if it's deleted
+/datum/action/cooldown/spell/summonitem/proc/on_marked_item_deleted(datum/source)
+ SIGNAL_HANDLER
+
+ if(owner)
+ to_chat(owner, span_boldwarning("You sense your marked item has been destroyed!"))
+ unmark_item()
+
+/datum/action/cooldown/spell/summonitem/cast(mob/living/cast_on)
+ . = ..()
+ if(QDELETED(marked_item))
+ try_link_item(cast_on)
+ return
+
+ if(marked_item == cast_on.get_active_held_item())
+ try_unlink_item(cast_on)
+ return
+
+ try_recall_item(cast_on)
+
+/// If we don't have a marked item, attempts to mark the caster's held item.
+/datum/action/cooldown/spell/summonitem/proc/try_link_item(mob/living/caster)
+ var/obj/item/potential_mark = caster.get_active_held_item()
+ if(!potential_mark)
+ if(caster.get_inactive_held_item())
+ to_chat(caster, span_warning("You must hold the desired item in your hands to mark it for recall!"))
+ else
+ to_chat(caster, span_warning("You aren't holding anything that can be marked for recall!"))
+ return FALSE
+
+ var/link_message = ""
+ if(potential_mark.item_flags & ABSTRACT)
+ return FALSE
+ if(SEND_SIGNAL(potential_mark, COMSIG_ITEM_MARK_RETRIEVAL, src, caster) & COMPONENT_BLOCK_MARK_RETRIEVAL)
+ return FALSE
+ if(HAS_TRAIT(potential_mark, TRAIT_NODROP))
+ link_message += "Though it feels redundant... "
+
+ link_message += "You mark [potential_mark] for recall."
+ to_chat(caster, span_notice(link_message))
+ mark_item(potential_mark)
+ return TRUE
+
+/// If we have a marked item and it's in our hand, we will try to unlink it
+/datum/action/cooldown/spell/summonitem/proc/try_unlink_item(mob/living/caster)
+ to_chat(caster, span_notice("You begin removing the mark on [marked_item]..."))
+ if(!do_after(caster, 5 SECONDS, marked_item))
+ to_chat(caster, span_notice("You decide to keep [marked_item] marked."))
+ return FALSE
+
+ to_chat(caster, span_notice("You remove the mark on [marked_item] to use elsewhere."))
+ unmark_item()
+ return TRUE
+
+/// Recalls our marked item to the caster. May bring some unexpected things along.
+/datum/action/cooldown/spell/summonitem/proc/try_recall_item(mob/living/caster)
+ var/obj/item_to_retrieve = marked_item
+
+ if(item_to_retrieve.loc)
+ // I don't want to know how someone could put something
+ // inside itself but these are wizards so let's be safe
+ var/infinite_recursion = 0
+
+ // if it's in something, you get the whole thing.
+ while(!isturf(item_to_retrieve.loc) && infinite_recursion < 10)
+ if(isitem(item_to_retrieve.loc))
+ var/obj/item/mark_loc = item_to_retrieve.loc
+ // Being able to summon abstract things because
+ // your item happened to get placed there is a no-no
+ if(mark_loc.item_flags & ABSTRACT)
+ break
+
+ // If its on someone, properly drop it
+ if(ismob(item_to_retrieve.loc))
+ var/mob/holding_mark = item_to_retrieve.loc
+
+ // Items in silicons warp the whole silicon
+ if(issilicon(holding_mark))
+ holding_mark.loc.visible_message(span_warning("[holding_mark] suddenly disappears!"))
+ holding_mark.forceMove(caster.loc)
+ holding_mark.loc.visible_message(span_warning("[holding_mark] suddenly appears!"))
+ item_to_retrieve = null
+ break
+
+ holding_mark.dropItemToGround(item_to_retrieve)
+
+ else if(isobj(item_to_retrieve.loc))
+ var/obj/retrieved_item = item_to_retrieve.loc
+ // Can't bring anchored things
+ if(retrieved_item.anchored)
+ return
+ // Edge cases for moving certain machinery...
+ if(istype(retrieved_item, /obj/machinery/portable_atmospherics))
+ var/obj/machinery/portable_atmospherics/atmos_item = retrieved_item
+ atmos_item.disconnect()
+ atmos_item.update_appearance()
+
+ // Otherwise bring the whole thing with us
+ item_to_retrieve = retrieved_item
+
+ infinite_recursion += 1
+
+ else
+ // Organs are usually stored in nullspace
+ if(isorgan(item_to_retrieve))
+ var/obj/item/organ/organ = item_to_retrieve
+ if(organ.owner)
+ // If this code ever runs I will be happy
+ log_combat(caster, organ.owner, "magically removed [organ.name] from", addition = "COMBAT MODE: [uppertext(caster.combat_mode)]")
+ organ.Remove(organ.owner)
+
+ if(!item_to_retrieve)
+ return
+
+ item_to_retrieve.loc?.visible_message(span_warning("[item_to_retrieve] suddenly disappears!"))
+
+ if(isitem(item_to_retrieve) && caster.put_in_hands(item_to_retrieve))
+ item_to_retrieve.loc.visible_message(span_warning("[item_to_retrieve] suddenly appears in [caster]'s hand!"))
+ else
+ item_to_retrieve.forceMove(caster.drop_location())
+ item_to_retrieve.loc.visible_message(span_warning("[item_to_retrieve] suddenly appears!"))
+ playsound(get_turf(item_to_retrieve), 'sound/magic/summonitems_generic.ogg', 50, TRUE)
diff --git a/code/modules/spells/spell_types/self/voice_of_god.dm b/code/modules/spells/spell_types/self/voice_of_god.dm
new file mode 100644
index 0000000000000..ae4c46a3bb73a
--- /dev/null
+++ b/code/modules/spells/spell_types/self/voice_of_god.dm
@@ -0,0 +1,50 @@
+/datum/action/cooldown/spell/voice_of_god
+ name = "Voice of God"
+ desc = "Speak with an incredibly compelling voice, forcing listeners to obey your commands."
+ icon_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "voice_of_god"
+ sound = 'sound/magic/clockwork/invoke_general.ogg'
+
+ cooldown_time = 120 SECONDS // Varies depending on command
+ invocation = "" // Handled by the VOICE OF GOD itself
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = NONE
+ antimagic_flags = NONE
+
+ /// The command to deliver on cast
+ var/command
+ /// The modifier to the cooldown, after cast
+ var/cooldown_mod = 1
+ /// The modifier put onto the power of the command
+ var/power_mod = 1
+ /// A list of spans to apply to commands given
+ var/list/spans = list("colossus", "yell")
+
+/datum/action/cooldown/spell/voice_of_god/before_cast(atom/cast_on)
+ . = ..()
+ if(. & SPELL_CANCEL_CAST)
+ return
+
+ command = tgui_input_text(cast_on, "Speak with the Voice of God", "Command")
+ if(QDELETED(src) || QDELETED(cast_on) || !can_cast_spell())
+ return . | SPELL_CANCEL_CAST
+ if(!command)
+ reset_spell_cooldown()
+ return . | SPELL_CANCEL_CAST
+
+/datum/action/cooldown/spell/voice_of_god/cast(atom/cast_on)
+ . = ..()
+ var/command_cooldown = voice_of_god(uppertext(command), cast_on, spans, base_multiplier = power_mod)
+ cooldown_time = (command_cooldown * cooldown_mod)
+
+// "Invocation" is done by the actual voice of god proc
+/datum/action/cooldown/spell/voice_of_god/invocation()
+ return
+
+/datum/action/cooldown/spell/voice_of_god/clown
+ name = "Voice of Clown"
+ desc = "Speak with an incredibly funny voice, startling people into obeying you for a brief moment."
+ sound = 'sound/misc/scary_horn.ogg'
+ cooldown_mod = 0.5
+ power_mod = 0.1
+ spans = list("clown")
diff --git a/code/modules/spells/spell_types/shadow_walk.dm b/code/modules/spells/spell_types/shadow_walk.dm
deleted file mode 100644
index 8bf7bdbc961c4..0000000000000
--- a/code/modules/spells/spell_types/shadow_walk.dm
+++ /dev/null
@@ -1,98 +0,0 @@
-#define SHADOW_REGEN_RATE 1.5
-
-/obj/effect/proc_holder/spell/targeted/shadowwalk
- name = "Shadow Walk"
- desc = "Grants unlimited movement in darkness."
- charge_max = 0
- clothes_req = FALSE
- antimagic_flags = NONE
- phase_allowed = TRUE
- selection_type = "range"
- range = -1
- include_user = TRUE
- cooldown_min = 0
- overlay = null
- action_icon = 'icons/mob/actions/actions_minor_antag.dmi'
- action_icon_state = "ninja_cloak"
- action_background_icon_state = "bg_alien"
-
-/obj/effect/proc_holder/spell/targeted/shadowwalk/cast_check(skipcharge = 0,mob/user = usr)
- . = ..()
- if(!.)
- return FALSE
- var/area/noteleport_check = get_area(user)
- if(noteleport_check && noteleport_check.area_flags & NOTELEPORT)
- to_chat(user, span_danger("Some dull, universal force is stopping you from melting into the shadows here."))
- return FALSE
-
-/obj/effect/proc_holder/spell/targeted/shadowwalk/cast(list/targets,mob/living/user = usr)
- var/L = user.loc
- if(istype(user.loc, /obj/effect/dummy/phased_mob/shadow))
- var/obj/effect/dummy/phased_mob/shadow/S = L
- S.end_jaunt(FALSE)
- return
- else
- var/turf/T = get_turf(user)
- var/light_amount = T.get_lumcount()
- if(light_amount < SHADOW_SPECIES_LIGHT_THRESHOLD)
- playsound(get_turf(user), 'sound/magic/ethereal_enter.ogg', 50, TRUE, -1)
- visible_message(span_boldwarning("[user] melts into the shadows!"))
- user.SetAllImmobility(0)
- user.setStaminaLoss(0, 0)
- var/obj/effect/dummy/phased_mob/shadow/S2 = new(get_turf(user.loc))
- user.forceMove(S2)
- S2.jaunter = user
- else
- to_chat(user, span_warning("It isn't dark enough here!"))
-
-/obj/effect/dummy/phased_mob/shadow
- var/mob/living/jaunter
-
-/obj/effect/dummy/phased_mob/shadow/Initialize(mapload)
- . = ..()
- START_PROCESSING(SSobj, src)
-
-/obj/effect/dummy/phased_mob/shadow/Destroy()
- jaunter = null
- STOP_PROCESSING(SSobj, src)
- . = ..()
-
-/obj/effect/dummy/phased_mob/shadow/process(delta_time)
- var/turf/T = get_turf(src)
- var/light_amount = T.get_lumcount()
- if(!jaunter || jaunter.loc != src)
- qdel(src)
- if (light_amount < 0.2 && (!QDELETED(jaunter))) //heal in the dark
- jaunter.heal_overall_damage((SHADOW_REGEN_RATE * delta_time), (SHADOW_REGEN_RATE * delta_time), 0, BODYTYPE_ORGANIC)
- check_light_level()
-
-
-/obj/effect/dummy/phased_mob/shadow/relaymove(mob/living/user, direction)
- var/turf/oldloc = loc
- . = ..()
- if(loc != oldloc)
- check_light_level()
-
-/obj/effect/dummy/phased_mob/shadow/phased_check(mob/living/user, direction)
- . = ..()
- if(. && isspaceturf(.))
- to_chat(user, span_warning("It really would not be wise to go into space."))
- return FALSE
-
-/obj/effect/dummy/phased_mob/shadow/proc/check_light_level()
- var/turf/T = get_turf(src)
- var/light_amount = T.get_lumcount()
- if(light_amount > 0.2) // jaunt ends
- end_jaunt(TRUE)
-
-/obj/effect/dummy/phased_mob/shadow/proc/end_jaunt(forced = FALSE)
- if(jaunter)
- if(forced)
- visible_message(span_boldwarning("[jaunter] is revealed by the light!"))
- else
- visible_message(span_boldwarning("[jaunter] emerges from the darkness!"))
- playsound(loc, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
- qdel(src)
-
-
-#undef SHADOW_REGEN_RATE
diff --git a/code/modules/spells/spell_types/shapeshift.dm b/code/modules/spells/spell_types/shapeshift.dm
deleted file mode 100644
index 9e856bf9d680d..0000000000000
--- a/code/modules/spells/spell_types/shapeshift.dm
+++ /dev/null
@@ -1,241 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/shapeshift
- name = "Shapechange"
- desc = "Take on the shape of another for a time to use their natural abilities. Once you've made your choice it cannot be changed."
- school = SCHOOL_TRANSMUTATION
- clothes_req = FALSE
- human_req = FALSE
- charge_max = 200
- cooldown_min = 50
- range = -1
- include_user = TRUE
- invocation = "RAC'WA NO!"
- invocation_type = INVOCATION_SHOUT
- action_icon_state = "shapeshift"
- nonabstract_req = TRUE
-
- var/revert_on_death = TRUE
- var/die_with_shapeshifted_form = TRUE
- ///If you want to convert the caster's health and blood to the shift, and vice versa.
- var/convert_damage = TRUE
- ///The damage type to convert to, as simplemobs don't have advanced damagetypes.
- var/convert_damage_type = BRUTE
-
- var/mob/living/shapeshift_type
- var/list/possible_shapes = list(
- /mob/living/simple_animal/mouse,
- /mob/living/simple_animal/pet/dog/corgi,
- /mob/living/simple_animal/hostile/carp/ranged/chaos,
- /mob/living/simple_animal/bot/secbot/ed209,
- /mob/living/simple_animal/hostile/giant_spider/viper/wizard,
- /mob/living/simple_animal/hostile/construct/juggernaut/mystic,
- )
-
-/obj/effect/proc_holder/spell/targeted/shapeshift/cast(list/targets, mob/user = usr)
- if(src in user.mob_spell_list)
- LAZYREMOVE(user.mob_spell_list, src)
- user.mind.AddSpell(src)
- if(user.buckled)
- user.buckled.unbuckle_mob(src,force=TRUE)
- for(var/mob/living/shapeshifted_targets in targets)
- if(!shapeshift_type)
- var/list/animal_list = list()
- var/list/display_animals = list()
- for(var/path in possible_shapes)
- var/mob/living/simple_animal/animal = path
- animal_list[initial(animal.name)] = path
- var/image/animal_image = image(icon = initial(animal.icon), icon_state = initial(animal.icon_state))
- display_animals += list(initial(animal.name) = animal_image)
- sort_list(display_animals)
- var/new_shapeshift_type = show_radial_menu(shapeshifted_targets, shapeshifted_targets, display_animals, custom_check = CALLBACK(src, .proc/check_menu, user), radius = 38, require_near = TRUE)
- if(shapeshift_type)
- return
- shapeshift_type = new_shapeshift_type
- if(!shapeshift_type) //If you aren't gonna decide I am!
- shapeshift_type = pick(animal_list)
- shapeshift_type = animal_list[shapeshift_type]
-
- var/obj/shapeshift_holder/shapeshift_ability = locate() in shapeshifted_targets
- var/currently_ventcrawling = FALSE
- if(shapeshift_ability)
- if(shapeshifted_targets.movement_type & VENTCRAWLING)
- currently_ventcrawling = TRUE
- shapeshifted_targets = restore_form(shapeshifted_targets)
- else
- shapeshifted_targets = Shapeshift(shapeshifted_targets)
-
- // Can our new form support ventcrawling?
- var/ventcrawler = HAS_TRAIT(shapeshifted_targets, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(shapeshifted_targets, TRAIT_VENTCRAWLER_NUDE)
- if(ventcrawler)
- continue
-
- // Are we currently ventcrawling?
- if(!currently_ventcrawling)
- continue
-
- // You're shapeshifting into something that can't fit into a vent
- var/obj/machinery/atmospherics/pipeyoudiein = shapeshifted_targets.loc
- var/datum/pipeline/ourpipeline
- var/pipenets = pipeyoudiein.return_pipenets()
- if(islist(pipenets))
- ourpipeline = pipenets[1]
- else
- ourpipeline = pipenets
-
- to_chat(shapeshifted_targets, span_userdanger("Casting [src] inside of [pipeyoudiein] quickly turns you into a bloody mush!"))
- var/gibtype = /obj/effect/gibspawner/generic
- if(isalien(shapeshifted_targets))
- gibtype = /obj/effect/gibspawner/xeno
- for(var/obj/machinery/atmospherics/components/unary/possiblevent in range(10, get_turf(shapeshifted_targets)))
- if(possiblevent.parents.len && possiblevent.parents[1] == ourpipeline)
- new gibtype(get_turf(possiblevent))
- playsound(possiblevent, 'sound/effects/reee.ogg', 75, TRUE)
- priority_announce("We detected a pipe blockage around [get_area(get_turf(shapeshifted_targets))], please dispatch someone to investigate.", "Central Command")
- shapeshifted_targets.death()
- qdel(shapeshifted_targets)
-
-/**
- * check_menu: Checks if we are allowed to interact with a radial menu
- *
- * Arguments:
- * * user The mob interacting with a menu
- */
-/obj/effect/proc_holder/spell/targeted/shapeshift/proc/check_menu(mob/user)
- if(!istype(user))
- return FALSE
- if(user.incapacitated())
- return FALSE
- return TRUE
-
-/obj/effect/proc_holder/spell/targeted/shapeshift/proc/Shapeshift(mob/living/caster)
- var/obj/shapeshift_holder/shapeshift_ability = locate() in caster
- if(shapeshift_ability)
- to_chat(caster, span_warning("You're already shapeshifted!"))
- return
-
- var/mob/living/shape = new shapeshift_type(caster.loc)
- shapeshift_ability = new(shape, src, caster)
-
- clothes_req = FALSE
- human_req = FALSE
- return shape
-
-/obj/effect/proc_holder/spell/targeted/shapeshift/proc/restore_form(mob/living/caster)
- var/obj/shapeshift_holder/shapeshift_ability = locate() in caster
- if(!shapeshift_ability)
- return
-
- var/mob/living/restored_player = shapeshift_ability.stored
-
- shapeshift_ability.restore()
-
- clothes_req = initial(clothes_req)
- human_req = initial(human_req)
- return restored_player
-
-/obj/effect/proc_holder/spell/targeted/shapeshift/dragon
- name = "Dragon Form"
- desc = "Take on the shape a lesser ash drake."
- invocation = "RAAAAAAAAWR!"
-
-
- shapeshift_type = /mob/living/simple_animal/hostile/megafauna/dragon/lesser
-
-
-/obj/shapeshift_holder
- name = "Shapeshift holder"
- resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ON_FIRE | UNACIDABLE | ACID_PROOF
- var/mob/living/stored
- var/mob/living/shape
- var/restoring = FALSE
- var/obj/effect/proc_holder/spell/targeted/shapeshift/source
-
-/obj/shapeshift_holder/Initialize(mapload, obj/effect/proc_holder/spell/targeted/shapeshift/_source, mob/living/caster)
- . = ..()
- source = _source
- shape = loc
- if(!istype(shape))
- stack_trace("shapeshift holder created outside mob/living")
- return INITIALIZE_HINT_QDEL
- stored = caster
- if(stored.mind)
- stored.mind.transfer_to(shape)
- stored.forceMove(src)
- stored.notransform = TRUE
- if(source.convert_damage)
- var/damage_percent = (stored.maxHealth - stored.health)/stored.maxHealth;
- var/damapply = damage_percent * shape.maxHealth;
-
- shape.apply_damage(damapply, source.convert_damage_type, forced = TRUE, wound_bonus=CANT_WOUND);
- shape.blood_volume = stored.blood_volume;
-
- RegisterSignal(shape, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH), .proc/shape_death)
- RegisterSignal(stored, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH), .proc/caster_death)
-
-/obj/shapeshift_holder/Destroy()
- // restore_form manages signal unregistering. If restoring is TRUE, we've already unregistered the signals and we're here
- // because restore() qdel'd src.
- if(!restoring)
- restore()
- stored = null
- shape = null
- return ..()
-
-/obj/shapeshift_holder/Moved()
- . = ..()
- if(!restoring && !QDELETED(src))
- restore()
-
-/obj/shapeshift_holder/handle_atom_del(atom/A)
- if(A == stored && !restoring)
- restore()
-
-/obj/shapeshift_holder/Exited(atom/movable/gone, direction)
- if(stored == gone && !restoring)
- restore()
-
-/obj/shapeshift_holder/proc/caster_death()
- SIGNAL_HANDLER
- //Something kills the stored caster through direct damage.
- if(source.revert_on_death)
- restore(death=TRUE)
- else
- shape.death()
-
-/obj/shapeshift_holder/proc/shape_death()
- SIGNAL_HANDLER
- //Shape dies.
- if(source.die_with_shapeshifted_form)
- if(source.revert_on_death)
- restore(death=TRUE)
- else
- restore()
-
-/obj/shapeshift_holder/proc/restore(death=FALSE)
- // Destroy() calls this proc if it hasn't been called. Unregistering here prevents multiple qdel loops
- // when caster and shape both die at the same time.
- UnregisterSignal(shape, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH))
- UnregisterSignal(stored, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH))
- restoring = TRUE
- stored.forceMove(shape.loc)
- stored.notransform = FALSE
- if(shape.mind)
- shape.mind.transfer_to(stored)
- if(death)
- stored.death()
- else if(source.convert_damage)
- stored.revive(full_heal = TRUE, admin_revive = FALSE)
-
- var/damage_percent = (shape.maxHealth - shape.health)/shape.maxHealth;
- var/damapply = stored.maxHealth * damage_percent
-
- stored.apply_damage(damapply, source.convert_damage_type, forced = TRUE, wound_bonus=CANT_WOUND)
- if(source.convert_damage)
- stored.blood_volume = shape.blood_volume;
-
- // This guard is important because restore() can also be called on COMSIG_PARENT_QDELETING for shape, as well as on death.
- // This can happen in, for example, [/proc/wabbajack] where the mob hit is qdel'd.
- if(!QDELETED(shape))
- QDEL_NULL(shape)
-
- qdel(src)
- return stored
diff --git a/code/modules/spells/spell_types/shapeshift/_shapeshift.dm b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
new file mode 100644
index 0000000000000..df154f2cedb6a
--- /dev/null
+++ b/code/modules/spells/spell_types/shapeshift/_shapeshift.dm
@@ -0,0 +1,244 @@
+/datum/action/cooldown/spell/shapeshift
+ school = SCHOOL_TRANSMUTATION
+
+ /// Whehter we revert to our human form on death.
+ var/revert_on_death = TRUE
+ /// Whether we die when our shapeshifted form is killed
+ var/die_with_shapeshifted_form = TRUE
+ /// Whether we convert our health from one form to another
+ var/convert_damage = TRUE
+ /// If convert damage is true, the damage type we deal when converting damage back and forth
+ var/convert_damage_type = BRUTE
+
+ /// Our chosen type
+ var/mob/living/shapeshift_type
+ /// All possible types we can become
+ var/list/atom/possible_shapes
+
+/datum/action/cooldown/spell/shapeshift/is_valid_target(atom/cast_on)
+ return isliving(cast_on)
+
+/datum/action/cooldown/spell/shapeshift/proc/is_shifted(mob/living/cast_on)
+ return locate(/obj/shapeshift_holder) in cast_on
+
+/datum/action/cooldown/spell/shapeshift/before_cast(atom/cast_on)
+ . = ..()
+ if(. & SPELL_CANCEL_CAST)
+ return
+
+ if(shapeshift_type)
+ return
+
+ if(length(possible_shapes) == 1)
+ shapeshift_type = possible_shapes[1]
+ return
+
+ var/list/shape_names_to_types = list()
+ var/list/shape_names_to_image = list()
+ if(!length(shape_names_to_types) || !length(shape_names_to_image))
+ for(var/atom/path as anything in possible_shapes)
+ var/shape_name = initial(path.name)
+ shape_names_to_types[shape_name] = path
+ shape_names_to_image[shape_name] = image(icon = initial(path.icon), icon_state = initial(path.icon_state))
+
+ var/picked_type = show_radial_menu(
+ cast_on,
+ cast_on,
+ shape_names_to_image,
+ custom_check = CALLBACK(src, .proc/check_menu, cast_on),
+ radius = 38,
+ )
+
+ if(!picked_type)
+ return . | SPELL_CANCEL_CAST
+
+ var/atom/shift_type = shape_names_to_types[picked_type]
+ if(!ispath(shift_type))
+ return . | SPELL_CANCEL_CAST
+
+ shapeshift_type = shift_type || pick(possible_shapes)
+ if(QDELETED(src) || QDELETED(owner) || !can_cast_spell(feedback = FALSE))
+ return . | SPELL_CANCEL_CAST
+
+/datum/action/cooldown/spell/shapeshift/cast(mob/living/cast_on)
+ . = ..()
+ cast_on.buckled?.unbuckle_mob(cast_on, force = TRUE)
+
+ var/currently_ventcrawling = (cast_on.movement_type & VENTCRAWLING)
+
+ // Do the shift back or forth
+ if(is_shifted(cast_on))
+ restore_form(cast_on)
+ else
+ do_shapeshift(cast_on)
+
+ // The shift is done, let's make sure they're in a valid state now
+ // If we're not ventcrawling, we don't need to mind
+ if(!currently_ventcrawling)
+ return
+
+ // We are ventcrawling - can our new form support ventcrawling?
+ if(HAS_TRAIT(cast_on, TRAIT_VENTCRAWLER_ALWAYS) || HAS_TRAIT(cast_on, TRAIT_VENTCRAWLER_NUDE))
+ return
+
+ // Uh oh. You've shapeshifted into something that can't fit into a vent, while ventcrawling.
+ eject_from_vents(cast_on)
+
+/// Whenever someone shapeshifts within a vent,
+/// and enters a state in which they are no longer a ventcrawler,
+/// they are brutally ejected from the vents. In the form of gibs.
+/datum/action/cooldown/spell/shapeshift/proc/eject_from_vents(mob/living/cast_on)
+ var/obj/machinery/atmospherics/pipe_you_die_in = cast_on.loc
+ var/datum/pipeline/our_pipeline
+ var/pipenets = pipe_you_die_in.return_pipenets()
+ if(islist(pipenets))
+ our_pipeline = pipenets[1]
+ else
+ our_pipeline = pipenets
+
+ to_chat(cast_on, span_userdanger("Casting [src] inside of [pipe_you_die_in] quickly turns you into a bloody mush!"))
+ var/obj/effect/gib_type = isalien(cast_on) ? /obj/effect/gibspawner/xeno : /obj/effect/gibspawner/generic
+
+ for(var/obj/machinery/atmospherics/components/unary/possible_vent in range(10, get_turf(cast_on)))
+ if(length(possible_vent.parents) && possible_vent.parents[1] == our_pipeline)
+ new gib_type(get_turf(possible_vent))
+ playsound(possible_vent, 'sound/effects/reee.ogg', 75, TRUE)
+
+ priority_announce("We detected a pipe blockage around [get_area(get_turf(cast_on))], please dispatch someone to investigate.", "Central Command")
+ cast_on.death()
+ qdel(cast_on)
+
+/datum/action/cooldown/spell/shapeshift/proc/check_menu(mob/living/caster)
+ if(QDELETED(src))
+ return FALSE
+ if(QDELETED(caster))
+ return FALSE
+
+ return !caster.incapacitated()
+
+/datum/action/cooldown/spell/shapeshift/proc/do_shapeshift(mob/living/caster)
+ if(is_shifted(caster))
+ to_chat(caster, span_warning("You're already shapeshifted!"))
+ CRASH("[type] called do_shapeshift while shapeshifted.")
+
+ var/mob/living/new_shape = new shapeshift_type(caster.loc)
+ var/obj/shapeshift_holder/new_shape_holder = new(new_shape, src, caster)
+
+ spell_requirements &= ~(SPELL_REQUIRES_HUMAN|SPELL_REQUIRES_WIZARD_GARB)
+
+ return new_shape_holder
+
+/datum/action/cooldown/spell/shapeshift/proc/restore_form(mob/living/caster)
+ var/obj/shapeshift_holder/current_shift = is_shifted(caster)
+ if(QDELETED(current_shift))
+ return
+
+ var/mob/living/restored_player = current_shift.stored
+
+ current_shift.restore()
+ spell_requirements = initial(spell_requirements) // Miiight mess with admin stuff.
+
+ return restored_player
+
+// Maybe one day, this can be a component or something
+// Until then, this is what holds data between wizard and shapeshift form whenever shapeshift is cast.
+/obj/shapeshift_holder
+ name = "Shapeshift holder"
+ resistance_flags = INDESTRUCTIBLE | LAVA_PROOF | FIRE_PROOF | ON_FIRE | UNACIDABLE | ACID_PROOF
+ var/mob/living/stored
+ var/mob/living/shape
+ var/restoring = FALSE
+ var/datum/action/cooldown/spell/shapeshift/source
+
+/obj/shapeshift_holder/Initialize(mapload, datum/action/cooldown/spell/shapeshift/_source, mob/living/caster)
+ . = ..()
+ source = _source
+ shape = loc
+ if(!istype(shape))
+ stack_trace("shapeshift holder created outside mob/living")
+ return INITIALIZE_HINT_QDEL
+ stored = caster
+ if(stored.mind)
+ stored.mind.transfer_to(shape)
+ stored.forceMove(src)
+ stored.notransform = TRUE
+ if(source.convert_damage)
+ var/damage_percent = (stored.maxHealth - stored.health) / stored.maxHealth;
+ var/damapply = damage_percent * shape.maxHealth;
+
+ shape.apply_damage(damapply, source.convert_damage_type, forced = TRUE, wound_bonus = CANT_WOUND);
+ shape.blood_volume = stored.blood_volume;
+
+ RegisterSignal(shape, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH), .proc/shape_death)
+ RegisterSignal(stored, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH), .proc/caster_death)
+
+/obj/shapeshift_holder/Destroy()
+ // restore_form manages signal unregistering. If restoring is TRUE, we've already unregistered the signals and we're here
+ // because restore() qdel'd src.
+ if(!restoring)
+ restore()
+ stored = null
+ shape = null
+ return ..()
+
+/obj/shapeshift_holder/Moved()
+ . = ..()
+ if(!restoring && !QDELETED(src))
+ restore()
+
+/obj/shapeshift_holder/handle_atom_del(atom/A)
+ if(A == stored && !restoring)
+ restore()
+
+/obj/shapeshift_holder/Exited(atom/movable/gone, direction)
+ if(stored == gone && !restoring)
+ restore()
+
+/obj/shapeshift_holder/proc/caster_death()
+ SIGNAL_HANDLER
+
+ //Something kills the stored caster through direct damage.
+ if(source.revert_on_death)
+ restore(death = TRUE)
+ else
+ shape.death()
+
+/obj/shapeshift_holder/proc/shape_death()
+ SIGNAL_HANDLER
+
+ //Shape dies.
+ if(source.die_with_shapeshifted_form)
+ if(source.revert_on_death)
+ restore(death = TRUE)
+ else
+ restore()
+
+/obj/shapeshift_holder/proc/restore(death=FALSE)
+ // Destroy() calls this proc if it hasn't been called. Unregistering here prevents multiple qdel loops
+ // when caster and shape both die at the same time.
+ UnregisterSignal(shape, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH))
+ UnregisterSignal(stored, list(COMSIG_PARENT_QDELETING, COMSIG_LIVING_DEATH))
+ restoring = TRUE
+ stored.forceMove(shape.loc)
+ stored.notransform = FALSE
+ if(shape.mind)
+ shape.mind.transfer_to(stored)
+ if(death)
+ stored.death()
+ else if(source.convert_damage)
+ stored.revive(full_heal = TRUE, admin_revive = FALSE)
+
+ var/damage_percent = (shape.maxHealth - shape.health)/shape.maxHealth;
+ var/damapply = stored.maxHealth * damage_percent
+
+ stored.apply_damage(damapply, source.convert_damage_type, forced = TRUE, wound_bonus=CANT_WOUND)
+ if(source.convert_damage)
+ stored.blood_volume = shape.blood_volume;
+
+ // This guard is important because restore() can also be called on COMSIG_PARENT_QDELETING for shape, as well as on death.
+ // This can happen in, for example, [/proc/wabbajack] where the mob hit is qdel'd.
+ if(!QDELETED(shape))
+ QDEL_NULL(shape)
+
+ qdel(src)
+ return stored
diff --git a/code/modules/spells/spell_types/shapeshift/dragon.dm b/code/modules/spells/spell_types/shapeshift/dragon.dm
new file mode 100644
index 0000000000000..358ff8a44fdde
--- /dev/null
+++ b/code/modules/spells/spell_types/shapeshift/dragon.dm
@@ -0,0 +1,8 @@
+
+/datum/action/cooldown/spell/shapeshift/dragon
+ name = "Dragon Form"
+ desc = "Take on the shape a lesser ash drake."
+ invocation = "RAAAAAAAAWR!"
+ spell_requirements = NONE
+
+ possible_shapes = list(/mob/living/simple_animal/hostile/megafauna/dragon/lesser)
diff --git a/code/modules/spells/spell_types/shapeshift/polar_bear.dm b/code/modules/spells/spell_types/shapeshift/polar_bear.dm
new file mode 100644
index 0000000000000..73f0bae94969b
--- /dev/null
+++ b/code/modules/spells/spell_types/shapeshift/polar_bear.dm
@@ -0,0 +1,7 @@
+/datum/action/cooldown/spell/shapeshift/polar_bear
+ name = "Polar Bear Form"
+ desc = "Take on the shape of a polar bear."
+ invocation = "RAAAAAAAAWR!"
+ spell_requirements = NONE
+
+ possible_shapes = list(/mob/living/simple_animal/hostile/asteroid/polarbear/lesser)
diff --git a/code/modules/spells/spell_types/shapeshift/shapechange.dm b/code/modules/spells/spell_types/shapeshift/shapechange.dm
new file mode 100644
index 0000000000000..a858ac414d96e
--- /dev/null
+++ b/code/modules/spells/spell_types/shapeshift/shapechange.dm
@@ -0,0 +1,22 @@
+/datum/action/cooldown/spell/shapeshift/wizard
+ name = "Wild Shapeshift"
+ desc = "Take on the shape of another for a time to use their natural abilities. \
+ Once you've made your choice, it cannot be changed."
+ button_icon_state = "shapeshift"
+
+ school = SCHOOL_TRANSMUTATION
+ cooldown_time = 20 SECONDS
+ cooldown_reduction_per_rank = 3.75 SECONDS
+
+ invocation = "RAC'WA NO!"
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+ possible_shapes = list(
+ /mob/living/simple_animal/mouse,
+ /mob/living/simple_animal/pet/dog/corgi,
+ /mob/living/simple_animal/hostile/carp/ranged/chaos,
+ /mob/living/simple_animal/bot/secbot/ed209,
+ /mob/living/simple_animal/hostile/giant_spider/viper/wizard,
+ /mob/living/simple_animal/hostile/construct/juggernaut/mystic,
+ )
diff --git a/code/modules/spells/spell_types/soultap.dm b/code/modules/spells/spell_types/soultap.dm
deleted file mode 100644
index 065b511fbba82..0000000000000
--- a/code/modules/spells/spell_types/soultap.dm
+++ /dev/null
@@ -1,42 +0,0 @@
-/// The amount of health taken per tap.
-#define HEALTH_LOST_PER_SOUL_TAP 20
-
-/**
- * SOUL TAP!
- *
- * Trades 20 max health for a refresh on all of your spells.
- * I was considering making it depend on the cooldowns of your spells, but I want to support "Big spell wizard" with this loadout.
- * The two spells that sound most problematic with this is mindswap and lichdom,
- * but soul tap requires clothes for mindswap and lichdom takes your soul.
- */
-/obj/effect/proc_holder/spell/self/tap
- name = "Soul Tap"
- desc = "Fuel your spells using your own soul!"
- action_icon = 'icons/mob/actions/actions_spells.dmi'
- action_icon_state = "soultap"
- invocation = "AT ANY COST!"
- invocation_type = INVOCATION_SHOUT
- school = SCHOOL_NECROMANCY //i could see why this wouldn't be necromancy but messing with souls or whatever. ectomancy?
- charge_max = 1 SECONDS
- cooldown_min = 1 SECONDS
- level_max = 0
-
-/obj/effect/proc_holder/spell/self/tap/cast(list/targets, mob/living/user = usr)
- if(HAS_TRAIT(user, TRAIT_NO_SOUL))
- to_chat(user, span_warning("You have no soul to tap into!"))
- return
-
- to_chat(user, span_danger("Your body feels drained and there is a burning pain in your chest."))
- user.maxHealth -= HEALTH_LOST_PER_SOUL_TAP
- user.health = min(user.health, user.maxHealth)
- if(user.maxHealth <= 0)
- to_chat(user, span_userdanger("Your weakened soul is completely consumed by the tap!"))
- ADD_TRAIT(user, TRAIT_NO_SOUL, MAGIC_TRAIT)
- return
-
- for(var/obj/effect/proc_holder/spell/spell in user.mind.spell_list)
- spell.charge_counter = spell.charge_max
- spell.recharging = FALSE
- spell.update_appearance()
-
-#undef HEALTH_LOST_PER_SOUL_TAP
diff --git a/code/modules/spells/spell_types/spacetime_distortion.dm b/code/modules/spells/spell_types/spacetime_distortion.dm
deleted file mode 100644
index 6ced2de231864..0000000000000
--- a/code/modules/spells/spell_types/spacetime_distortion.dm
+++ /dev/null
@@ -1,130 +0,0 @@
-/obj/effect/proc_holder/spell/spacetime_dist
- name = "Spacetime Distortion"
- desc = "Entangle the strings of space-time in an area around you, randomizing the layout and making proper movement impossible. The strings vibrate..."
- charge_max = 300
- var/duration = 150
- range = 7
- var/list/effects
- var/ready = TRUE
- school = SCHOOL_EVOCATION
- centcom_cancast = FALSE
- sound = 'sound/effects/magic.ogg'
- cooldown_min = 300
- level_max = 0
- action_icon_state = "spacetime"
-
-/obj/effect/proc_holder/spell/spacetime_dist/can_cast(mob/user = usr)
- if(ready)
- return ..()
- return FALSE
-
-/obj/effect/proc_holder/spell/spacetime_dist/choose_targets(mob/user = usr)
- var/list/turfs = spiral_range_turfs(range, user)
- if(!turfs.len)
- revert_cast()
- return
-
- ready = FALSE
- var/list/turf_steps = list()
- var/length = round(turfs.len * 0.5)
- for(var/i in 1 to length)
- turf_steps[pick_n_take(turfs)] = pick_n_take(turfs)
- if(turfs.len > 0)
- var/turf/loner = pick(turfs)
- var/area/A = get_area(user)
- turf_steps[loner] = get_turf(pick(A.contents))
-
- perform(turf_steps,user=user)
-
-/obj/effect/proc_holder/spell/spacetime_dist/after_cast(list/targets)
- addtimer(CALLBACK(src, .proc/clean_turfs), duration)
-
-/obj/effect/proc_holder/spell/spacetime_dist/cast(list/targets, mob/user = usr)
- effects = list()
- for(var/V in targets)
- var/turf/T0 = V
- var/turf/T1 = targets[V]
- var/obj/effect/cross_action/spacetime_dist/STD0 = new /obj/effect/cross_action/spacetime_dist(T0)
- var/obj/effect/cross_action/spacetime_dist/STD1 = new /obj/effect/cross_action/spacetime_dist(T1)
- STD0.linked_dist = STD1
- STD0.add_overlay(T1.photograph())
- STD1.linked_dist = STD0
- STD1.add_overlay(T0.photograph())
- STD1.set_light(4, 30, "#c9fff5")
- effects += STD0
- effects += STD1
-
-/obj/effect/proc_holder/spell/spacetime_dist/proc/clean_turfs()
- for(var/effect in effects)
- qdel(effect)
- effects.Cut()
- effects = null
- ready = TRUE
-
-/obj/effect/cross_action
- name = "cross me"
- desc = "for crossing"
- anchored = TRUE
-
-/obj/effect/cross_action/spacetime_dist
- name = "spacetime distortion"
- desc = "A distortion in spacetime. You can hear faint music..."
- icon_state = ""
- var/obj/effect/cross_action/spacetime_dist/linked_dist
- var/busy = FALSE
- var/sound
- var/walks_left = 50 //prevents the game from hanging in extreme cases (such as minigun fire)
-
-/obj/effect/cross_action/singularity_act()
- return
-
-/obj/effect/cross_action/singularity_pull()
- return
-
-/obj/effect/cross_action/spacetime_dist/Initialize(mapload)
- . = ..()
- setDir(pick(GLOB.cardinals))
- var/static/list/loc_connections = list(
- COMSIG_ATOM_ENTERED = .proc/on_entered,
- )
- AddElement(/datum/element/connect_loc, loc_connections)
-
-/obj/effect/cross_action/spacetime_dist/proc/walk_link(atom/movable/AM)
- if(ismob(AM))
- var/mob/M = AM
- if(M.can_block_magic(charge_cost = 0))
- return
- if(linked_dist && walks_left > 0)
- flick("purplesparkles", src)
- linked_dist.get_walker(AM)
- walks_left--
-
-/obj/effect/cross_action/spacetime_dist/proc/get_walker(atom/movable/AM)
- busy = TRUE
- flick("purplesparkles", src)
- AM.forceMove(get_turf(src))
- playsound(get_turf(src),sound,70,FALSE)
- busy = FALSE
-
-/obj/effect/cross_action/spacetime_dist/proc/on_entered(datum/source, atom/movable/AM)
- SIGNAL_HANDLER
- if(!busy)
- walk_link(AM)
-
-/obj/effect/cross_action/spacetime_dist/attackby(obj/item/W, mob/user, params)
- if(user.temporarilyRemoveItemFromInventory(W))
- walk_link(W)
- else
- walk_link(user)
-
-//ATTACK HAND IGNORING PARENT RETURN VALUE
-/obj/effect/cross_action/spacetime_dist/attack_hand(mob/user, list/modifiers)
- walk_link(user)
-
-/obj/effect/cross_action/spacetime_dist/attack_paw(mob/user, list/modifiers)
- walk_link(user)
-
-/obj/effect/cross_action/spacetime_dist/Destroy()
- busy = TRUE
- linked_dist = null
- return ..()
diff --git a/code/modules/spells/spell_types/summonitem.dm b/code/modules/spells/spell_types/summonitem.dm
deleted file mode 100644
index b5c24f32d1107..0000000000000
--- a/code/modules/spells/spell_types/summonitem.dm
+++ /dev/null
@@ -1,108 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/summonitem
- name = "Instant Summons"
- desc = "This spell can be used to recall a previously marked item to your hand from anywhere in the universe."
- school = SCHOOL_TRANSMUTATION
- charge_max = 100
- clothes_req = FALSE
- invocation = "GAR YOK"
- invocation_type = INVOCATION_WHISPER
- range = -1
- level_max = 0 //cannot be improved
- cooldown_min = 100
- include_user = TRUE
- action_icon_state = "summons"
- ///The obj marked for recall
- var/obj/marked_item
-
-/obj/effect/proc_holder/spell/targeted/summonitem/cast(list/targets, mob/user = usr)
- for(var/mob/living/L in targets)
- var/list/hand_items = list(L.get_active_held_item(), L.get_inactive_held_item())
- var/message
-
- if(!marked_item) //linking item to the spell
- message = ""
- for(var/obj/item/item in hand_items)
- if(item.item_flags & ABSTRACT)
- continue
- if(SEND_SIGNAL(item, COMSIG_ITEM_MARK_RETRIEVAL) & COMPONENT_BLOCK_MARK_RETRIEVAL)
- continue
- if(HAS_TRAIT(item, TRAIT_NODROP))
- message += "Though it feels redundant, "
- marked_item = item
- message += "You mark [item] for recall."
- name = "Recall [item]"
- break
-
- if(!marked_item)
- if(hand_items)
- message = span_warning("You aren't holding anything that can be marked for recall!")
- else
- message = span_warning("You must hold the desired item in your hands to mark it for recall!")
-
- else if(marked_item && (marked_item in hand_items)) //unlinking item to the spell
- message = span_notice("You remove the mark on [marked_item] to use elsewhere.")
- name = "Instant Summons"
- marked_item = null
-
- else if(marked_item && QDELETED(marked_item)) //the item was destroyed at some point
- message = span_warning("You sense your marked item has been destroyed!")
- name = "Instant Summons"
- marked_item = null
-
- else //Getting previously marked item
- var/obj/item_to_retrieve = marked_item
- var/infinite_recursion = 0 //I don't want to know how someone could put something inside itself but these are wizards so let's be safe
-
- if(!item_to_retrieve.loc)
- if(isorgan(item_to_retrieve)) // Organs are usually stored in nullspace
- var/obj/item/organ/organ = item_to_retrieve
- if(organ.owner)
- // If this code ever runs I will be happy
- log_combat(L, organ.owner, "magically removed [organ.name] from", addition="COMBAT MODE: [uppertext(L.combat_mode)]")
- organ.Remove(organ.owner)
- else
- while(!isturf(item_to_retrieve.loc) && infinite_recursion < 10) //if it's in something you get the whole thing.
- if(isitem(item_to_retrieve.loc))
- var/obj/item/I = item_to_retrieve.loc
- if(I.item_flags & ABSTRACT) //Being able to summon abstract things because your item happened to get placed there is a no-no
- break
- if(ismob(item_to_retrieve.loc)) //If its on someone, properly drop it
- var/mob/M = item_to_retrieve.loc
-
- if(issilicon(M)) //Items in silicons warp the whole silicon
- M.loc.visible_message(span_warning("[M] suddenly disappears!"))
- M.forceMove(L.loc)
- M.loc.visible_message(span_warning("[M] suddenly appears!"))
- item_to_retrieve = null
- break
- M.dropItemToGround(item_to_retrieve)
-
- else
- var/obj/retrieved_item = item_to_retrieve.loc
- if(retrieved_item.anchored)
- return
- if(istype(retrieved_item, /obj/machinery/portable_atmospherics)) //Edge cases for moved machinery
- var/obj/machinery/portable_atmospherics/P = retrieved_item
- P.disconnect()
- P.update_appearance()
-
- item_to_retrieve = retrieved_item
-
- infinite_recursion += 1
-
- if(!item_to_retrieve)
- return
-
- if(item_to_retrieve.loc)
- item_to_retrieve.loc.visible_message(span_warning("The [item_to_retrieve.name] suddenly disappears!"))
- if(!L.put_in_hands(item_to_retrieve))
- item_to_retrieve.forceMove(L.drop_location())
- item_to_retrieve.loc.visible_message(span_warning("The [item_to_retrieve.name] suddenly appears!"))
- playsound(get_turf(L), 'sound/magic/summonitems_generic.ogg', 50, TRUE)
- else
- item_to_retrieve.loc.visible_message(span_warning("The [item_to_retrieve.name] suddenly appears in [L]'s hand!"))
- playsound(get_turf(L), 'sound/magic/summonitems_generic.ogg', 50, TRUE)
-
-
- if(message)
- to_chat(L, message)
diff --git a/code/modules/spells/spell_types/telepathy.dm b/code/modules/spells/spell_types/telepathy.dm
deleted file mode 100644
index 2185d6174f91f..0000000000000
--- a/code/modules/spells/spell_types/telepathy.dm
+++ /dev/null
@@ -1,30 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/telepathy
- name = "Telepathy"
- desc = "Telepathically transmits a message to the target."
- charge_max = 0
- clothes_req = 0
- range = 7
- include_user = 0
- action_icon = 'icons/mob/actions/actions_revenant.dmi'
- action_icon_state = "r_transmit"
- action_background_icon_state = "bg_spell"
- antimagic_flags = MAGIC_RESISTANCE_MIND
- var/notice = "notice"
- var/boldnotice = "boldnotice"
-
-/obj/effect/proc_holder/spell/targeted/telepathy/cast(list/targets, mob/living/simple_animal/revenant/user = usr)
- for(var/mob/living/M in targets)
- var/msg = tgui_input_text(user, "What do you wish to tell [M]?", "Telepathy")
- if(!msg)
- charge_counter = charge_max
- return
- log_directed_talk(user, M, msg, LOG_SAY, "[name]")
- to_chat(user, "You transmit to [M]:[msg]")
- if(!M.can_block_magic(antimagic_flags, charge_cost = 0)) //hear no evil
- to_chat(M, "You hear something behind you talking...[msg]")
- for(var/ded in GLOB.dead_mob_list)
- if(!isobserver(ded))
- continue
- var/follow_rev = FOLLOW_LINK(ded, user)
- var/follow_whispee = FOLLOW_LINK(ded, M)
- to_chat(ded, "[follow_rev] [user] [name]:\"[msg]\" to [follow_whispee] [span_name("[M]")]")
diff --git a/code/modules/spells/spell_types/teleport/_teleport.dm b/code/modules/spells/spell_types/teleport/_teleport.dm
new file mode 100644
index 0000000000000..794da323dc539
--- /dev/null
+++ b/code/modules/spells/spell_types/teleport/_teleport.dm
@@ -0,0 +1,145 @@
+
+/**
+ * ## Teleport Spell
+ *
+ * Teleports the caster to a turf selected by get_destinations().
+ */
+/datum/action/cooldown/spell/teleport
+ sound = 'sound/weapons/zapbang.ogg'
+
+ school = SCHOOL_TRANSLOCATION
+
+ /// What channel the teleport is done under.
+ var/teleport_channel = TELEPORT_CHANNEL_MAGIC
+ /// Whether we force the teleport to happen (ie, it cannot be blocked by noteleport areas or blessings or whatever)
+ var/force_teleport = FALSE
+ /// A list of flags related to determining if our destination target is valid or not.
+ var/destination_flags = NONE
+ /// The sound played on arrival, after the teleport.
+ var/post_teleport_sound = 'sound/weapons/zapbang.ogg'
+
+/datum/action/cooldown/spell/teleport/cast(atom/cast_on)
+ . = ..()
+ var/list/turf/destinations = get_destinations(cast_on)
+ if(!length(destinations))
+ CRASH("[type] failed to find a teleport destination.")
+
+ do_teleport(cast_on, pick(destinations), asoundout = post_teleport_sound, channel = teleport_channel, forced = force_teleport)
+
+/// Gets a list of destinations that are valid
+/datum/action/cooldown/spell/teleport/proc/get_destinations(atom/center)
+ CRASH("[type] did not implement get_destinations and either has no effects or implemented the spell incorrectly.")
+
+/// Checks if the passed turf is a valid destination.
+/datum/action/cooldown/spell/teleport/proc/is_valid_destination(turf/selected)
+ if(isspaceturf(selected) && (destination_flags & TELEPORT_SPELL_SKIP_SPACE))
+ return FALSE
+ if(selected.density && (destination_flags & TELEPORT_SPELL_SKIP_DENSE))
+ return FALSE
+ if(selected.is_blocked_turf(exclude_mobs = TRUE) && (destination_flags & TELEPORT_SPELL_SKIP_BLOCKED))
+ return FALSE
+
+ return TRUE
+
+/**
+ * ### Radius Teleport Spell
+ *
+ * A subtype of teleport that will teleport the caster
+ * to a random turf within a radius of themselves.
+ */
+/datum/action/cooldown/spell/teleport/radius_turf
+ /// The inner radius around the caster that we can teleport to
+ var/inner_tele_radius = 1
+ /// The outer radius around the caster that we can teleport to
+ var/outer_tele_radius = 2
+
+/datum/action/cooldown/spell/teleport/radius_turf/get_destinations(atom/center)
+ var/list/valid_turfs = list()
+ var/list/possibles = RANGE_TURFS(outer_tele_radius, center)
+ if(inner_tele_radius > 0)
+ possibles -= RANGE_TURFS(inner_tele_radius, center)
+
+ for(var/turf/nearby_turf as anything in possibles)
+ if(!is_valid_destination(nearby_turf))
+ continue
+
+ valid_turfs += nearby_turf
+
+ // If there are valid turfs around us?
+ // Screw it, allow 'em to teleport to ANY nearby turf.
+ return length(valid_turfs) ? valid_turfs : possibles
+
+/datum/action/cooldown/spell/teleport/radius_turf/is_valid_destination(turf/selected)
+ . = ..()
+ if(!.)
+ return FALSE
+
+ // putting them at the edge is dumb
+ if(selected.x > world.maxx - outer_tele_radius || selected.x < outer_tele_radius)
+ return FALSE
+ if(selected.y > world.maxy - outer_tele_radius || selected.y < outer_tele_radius)
+ return FALSE
+
+ return TRUE
+
+/**
+ * ### Area Teleport Spell
+ *
+ * A subtype of teleport that will teleport the caster
+ * to a random turf within a selected (or random) area.
+ */
+/datum/action/cooldown/spell/teleport/area_teleport
+ force_teleport = TRUE // Forced, as the Wizard Den is noteleport and wizards couldn't escape otherwise.
+ destination_flags = TELEPORT_SPELL_SKIP_BLOCKED
+ /// The last area we chose to teleport / where we're currently teleporting to, if mid-cast
+ var/last_chosen_area_name
+ /// If FALSE, the caster can select the destination area. If TRUE, they will teleport to somewhere randomly instead.
+ var/randomise_selection = FALSE
+ /// If the invocation appends the selected area when said. Requires invocation mode shout or whisper.
+ var/invocation_says_area = TRUE
+
+/datum/action/cooldown/spell/teleport/area_teleport/get_destinations(atom/center)
+ var/list/valid_turfs = list()
+ for(var/turf/possible_destination as anything in get_area_turfs(GLOB.teleportlocs[last_chosen_area_name]))
+ if(!is_valid_destination(possible_destination))
+ continue
+
+ valid_turfs += possible_destination
+
+ return valid_turfs
+
+/datum/action/cooldown/spell/teleport/area_teleport/before_cast(atom/cast_on)
+ . = ..()
+ if(. & SPELL_CANCEL_CAST)
+ return
+
+ var/area/target_area
+ if(randomise_selection)
+ target_area = pick(GLOB.teleportlocs)
+ else
+ target_area = tgui_input_list(cast_on, "Chose an area to teleport to.", "Teleport", GLOB.teleportlocs)
+
+ if(QDELETED(src) || QDELETED(cast_on) || !can_cast_spell())
+ return . | SPELL_CANCEL_CAST
+ if(!target_area || isnull(GLOB.teleportlocs[target_area]))
+ return . | SPELL_CANCEL_CAST
+
+ last_chosen_area_name = target_area
+
+/datum/action/cooldown/spell/teleport/area_teleport/cast(atom/cast_on)
+ if(isliving(cast_on))
+ var/mob/living/living_cast_on = cast_on
+ living_cast_on.buckled?.unbuckle_mob(cast_on, force = TRUE)
+ return ..()
+
+/datum/action/cooldown/spell/teleport/area_teleport/invocation()
+ var/area/last_chosen_area = GLOB.teleportlocs[last_chosen_area_name]
+
+ if(!invocation_says_area || isnull(last_chosen_area))
+ return ..()
+
+ switch(invocation_type)
+ if(INVOCATION_SHOUT)
+ owner.say("[invocation], [uppertext(last_chosen_area.name)]!", forced = "spell ([src])")
+ if(INVOCATION_WHISPER)
+ owner.whisper("[invocation], [uppertext(last_chosen_area.name)].", forced = "spell ([src])")
diff --git a/code/modules/spells/spell_types/teleport/blink.dm b/code/modules/spells/spell_types/teleport/blink.dm
new file mode 100644
index 0000000000000..623bef237fb6e
--- /dev/null
+++ b/code/modules/spells/spell_types/teleport/blink.dm
@@ -0,0 +1,19 @@
+/datum/action/cooldown/spell/teleport/radius_turf/blink
+ name = "Blink"
+ desc = "This spell randomly teleports you a short distance."
+ button_icon_state = "blink"
+ sound = 'sound/magic/blink.ogg'
+
+ school = SCHOOL_FORBIDDEN
+ cooldown_time = 2 SECONDS
+ cooldown_reduction_per_rank = 0.4 SECONDS
+
+ invocation_type = INVOCATION_NONE
+
+ smoke_type = /datum/effect_system/fluid_spread/smoke
+ smoke_amt = 0
+
+ inner_tele_radius = 0
+ outer_tele_radius = 6
+
+ post_teleport_sound = 'sound/magic/blink.ogg'
diff --git a/code/modules/spells/spell_types/teleport/teleport.dm b/code/modules/spells/spell_types/teleport/teleport.dm
new file mode 100644
index 0000000000000..a6ce6d1b9a58b
--- /dev/null
+++ b/code/modules/spells/spell_types/teleport/teleport.dm
@@ -0,0 +1,51 @@
+/// The wizard's teleport SPELL
+/datum/action/cooldown/spell/teleport/area_teleport/wizard
+ name = "Teleport"
+ desc = "This spell teleports you to an area of your selection."
+ button_icon_state = "teleport"
+ sound = 'sound/magic/teleport_diss.ogg'
+
+ school = SCHOOL_TRANSLOCATION
+ cooldown_time = 1 MINUTES
+ cooldown_reduction_per_rank = 10 SECONDS
+
+ invocation = "SCYAR NILA"
+ invocation_type = INVOCATION_SHOUT
+
+ smoke_type = /datum/effect_system/fluid_spread/smoke
+ smoke_amt = 2
+
+ post_teleport_sound = 'sound/magic/teleport_app.ogg'
+
+// Santa's teleport, themed as such
+/datum/action/cooldown/spell/teleport/area_teleport/wizard/santa
+ name = "Santa Teleport"
+
+ invocation = "HO HO HO!"
+ spell_requirements = NONE
+ antimagic_flags = NONE
+
+ invocation_says_area = FALSE // Santa moves in mysterious ways
+
+/// Used by the wizard's teleport scroll
+/datum/action/cooldown/spell/teleport/area_teleport/wizard/scroll
+ name = "Teleport (scroll)"
+ cooldown_time = 0 SECONDS
+
+ invocation = null
+ invocation_type = INVOCATION_NONE
+ spell_requirements = NONE
+
+ invocation_says_area = FALSE
+
+/datum/action/cooldown/spell/teleport/area_teleport/wizard/scroll/IsAvailable()
+ return ..() && owner.is_holding(target)
+
+/datum/action/cooldown/spell/teleport/area_teleport/wizard/scroll/before_cast(atom/cast_on)
+ . = ..()
+ if(. & SPELL_CANCEL_CAST)
+ return
+
+ var/mob/living/carbon/caster = cast_on
+ if(caster.incapacitated() || !caster.is_holding(target))
+ return . | SPELL_CANCEL_CAST
diff --git a/code/modules/spells/spell_types/the_traps.dm b/code/modules/spells/spell_types/the_traps.dm
deleted file mode 100644
index 6fb86100ab0db..0000000000000
--- a/code/modules/spells/spell_types/the_traps.dm
+++ /dev/null
@@ -1,26 +0,0 @@
-/obj/effect/proc_holder/spell/aoe_turf/conjure/the_traps
- name = "The Traps!"
- desc = "Summon a number of traps around you. They will damage and enrage any enemies that step on them."
-
- charge_max = 250
- cooldown_min = 50
-
- clothes_req = TRUE
- invocation = "CAVERE INSIDIAS"
- invocation_type = INVOCATION_SHOUT
- range = 3
-
- summon_type = list(
- /obj/structure/trap/stun,
- /obj/structure/trap/fire,
- /obj/structure/trap/chill,
- /obj/structure/trap/damage
- )
- summon_lifespan = 3000
- summon_amt = 5
-
- action_icon_state = "the_traps"
-
-/obj/effect/proc_holder/spell/aoe_turf/conjure/the_traps/post_summon(obj/structure/trap/T, mob/user)
- T.immune_minds += user.mind
- T.charges = 1
diff --git a/code/modules/spells/spell_types/touch/_touch.dm b/code/modules/spells/spell_types/touch/_touch.dm
new file mode 100644
index 0000000000000..d17ef04387d21
--- /dev/null
+++ b/code/modules/spells/spell_types/touch/_touch.dm
@@ -0,0 +1,265 @@
+/datum/action/cooldown/spell/touch
+ check_flags = AB_CHECK_CONSCIOUS|AB_CHECK_HANDS_BLOCKED
+ sound = 'sound/items/welder.ogg'
+ invocation = "High Five!"
+ invocation_type = INVOCATION_SHOUT
+
+ /// Typepath of what hand we create on initial cast.
+ var/obj/item/melee/touch_attack/hand_path = /obj/item/melee/touch_attack
+ /// Ref to the hand we currently have deployed.
+ var/obj/item/melee/touch_attack/attached_hand
+ /// The message displayed to the person upon creating the touch hand
+ var/draw_message = span_notice("You channel the power of the spell to your hand.")
+ /// The message displayed upon willingly dropping / deleting / cancelling the touch hand before using it
+ var/drop_message = span_notice("You draw the power out of your hand.")
+
+/datum/action/cooldown/spell/touch/Destroy()
+ // If we have an owner, the hand is cleaned up in Remove(), which Destroy() calls.
+ if(!owner)
+ QDEL_NULL(attached_hand)
+ return ..()
+
+/datum/action/cooldown/spell/touch/Remove(mob/living/remove_from)
+ remove_hand(remove_from)
+ return ..()
+
+/datum/action/cooldown/spell/touch/UpdateButton(atom/movable/screen/movable/action_button/button, status_only = FALSE, force = FALSE)
+ . = ..()
+ if(!button)
+ return
+ if(attached_hand)
+ button.color = COLOR_GREEN
+
+/datum/action/cooldown/spell/touch/set_statpanel_format()
+ . = ..()
+ if(!islist(.))
+ return
+
+ if(attached_hand)
+ .[PANEL_DISPLAY_STATUS] = "ACTIVE"
+
+/datum/action/cooldown/spell/touch/can_cast_spell(feedback = TRUE)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!iscarbon(owner))
+ return FALSE
+ var/mob/living/carbon/carbon_owner = owner
+ if(!(carbon_owner.mobility_flags & MOBILITY_USE))
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/spell/touch/is_valid_target(atom/cast_on)
+ return iscarbon(cast_on)
+
+/**
+ * Creates a new hand_path hand and equips it to the caster.
+ *
+ * If the equipping action fails, reverts the cooldown and returns FALSE.
+ * Otherwise, registers signals and returns TRUE.
+ */
+/datum/action/cooldown/spell/touch/proc/create_hand(mob/living/carbon/cast_on)
+ var/obj/item/melee/touch_attack/new_hand = new hand_path(cast_on, src)
+ if(!cast_on.put_in_hands(new_hand, del_on_fail = TRUE))
+ reset_spell_cooldown()
+ if (cast_on.usable_hands == 0)
+ to_chat(cast_on, span_warning("You dont have any usable hands!"))
+ else
+ to_chat(cast_on, span_warning("Your hands are full!"))
+ return FALSE
+
+ attached_hand = new_hand
+ RegisterSignal(attached_hand, COMSIG_ITEM_AFTERATTACK, .proc/on_hand_hit)
+ RegisterSignal(attached_hand, COMSIG_ITEM_AFTERATTACK_SECONDARY, .proc/on_secondary_hand_hit)
+ RegisterSignal(attached_hand, COMSIG_PARENT_QDELETING, .proc/on_hand_deleted)
+ RegisterSignal(attached_hand, COMSIG_ITEM_DROPPED, .proc/on_hand_dropped)
+ to_chat(cast_on, draw_message)
+ return TRUE
+
+/**
+ * Unregisters any signals and deletes the hand currently summoned by the spell.
+ *
+ * If reset_cooldown_after is TRUE, we will additionally refund the cooldown of the spell.
+ * If reset_cooldown_after is FALSE, we will instead just start the spell's cooldown
+ */
+/datum/action/cooldown/spell/touch/proc/remove_hand(mob/living/hand_owner, reset_cooldown_after = FALSE)
+ if(!QDELETED(attached_hand))
+ UnregisterSignal(attached_hand, list(COMSIG_ITEM_AFTERATTACK, COMSIG_ITEM_AFTERATTACK_SECONDARY, COMSIG_PARENT_QDELETING, COMSIG_ITEM_DROPPED))
+ hand_owner?.temporarilyRemoveItemFromInventory(attached_hand)
+ QDEL_NULL(attached_hand)
+
+ if(reset_cooldown_after)
+ if(hand_owner)
+ to_chat(hand_owner, drop_message)
+ reset_spell_cooldown()
+ else
+ StartCooldown()
+
+// Touch spells don't go on cooldown OR give off an invocation until the hand is used itself.
+/datum/action/cooldown/spell/touch/before_cast(atom/cast_on)
+ return ..() | SPELL_NO_FEEDBACK | SPELL_NO_IMMEDIATE_COOLDOWN
+
+/datum/action/cooldown/spell/touch/cast(mob/living/carbon/cast_on)
+ if(!QDELETED(attached_hand) && (attached_hand in cast_on.held_items))
+ remove_hand(cast_on, reset_cooldown_after = TRUE)
+ return
+
+ create_hand(cast_on)
+ return ..()
+
+/**
+ * Signal proc for [COMSIG_ITEM_AFTERATTACK] from our attached hand.
+ *
+ * When our hand hits an atom, we can cast do_hand_hit() on them.
+ */
+/datum/action/cooldown/spell/touch/proc/on_hand_hit(datum/source, atom/victim, mob/caster, proximity_flag, click_parameters)
+ SIGNAL_HANDLER
+
+ if(!proximity_flag)
+ return
+ if(victim == caster)
+ return
+ if(!can_cast_spell(feedback = FALSE))
+ return
+
+ INVOKE_ASYNC(src, .proc/do_hand_hit, source, victim, caster)
+
+/**
+ * Signal proc for [COMSIG_ITEM_AFTERATTACK_SECONDARY] from our attached hand.
+ *
+ * Same as on_hand_hit, but for if right-click was used on hit.
+ */
+/datum/action/cooldown/spell/touch/proc/on_secondary_hand_hit(datum/source, atom/victim, mob/caster, proximity_flag, click_parameters)
+ SIGNAL_HANDLER
+
+ if(!proximity_flag)
+ return
+ if(victim == caster)
+ return
+ if(!can_cast_spell(feedback = FALSE))
+ return
+
+ INVOKE_ASYNC(src, .proc/do_secondary_hand_hit, source, victim, caster)
+
+/**
+ * Calls cast_on_hand_hit() from the caster onto the victim.
+ */
+/datum/action/cooldown/spell/touch/proc/do_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster)
+ SEND_SIGNAL(src, COMSIG_SPELL_TOUCH_HAND_HIT, victim, caster, hand)
+ if(!cast_on_hand_hit(hand, victim, caster))
+ return
+
+ log_combat(caster, victim, "cast the touch spell [name] on", hand)
+ spell_feedback()
+ remove_hand(caster)
+
+/**
+ * Calls do_secondary_hand_hit() from the caster onto the victim.
+ */
+/datum/action/cooldown/spell/touch/proc/do_secondary_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster)
+ var/secondary_result = cast_on_secondary_hand_hit(hand, victim, caster)
+ switch(secondary_result)
+ // Continue will remove the hand here and stop
+ if(SECONDARY_ATTACK_CONTINUE_CHAIN)
+ log_combat(caster, victim, "cast the touch spell [name] on", hand, "(secondary / alt cast)")
+ spell_feedback()
+ remove_hand(caster)
+
+ // Call normal will call the normal cast proc
+ if(SECONDARY_ATTACK_CALL_NORMAL)
+ do_hand_hit(hand, victim, caster)
+
+ // Cancel chain will do nothing,
+ if(SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN)
+ return
+
+/**
+ * The actual process of casting the spell on the victim from the caster.
+ *
+ * Override / extend this to implement casting effects.
+ * Return TRUE on a successful cast to use up the hand (delete it)
+ * Return FALSE to do nothing and let them keep the hand in hand
+ */
+/datum/action/cooldown/spell/touch/proc/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster)
+ return FALSE
+
+/**
+ * For any special casting effects done if the user right-clicks
+ * on touch spell instead of left-clicking
+ *
+ * Return SECONDARY_ATTACK_CALL_NORMAL to call the normal cast_on_hand_hit
+ * Return SECONDARY_ATTACK_CONTINUE_CHAIN to prevent the normal cast_on_hand_hit from calling, but still use up the hand
+ * Return SECONDARY_ATTACK_CANCEL_CHAIN to prevent the spell from being used
+ */
+/datum/action/cooldown/spell/touch/proc/cast_on_secondary_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster)
+ return SECONDARY_ATTACK_CALL_NORMAL
+
+/**
+ * Signal proc for [COMSIG_PARENT_QDELETING] from our attached hand.
+ *
+ * If our hand is deleted for a reason unrelated to our spell,
+ * unlink it (clear refs) and revert the cooldown
+ */
+/datum/action/cooldown/spell/touch/proc/on_hand_deleted(datum/source)
+ SIGNAL_HANDLER
+
+ remove_hand(reset_cooldown_after = TRUE)
+
+/**
+ * Signal proc for [COMSIG_ITEM_DROPPED] from our attached hand.
+ *
+ * If our caster drops the hand, remove the hand / revert the cast
+ * Basically gives them an easy hotkey to lose their hand without needing to click the button
+ */
+/datum/action/cooldown/spell/touch/proc/on_hand_dropped(datum/source, mob/living/dropper)
+ SIGNAL_HANDLER
+
+ remove_hand(dropper, reset_cooldown_after = TRUE)
+
+/obj/item/melee/touch_attack
+ name = "\improper outstretched hand"
+ desc = "High Five?"
+ icon = 'icons/obj/items_and_weapons.dmi'
+ lefthand_file = 'icons/mob/inhands/misc/touchspell_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/misc/touchspell_righthand.dmi'
+ icon_state = "latexballon"
+ inhand_icon_state = null
+ item_flags = NEEDS_PERMIT | ABSTRACT
+ w_class = WEIGHT_CLASS_HUGE
+ force = 0
+ throwforce = 0
+ throw_range = 0
+ throw_speed = 0
+ /// A weakref to what spell made us.
+ var/datum/weakref/spell_which_made_us
+
+/obj/item/melee/touch_attack/Initialize(mapload, datum/action/cooldown/spell/spell)
+ . = ..()
+
+ if(spell)
+ spell_which_made_us = WEAKREF(spell)
+
+/obj/item/melee/touch_attack/attack(mob/target, mob/living/carbon/user)
+ if(!iscarbon(user)) //Look ma, no hands
+ return TRUE
+ if(!(user.mobility_flags & MOBILITY_USE))
+ to_chat(user, span_warning("You can't reach out!"))
+ return TRUE
+ return ..()
+
+/**
+ * When the hand component of a touch spell is qdel'd, (the hand is dropped or otherwise lost),
+ * the cooldown on the spell that made it is automatically refunded.
+ *
+ * However, if you want to consume the hand and not give a cooldown,
+ * such as adding a unique behavior to the hand specifically, this function will do that.
+ */
+/obj/item/melee/touch_attack/mansus_fist/proc/remove_hand_with_no_refund(mob/holder)
+ var/datum/action/cooldown/spell/touch/hand_spell = spell_which_made_us?.resolve()
+ if(!QDELETED(hand_spell))
+ hand_spell.remove_hand(holder, reset_cooldown_after = FALSE)
+ return
+
+ // We have no spell associated for some reason, just delete us as normal.
+ holder.temporarilyRemoveItemFromInventory(src, force = TRUE)
+ qdel(src)
diff --git a/code/modules/spells/spell_types/touch/duffelbag_curse.dm b/code/modules/spells/spell_types/touch/duffelbag_curse.dm
new file mode 100644
index 0000000000000..0416350ca6924
--- /dev/null
+++ b/code/modules/spells/spell_types/touch/duffelbag_curse.dm
@@ -0,0 +1,87 @@
+
+/datum/action/cooldown/spell/touch/duffelbag
+ name = "Bestow Cursed Duffel Bag"
+ desc = "A spell that summons a duffel bag demon on the target, slowing them down and slowly eating them."
+ button_icon_state = "duffelbag_curse"
+ sound = 'sound/magic/mm_hit.ogg'
+
+ school = SCHOOL_CONJURATION
+ cooldown_time = 6 SECONDS
+ cooldown_reduction_per_rank = 1 SECONDS
+
+ invocation = "HU'SWCH H'ANS!!"
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+ hand_path = /obj/item/melee/touch_attack/duffelbag
+
+/datum/action/cooldown/spell/touch/duffelbag/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster)
+ if(!iscarbon(victim))
+ return FALSE
+
+ var/mob/living/carbon/duffel_victim = victim
+ var/static/list/elaborate_backstory = list(
+ "spacewar origin story",
+ "military background",
+ "corporate connections",
+ "life in the colonies",
+ "anti-government activities",
+ "upbringing on the space farm",
+ "fond memories with your buddy Keith",
+ )
+ if(duffel_victim.can_block_magic(antimagic_flags))
+ to_chat(caster, span_warning("The spell can't seem to affect [duffel_victim]!"))
+ to_chat(duffel_victim, span_warning("You really don't feel like talking about your [pick(elaborate_backstory)] with complete strangers today."))
+ return TRUE
+
+ // To get it started, stun and knockdown the person being hit
+ duffel_victim.flash_act()
+ duffel_victim.Immobilize(5 SECONDS)
+ duffel_victim.apply_damage(80, STAMINA)
+ duffel_victim.Knockdown(5 SECONDS)
+
+ // If someone's already cursed, don't try to give them another
+ if(HAS_TRAIT(duffel_victim, TRAIT_DUFFEL_CURSE_PROOF))
+ to_chat(caster, span_warning("The burden of [duffel_victim]'s duffel bag becomes too much, shoving them to the floor!"))
+ to_chat(duffel_victim, span_warning("The weight of this bag becomes overburdening!"))
+ return TRUE
+
+ // However if they're uncursed, they're fresh for getting a cursed bag
+ var/obj/item/storage/backpack/duffelbag/cursed/conjured_duffel = new get_turf(victim)
+ duffel_victim.visible_message(
+ span_danger("A growling duffel bag appears on [duffel_victim]!"),
+ span_danger("You feel something attaching itself to you, and a strong desire to discuss your [pick(elaborate_backstory)] at length!"),
+ )
+
+ // This duffelbag is now cuuuurrrsseed! Equip it on them
+ ADD_TRAIT(conjured_duffel, TRAIT_DUFFEL_CURSE_PROOF, CURSED_ITEM_TRAIT(conjured_duffel.name))
+ conjured_duffel.pickup(duffel_victim)
+ conjured_duffel.forceMove(duffel_victim)
+
+ // Put it on their back first
+ if(duffel_victim.dropItemToGround(duffel_victim.back))
+ duffel_victim.equip_to_slot_if_possible(conjured_duffel, ITEM_SLOT_BACK, TRUE, TRUE)
+ return TRUE
+
+ // If the back equip failed, put it in their hands first
+ if(duffel_victim.put_in_hands(conjured_duffel))
+ return TRUE
+
+ // If they had no empty hands, try to put it in their inactive hand first
+ duffel_victim.dropItemToGround(duffel_victim.get_inactive_held_item())
+ if(duffel_victim.put_in_hands(conjured_duffel))
+ return TRUE
+
+ // If their inactive hand couldn't be emptied or found, put it in their active hand
+ duffel_victim.dropItemToGround(duffel_victim.get_active_held_item())
+ if(duffel_victim.put_in_hands(conjured_duffel))
+ return TRUE
+
+ // Well, we failed to give them the duffel bag,
+ // but technically we still stunned them so that's something
+ return TRUE
+
+/obj/item/melee/touch_attack/duffelbag
+ name = "\improper burdening touch"
+ desc = "Where is the bar from here?"
+ icon_state = "duffelcurse"
+ inhand_icon_state = "duffelcurse"
diff --git a/code/modules/spells/spell_types/touch/flesh_to_stone.dm b/code/modules/spells/spell_types/touch/flesh_to_stone.dm
new file mode 100644
index 0000000000000..c6693c7c904e3
--- /dev/null
+++ b/code/modules/spells/spell_types/touch/flesh_to_stone.dm
@@ -0,0 +1,33 @@
+/datum/action/cooldown/spell/touch/flesh_to_stone
+ name = "Flesh to Stone"
+ desc = "This spell charges your hand with the power to turn victims into inert statues for a long period of time."
+ button_icon_state = "statue"
+ sound = 'sound/magic/fleshtostone.ogg'
+
+ school = SCHOOL_TRANSMUTATION
+ cooldown_time = 1 MINUTES
+ cooldown_reduction_per_rank = 10 SECONDS
+
+ invocation = "STAUN EI!!"
+
+ hand_path = /obj/item/melee/touch_attack/flesh_to_stone
+
+/datum/action/cooldown/spell/touch/flesh_to_stone/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster)
+ if(!isliving(victim))
+ return FALSE
+
+ var/mob/living/living_victim = victim
+ if(living_victim.can_block_magic(antimagic_flags))
+ to_chat(caster, span_warning("The spell can't seem to affect [victim]!"))
+ to_chat(victim, span_warning("You feel your flesh turn to stone for a moment, then revert back!"))
+ return TRUE
+
+ living_victim.Stun(4 SECONDS)
+ living_victim.petrify()
+ return TRUE
+
+/obj/item/melee/touch_attack/flesh_to_stone
+ name = "\improper petrifying touch"
+ desc = "That's the bottom line, because flesh to stone said so!"
+ icon_state = "fleshtostone"
+ inhand_icon_state = "fleshtostone"
diff --git a/code/modules/spells/spell_types/touch/smite.dm b/code/modules/spells/spell_types/touch/smite.dm
new file mode 100644
index 0000000000000..d39ddb3e30ce4
--- /dev/null
+++ b/code/modules/spells/spell_types/touch/smite.dm
@@ -0,0 +1,55 @@
+/datum/action/cooldown/spell/touch/smite
+ name = "Smite"
+ desc = "This spell charges your hand with an unholy energy \
+ that can be used to cause a touched victim to violently explode."
+ button_icon_state = "gib"
+ sound = 'sound/magic/disintegrate.ogg'
+
+ school = SCHOOL_EVOCATION
+ cooldown_time = 1 MINUTES
+ cooldown_reduction_per_rank = 10 SECONDS
+
+ invocation = "EI NATH!!"
+ sparks_amt = 4
+
+ hand_path = /obj/item/melee/touch_attack/smite
+
+/datum/action/cooldown/spell/touch/smite/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster)
+ if(!isliving(victim))
+ return FALSE
+
+ do_sparks(sparks_amt, FALSE, get_turf(victim))
+ for(var/mob/living/nearby_spectator in view(caster, 7))
+ if(nearby_spectator == caster)
+ continue
+ nearby_spectator.flash_act(affect_silicon = FALSE)
+
+ var/mob/living/living_victim = victim
+ if(living_victim.can_block_magic(antimagic_flags))
+ caster.visible_message(
+ span_warning("The feedback blows [caster]'s arm off!"),
+ span_userdanger("The spell bounces from [living_victim]'s skin back into your arm!"),
+ )
+ caster.flash_act()
+ var/obj/item/bodypart/to_dismember = caster.get_holding_bodypart_of_item(hand)
+ to_dismember?.dismember()
+ return TRUE
+
+ if(ishuman(victim))
+ var/mob/living/carbon/human/human_victim = victim
+ var/obj/item/clothing/suit/worn_suit = human_victim.wear_suit
+ if(istype(worn_suit, /obj/item/clothing/suit/hooded/bloated_human))
+ human_victim.visible_message(span_danger("[victim]'s [worn_suit] explodes off of them into a puddle of gore!"))
+ human_victim.dropItemToGround(worn_suit)
+ qdel(worn_suit)
+ new /obj/effect/gibspawner(get_turf(victim))
+ return TRUE
+
+ living_victim.gib()
+ return TRUE
+
+/obj/item/melee/touch_attack/smite
+ name = "\improper smiting touch"
+ desc = "This hand of mine glows with an awesome power!"
+ icon_state = "disintegrate"
+ inhand_icon_state = "disintegrate"
diff --git a/code/modules/spells/spell_types/touch_attacks.dm b/code/modules/spells/spell_types/touch_attacks.dm
deleted file mode 100644
index 6267706074fb2..0000000000000
--- a/code/modules/spells/spell_types/touch_attacks.dm
+++ /dev/null
@@ -1,96 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/touch
- var/hand_path = /obj/item/melee/touch_attack
- var/obj/item/melee/touch_attack/attached_hand = null
- var/drawmessage = "You channel the power of the spell to your hand."
- var/dropmessage = "You draw the power out of your hand."
- invocation_type = INVOCATION_NONE //you scream on connecting, not summoning
- include_user = TRUE
- range = -1
-
-/obj/effect/proc_holder/spell/targeted/touch/Destroy()
- remove_hand()
- if(action?.owner)
- var/mob/guy_who_needs_to_know = action.owner
- to_chat(guy_who_needs_to_know, span_notice("The power of the spell dissipates from your hand."))
- return ..()
-
-/obj/effect/proc_holder/spell/targeted/touch/proc/remove_hand(recharge = FALSE)
- QDEL_NULL(attached_hand)
- if(recharge)
- charge_counter = charge_max
-
-/obj/effect/proc_holder/spell/targeted/touch/proc/on_hand_destroy(obj/item/melee/touch_attack/hand)
- if(hand != attached_hand)
- CRASH("Incorrect touch spell hand.")
- //Start recharging.
- attached_hand = null
- recharging = TRUE
- action.UpdateButtons()
-
-/obj/effect/proc_holder/spell/targeted/touch/cast(list/targets,mob/user = usr)
- if(!QDELETED(attached_hand))
- remove_hand(TRUE)
- to_chat(user, span_notice("[dropmessage]"))
- return
-
- for(var/mob/living/carbon/C in targets)
- if(!attached_hand)
- if(ChargeHand(C))
- recharging = FALSE
- return
-
-/obj/effect/proc_holder/spell/targeted/touch/charge_check(mob/user,silent = FALSE)
- if(!QDELETED(attached_hand)) //Charge doesn't matter when putting the hand away.
- return TRUE
- else
- return ..()
-
-/obj/effect/proc_holder/spell/targeted/touch/proc/ChargeHand(mob/living/carbon/user)
- attached_hand = new hand_path(src)
- attached_hand.attached_spell = src
- if(!user.put_in_hands(attached_hand))
- remove_hand(TRUE)
- if (user.usable_hands == 0)
- to_chat(user, span_warning("You dont have any usable hands!"))
- else
- to_chat(user, span_warning("Your hands are full!"))
- return FALSE
- to_chat(user, span_notice("[drawmessage]"))
- return TRUE
-
-
-/obj/effect/proc_holder/spell/targeted/touch/disintegrate
- name = "Smite"
- desc = "This spell charges your hand with an unholy energy that can be used to cause a touched victim to violently explode."
- hand_path = /obj/item/melee/touch_attack/disintegrate
-
- school = SCHOOL_EVOCATION
- charge_max = 600
- clothes_req = TRUE
- cooldown_min = 200 //100 deciseconds reduction per rank
-
- action_icon_state = "gib"
-
-/obj/effect/proc_holder/spell/targeted/touch/flesh_to_stone
- name = "Flesh to Stone"
- desc = "This spell charges your hand with the power to turn victims into inert statues for a long period of time."
- hand_path = /obj/item/melee/touch_attack/fleshtostone
-
- school = SCHOOL_TRANSMUTATION
- charge_max = 600
- clothes_req = TRUE
- cooldown_min = 200 //100 deciseconds reduction per rank
-
- action_icon_state = "statue"
- sound = 'sound/magic/fleshtostone.ogg'
-
-/obj/effect/proc_holder/spell/targeted/touch/duffelbag
- name = "Bestow Cursed Duffel Bag"
- desc = "A spell that summons a duffel bag demon on the target, slowing them down and slowly eating them."
- hand_path = /obj/item/melee/touch_attack/duffelbag
- action_icon_state = "duffelbag_curse"
-
- school = SCHOOL_CONJURATION
- charge_max = 60
- clothes_req = FALSE
- cooldown_min = 20
diff --git a/code/modules/spells/spell_types/trigger.dm b/code/modules/spells/spell_types/trigger.dm
deleted file mode 100644
index c13c96686c6df..0000000000000
--- a/code/modules/spells/spell_types/trigger.dm
+++ /dev/null
@@ -1,26 +0,0 @@
-/obj/effect/proc_holder/spell/pointed/trigger
- name = "Trigger"
- desc = "This spell triggers another spell or a few."
- var/list/linked_spells = list() //those are just referenced by the trigger spell and are unaffected by it directly
- var/list/starting_spells = list() //those are added on New() to contents from default spells and are deleted when the trigger spell is deleted to prevent memory leaks
-
-/obj/effect/proc_holder/spell/pointed/trigger/Initialize(mapload)
- . = ..()
- for(var/spell in starting_spells)
- var/spell_to_add = text2path(spell)
- new spell_to_add(src) //should result in adding to contents, needs testing
-
-/obj/effect/proc_holder/spell/pointed/trigger/Destroy()
- for(var/spell in contents)
- qdel(spell)
- linked_spells = null
- starting_spells = null
- return ..()
-
-/obj/effect/proc_holder/spell/pointed/trigger/cast(list/targets, mob/user = usr)
- playMagSound()
- for(var/mob/living/target in targets)
- for(var/obj/effect/proc_holder/spell/spell in contents)
- spell.perform(list(target),0)
- for(var/obj/effect/proc_holder/spell/spell in linked_spells)
- spell.perform(list(target),0)
diff --git a/code/modules/spells/spell_types/turf_teleport.dm b/code/modules/spells/spell_types/turf_teleport.dm
deleted file mode 100644
index d1c4813bc6765..0000000000000
--- a/code/modules/spells/spell_types/turf_teleport.dm
+++ /dev/null
@@ -1,46 +0,0 @@
-/obj/effect/proc_holder/spell/targeted/turf_teleport
- name = "Turf Teleport"
- desc = "This spell teleports the target to the turf in range."
- nonabstract_req = TRUE
-
- school = SCHOOL_TRANSLOCATION
-
- var/inner_tele_radius = 1
- var/outer_tele_radius = 2
-
- var/include_space = FALSE //whether it includes space tiles in possible teleport locations
- var/include_dense = FALSE //whether it includes dense tiles in possible teleport locations
- var/sound1 = 'sound/weapons/zapbang.ogg'
- var/sound2 = 'sound/weapons/zapbang.ogg'
-
-/obj/effect/proc_holder/spell/targeted/turf_teleport/cast(list/targets,mob/user = usr)
- playsound(get_turf(user), sound1, 50,TRUE)
- for(var/mob/living/target in targets)
- var/list/turfs = new/list()
- for(var/turf/T as anything in RANGE_TURFS(outer_tele_radius, target))
- if(T in RANGE_TURFS(inner_tele_radius, target))
- continue
- if(isspaceturf(T) && !include_space)
- continue
- if(T.density && !include_dense)
- continue
- if(T.x>world.maxx-outer_tele_radius || T.xworld.maxy-outer_tele_radius || T.y 3)
+ log_tgui(user, "Error: TGUI Alert initiated with too many buttons. Use a list.", "TguiAlert")
+ return tgui_input_list(user, message, title, buttons, timeout, autofocus)
+ // Client does NOT have tgui_input on: Returns regular input
+ if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
+ if(length(buttons) == 2)
+ return alert(user, message, title, buttons[1], buttons[2])
+ if(length(buttons) == 3)
+ return alert(user, message, title, buttons[1], buttons[2], buttons[3])
+ var/datum/tgui_alert/alert = new(user, message, title, buttons, timeout, autofocus)
+ alert.ui_interact(user)
+ alert.wait()
+ if (alert)
+ . = alert.choice
+ qdel(alert)
+
+/**
+ * # tgui_alert
+ *
+ * Datum used for instantiating and using a TGUI-controlled modal that prompts the user with
+ * a message and has buttons for responses.
+ */
+/datum/tgui_alert
+ /// The title of the TGUI window
+ var/title
+ /// The textual body of the TGUI window
+ var/message
+ /// The list of buttons (responses) provided on the TGUI window
+ var/list/buttons
+ /// The button that the user has pressed, null if no selection has been made
+ var/choice
+ /// The time at which the tgui_alert was created, for displaying timeout progress.
+ var/start_time
+ /// The lifespan of the tgui_alert, after which the window will close and delete itself.
+ var/timeout
+ /// The bool that controls if this modal should grab window focus
+ var/autofocus
+ /// Boolean field describing if the tgui_alert was closed by the user.
+ var/closed
+
+/datum/tgui_alert/New(mob/user, message, title, list/buttons, timeout, autofocus)
+ src.autofocus = autofocus
+ src.buttons = buttons.Copy()
+ src.message = message
+ src.title = title
+ if (timeout)
+ src.timeout = timeout
+ start_time = world.time
+ QDEL_IN(src, timeout)
+
+/datum/tgui_alert/Destroy(force, ...)
+ SStgui.close_uis(src)
+ QDEL_NULL(buttons)
+ return ..()
+
+/**
+ * Waits for a user's response to the tgui_alert's prompt before returning. Returns early if
+ * the window was closed by the user.
+ */
+/datum/tgui_alert/proc/wait()
+ while (!choice && !closed && !QDELETED(src))
+ stoplag(1)
+
+/datum/tgui_alert/ui_interact(mob/user, datum/tgui/ui)
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(!ui)
+ ui = new(user, src, "AlertModal")
+ ui.open()
+
+/datum/tgui_alert/ui_close(mob/user)
+ . = ..()
+ closed = TRUE
+
+/datum/tgui_alert/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/tgui_alert/ui_static_data(mob/user)
+ var/list/data = list()
+ data["autofocus"] = autofocus
+ data["buttons"] = buttons
+ data["message"] = message
+ data["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
+ data["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
+ data["title"] = title
+ return data
+
+/datum/tgui_alert/ui_data(mob/user)
+ var/list/data = list()
+ if(timeout)
+ data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+ return data
+
+/datum/tgui_alert/ui_act(action, list/params)
+ . = ..()
+ if (.)
+ return
+ switch(action)
+ if("choose")
+ if (!(params["choice"] in buttons))
+ CRASH("[usr] entered a non-existent button choice: [params["choice"]]")
+ set_choice(params["choice"])
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+ if("cancel")
+ closed = TRUE
+ SStgui.close_uis(src)
+ return TRUE
+
+/datum/tgui_alert/proc/set_choice(choice)
+ src.choice = choice
diff --git a/code/modules/tgui/tgui_input_list.dm b/code/modules/tgui_input/list.dm
similarity index 61%
rename from code/modules/tgui/tgui_input_list.dm
rename to code/modules/tgui_input/list.dm
index 8a38ec768b8d5..abac3ec43561b 100644
--- a/code/modules/tgui/tgui_input_list.dm
+++ b/code/modules/tgui_input/list.dm
@@ -31,36 +31,6 @@
. = input.choice
qdel(input)
-/**
- * Creates an asynchronous TGUI input list window with an associated callback.
- *
- * This proc should be used to create inputs that invoke a callback with the user's chosen option.
- * Arguments:
- * * user - The user to show the input box to.
- * * message - The content of the input box, shown in the body of the TGUI window.
- * * title - The title of the input box, shown on the top of the TGUI window.
- * * items - The options that can be chosen by the user, each string is assigned a button on the UI.
- * * default - If an option is already preselected on the UI. Current values, etc.
- * * callback - The callback to be invoked when a choice is made.
- * * timeout - The timeout of the input box, after which the menu will close and qdel itself. Set to zero for no timeout.
- */
-/proc/tgui_input_list_async(mob/user, message, title = "Select", list/items, default, datum/callback/callback, timeout = 60 SECONDS)
- if (!user)
- user = usr
- if(!length(items))
- return
- if (!istype(user))
- if (istype(user, /client))
- var/client/client = user
- user = client.mob
- else
- return
- /// Client does NOT have tgui_input on: Returns regular input
- if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
- return input(user, message, title) as null|anything in items
- var/datum/tgui_list_input/async/input = new(user, message, title, items, default, callback, timeout)
- input.ui_interact(user)
-
/**
* # tgui_list_input
*
@@ -94,22 +64,16 @@
src.items_map = list()
src.default = default
var/list/repeat_items = list()
-
// Gets rid of illegal characters
var/static/regex/whitelistedWords = regex(@{"([^\u0020-\u8000]+)"})
-
for(var/i in items)
if(!i)
continue
-
var/string_key = whitelistedWords.Replace("[i]", "")
-
//avoids duplicated keys E.g: when areas have the same name
string_key = avoid_assoc_duplicate_keys(string_key, repeat_items)
-
src.items += string_key
src.items_map[string_key] = i
-
if (timeout)
src.timeout = timeout
start_time = world.time
@@ -118,7 +82,7 @@
/datum/tgui_list_input/Destroy(force, ...)
SStgui.close_uis(src)
QDEL_NULL(items)
- . = ..()
+ return ..()
/**
* Waits for a user's response to the tgui_list_input's prompt before returning. Returns early if
@@ -142,18 +106,20 @@
return GLOB.always_state
/datum/tgui_list_input/ui_static_data(mob/user)
- . = list()
- .["init_value"] = default || items[1]
- .["items"] = items
- .["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
- .["message"] = message
- .["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
- .["title"] = title
+ var/list/data = list()
+ data["init_value"] = default || items[1]
+ data["items"] = items
+ data["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
+ data["message"] = message
+ data["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
+ data["title"] = title
+ return data
/datum/tgui_list_input/ui_data(mob/user)
- . = list()
+ var/list/data = list()
if(timeout)
- .["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
+ data["timeout"] = clamp((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS), 0, 1)
+ return data
/datum/tgui_list_input/ui_act(action, list/params)
. = ..()
@@ -174,28 +140,3 @@
/datum/tgui_list_input/proc/set_choice(choice)
src.choice = choice
-
-/**
- * # async tgui_list_input
- *
- * An asynchronous version of tgui_list_input to be used with callbacks instead of waiting on user responses.
- */
-/datum/tgui_list_input/async
- /// The callback to be invoked by the tgui_list_input upon having a choice made.
- var/datum/callback/callback
-
-/datum/tgui_list_input/async/New(mob/user, message, title, list/items, default, callback, timeout)
- ..(user, message, title, items, default, timeout)
- src.callback = callback
-
-/datum/tgui_list_input/async/Destroy(force, ...)
- QDEL_NULL(callback)
- . = ..()
-
-/datum/tgui_list_input/async/set_choice(choice)
- . = ..()
- if(!isnull(src.choice))
- callback?.InvokeAsync(src.choice)
-
-/datum/tgui_list_input/async/wait()
- return
diff --git a/code/modules/tgui/tgui_input_number.dm b/code/modules/tgui_input/number.dm
similarity index 61%
rename from code/modules/tgui/tgui_input_number.dm
rename to code/modules/tgui_input/number.dm
index 9bf1981d2fc48..81964597df7d9 100644
--- a/code/modules/tgui/tgui_input_number.dm
+++ b/code/modules/tgui_input/number.dm
@@ -35,38 +35,6 @@
. = number_input.entry
qdel(number_input)
-/**
- * Creates an asynchronous TGUI number input window with an associated callback.
- *
- * This proc should be used to create number inputs that invoke a callback with the user's entry.
- *
- * Arguments:
- * * user - The user to show the number input to.
- * * message - The content of the number input, shown in the body of the TGUI window.
- * * title - The title of the number input modal, shown on the top of the TGUI window.
- * * default - The default (or current) value, shown as a placeholder. Users can press refresh with this.
- * * max_value - Specifies a maximum value. If none is set, any number can be entered. Pressing "max" defaults to 1000.
- * * min_value - Specifies a minimum value. Often 0.
- * * callback - The callback to be invoked when a choice is made.
- * * timeout - The timeout of the number input, after which the modal will close and qdel itself. Set to zero for no timeout.
- * * round_value - whether the inputted number is rounded down into an integer.
- */
-/proc/tgui_input_number_async(mob/user, message, title = "Number Input", default = 0, max_value = 10000, min_value = 0, datum/callback/callback, timeout = 60 SECONDS, round_value = TRUE)
- if (!user)
- user = usr
- if (!istype(user))
- if (istype(user, /client))
- var/client/client = user
- user = client.mob
- else
- return
- // Client does NOT have tgui_input on: Returns regular input
- if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
- var/input_number = input(user, message, title, default) as null|num
- return clamp(round_value ? round(input_number) : input_number, min_value, max_value)
- var/datum/tgui_input_number/async/number_input = new(user, message, title, default, max_value, min_value, callback, timeout, round_value)
- number_input.ui_interact(user)
-
/**
* # tgui_input_number
*
@@ -86,15 +54,14 @@
var/message
/// The minimum value that can be entered.
var/min_value
+ /// Whether the submitted number is rounded down into an integer.
+ var/round_value
/// The time at which the number input was created, for displaying timeout progress.
var/start_time
/// The lifespan of the number input, after which the window will close and delete itself.
var/timeout
/// The title of the TGUI window
var/title
- /// Whether the submitted number is rounded down into an integer.
- var/round_value
-
/datum/tgui_input_number/New(mob/user, message, title, default, max_value, min_value, timeout, round_value)
src.default = default
@@ -120,7 +87,7 @@
/datum/tgui_input_number/Destroy(force, ...)
SStgui.close_uis(src)
- . = ..()
+ return ..()
/**
* Waits for a user's response to the tgui_input_number's prompt before returning. Returns early if
@@ -144,19 +111,21 @@
return GLOB.always_state
/datum/tgui_input_number/ui_static_data(mob/user)
- . = list()
- .["init_value"] = default // Default is a reserved keyword
- .["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
- .["max_value"] = max_value
- .["message"] = message
- .["min_value"] = min_value
- .["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
- .["title"] = title
+ var/list/data = list()
+ data["init_value"] = default // Default is a reserved keyword
+ data["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
+ data["max_value"] = max_value
+ data["message"] = message
+ data["min_value"] = min_value
+ data["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
+ data["title"] = title
+ return data
/datum/tgui_input_number/ui_data(mob/user)
- . = list()
+ var/list/data = list()
if(timeout)
- .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+ data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+ return data
/datum/tgui_input_number/ui_act(action, list/params)
. = ..()
@@ -181,29 +150,4 @@
return TRUE
/datum/tgui_input_number/proc/set_entry(entry)
- src.entry = entry
-
-/**
- * # async tgui_input_number
- *
- * An asynchronous version of tgui_input_number to be used with callbacks instead of waiting on user responses.
- */
-/datum/tgui_input_number/async
- /// The callback to be invoked by the tgui_input_number upon having a choice made.
- var/datum/callback/callback
-
-/datum/tgui_input_number/async/New(mob/user, message, title, default, max_value, min_value, callback, timeout)
- ..(user, message, title, default, max_value, min_value, timeout)
- src.callback = callback
-
-/datum/tgui_input_number/async/Destroy(force, ...)
- QDEL_NULL(callback)
- . = ..()
-
-/datum/tgui_input_number/async/set_entry(entry)
- . = ..()
- if(!isnull(src.entry))
- callback?.InvokeAsync(src.entry)
-
-/datum/tgui_input_number/async/wait()
- return
+ src.entry = entry
diff --git a/code/modules/tgui_input/say_modal/modal.dm b/code/modules/tgui_input/say_modal/modal.dm
new file mode 100644
index 0000000000000..e101223835726
--- /dev/null
+++ b/code/modules/tgui_input/say_modal/modal.dm
@@ -0,0 +1,133 @@
+/** Assigned say modal of the client */
+/client/var/datum/tgui_say/tgui_say
+
+/**
+ * Creates a JSON encoded message to open TGUI say modals properly.
+ *
+ * Arguments:
+ * channel - The channel to open the modal in.
+ * Returns:
+ * string - A JSON encoded message to open the modal.
+ */
+/client/proc/tgui_say_create_open_command(channel)
+ var/message = TGUI_CREATE_MESSAGE("open", list(
+ channel = channel,
+ ))
+ return "\".output tgui_say.browser:update [message]\""
+
+/**
+ * The tgui say modal. This initializes an input window which hides until
+ * the user presses one of the speech hotkeys. Once something is entered, it will
+ * delegate the speech to the proper channel.
+ */
+/datum/tgui_say
+ /// The user who opened the window
+ var/client/client
+ /// Injury phrases to blurt out
+ var/list/hurt_phrases = list("GACK!", "GLORF!", "OOF!", "AUGH!", "OW!", "URGH!", "HRNK!")
+ /// Max message length
+ var/max_length = MAX_MESSAGE_LEN
+ /// The modal window
+ var/datum/tgui_window/window
+ /// Boolean for whether the tgui_say was opened by the user.
+ var/window_open
+
+/** Creates the new input window to exist in the background. */
+/datum/tgui_say/New(client/client, id)
+ src.client = client
+ window = new(client, id)
+ window.subscribe(src, .proc/on_message)
+ window.is_browser = TRUE
+
+/**
+ * After a brief period, injects the scripts into
+ * the window to listen for open commands.
+ */
+/datum/tgui_say/proc/initialize()
+ set waitfor = FALSE
+ // Sleep to defer initialization to after client constructor
+ sleep(3 SECONDS)
+ window.initialize(
+ strict_mode = TRUE,
+ fancy = TRUE,
+ inline_css = file("tgui/public/tgui-say.bundle.css"),
+ inline_js = file("tgui/public/tgui-say.bundle.js"),
+ );
+
+/**
+ * Ensures nothing funny is going on window load.
+ * Minimizes the window, sets max length, closes all
+ * typing and thinking indicators. This is triggered
+ * as soon as the window sends the "ready" message.
+ */
+/datum/tgui_say/proc/load()
+ window_open = FALSE
+ winshow(client, "tgui_say", FALSE)
+ window.send_message("props", list(
+ lightMode = client.prefs?.read_preference(/datum/preference/toggle/tgui_say_light_mode),
+ maxLength = max_length,
+ ))
+ stop_thinking()
+ return TRUE
+
+/**
+ * Sets the window as "opened" server side, though it is already
+ * visible to the user. We do this to set local vars &
+ * start typing (if enabled and in an IC channel). Logs the event.
+ *
+ * Arguments:
+ * payload - A list containing the channel the window was opened in.
+ */
+/datum/tgui_say/proc/open(payload)
+ if(!payload?["channel"])
+ CRASH("No channel provided to an open TGUI-Say")
+ window_open = TRUE
+ if(payload["channel"] != OOC_CHANNEL)
+ start_thinking()
+ if(client.typing_indicators)
+ log_speech_indicators("[key_name(client)] started typing at [loc_name(client.mob)], indicators enabled.")
+ else
+ log_speech_indicators("[key_name(client)] started typing at [loc_name(client.mob)], indicators DISABLED.")
+ return TRUE
+
+/**
+ * Closes the window serverside. Closes any open chat bubbles
+ * regardless of preference. Logs the event.
+ */
+/datum/tgui_say/proc/close()
+ window_open = FALSE
+ stop_thinking()
+ if(client.typing_indicators)
+ log_speech_indicators("[key_name(client)] stopped typing at [loc_name(client.mob)], indicators enabled.")
+ else
+ log_speech_indicators("[key_name(client)] stopped typing at [loc_name(client.mob)], indicators DISABLED.")
+
+/**
+ * The equivalent of ui_act, this waits on messages from the window
+ * and delegates actions.
+ */
+/datum/tgui_say/proc/on_message(type, payload)
+ if(type == "ready")
+ load()
+ return TRUE
+ if (type == "open")
+ open(payload)
+ return TRUE
+ if (type == "close")
+ close()
+ return TRUE
+ if (type == "thinking")
+ if(payload["mode"] == TRUE)
+ start_thinking()
+ return TRUE
+ if(payload["mode"] == FALSE)
+ stop_thinking()
+ return TRUE
+ return FALSE
+ if (type == "typing")
+ start_typing()
+ return TRUE
+ if (type == "entry" || type == "force")
+ handle_entry(type, payload)
+ return TRUE
+ return FALSE
diff --git a/code/modules/tgui_input/say_modal/speech.dm b/code/modules/tgui_input/say_modal/speech.dm
new file mode 100644
index 0000000000000..bf357133a7d55
--- /dev/null
+++ b/code/modules/tgui_input/say_modal/speech.dm
@@ -0,0 +1,92 @@
+/**
+ * Alters text when players are injured.
+ * Adds text, trims left and right side
+ *
+ * Arguments:
+ * payload - a string list containing entry & channel
+ * Returns:
+ * string - the altered entry
+ */
+/datum/tgui_say/proc/alter_entry(payload)
+ var/entry = payload["entry"]
+ /// No OOC leaks
+ if(!entry || payload["channel"] == OOC_CHANNEL || payload["channel"] == ME_CHANNEL)
+ return pick(hurt_phrases)
+ /// Random trimming for larger sentences
+ if(length(entry) > 50)
+ entry = trim(entry, rand(40, 50))
+ else
+ /// Otherwise limit trim to just last letter
+ if(length(entry) > 1)
+ entry = trim(entry, length(entry))
+ return entry + "-" + pick(hurt_phrases)
+
+/**
+ * Delegates the speech to the proper channel.
+ *
+ * Arguments:
+ * entry - the text to broadcast
+ * channel - the channel to broadcast in
+ * Returns:
+ * boolean - on success or failure
+ */
+/datum/tgui_say/proc/delegate_speech(entry, channel)
+ switch(channel)
+ if(SAY_CHANNEL)
+ client.mob.say_verb(entry)
+ return TRUE
+ if(RADIO_CHANNEL)
+ client.mob.say_verb(";" + entry)
+ return TRUE
+ if(ME_CHANNEL)
+ client.mob.me_verb(entry)
+ return TRUE
+ if(OOC_CHANNEL)
+ client.ooc(entry)
+ return TRUE
+ return FALSE
+
+/**
+ * Force say handler.
+ * Sends a message to the say modal to send its current value.
+ */
+/datum/tgui_say/proc/force_say()
+ window.send_message("force")
+ stop_typing()
+
+/**
+ * Makes the player force say what's in their current input box.
+ */
+/mob/living/carbon/human/proc/force_say()
+ if(stat != CONSCIOUS || !client?.tgui_say?.window_open)
+ return FALSE
+ client.tgui_say.force_say()
+ if(client.typing_indicators)
+ log_speech_indicators("[key_name(client)] FORCED to stop typing, indicators enabled.")
+ else
+ log_speech_indicators("[key_name(client)] FORCED to stop typing, indicators DISABLED.")
+
+/**
+ * Handles text entry and forced speech.
+ *
+ * Arguments:
+ * type - a string "entry" or "force" based on how this function is called
+ * payload - a string list containing entry & channel
+ * Returns:
+ * boolean - success or failure
+ */
+/datum/tgui_say/proc/handle_entry(type, payload)
+ if(!payload?["channel"] || !payload["entry"])
+ CRASH("[usr] entered in a null payload to the chat window.")
+ if(length(payload["entry"]) > max_length)
+ CRASH("[usr] has entered more characters than allowed into a TGUI-Say")
+ if(type == "entry")
+ delegate_speech(payload["entry"], payload["channel"])
+ return TRUE
+ if(type == "force")
+ var/target_channel = payload["channel"]
+ if(target_channel == ME_CHANNEL || target_channel == OOC_CHANNEL)
+ target_channel = SAY_CHANNEL // No ooc leaks
+ delegate_speech(alter_entry(payload), target_channel)
+ return TRUE
+ return FALSE
diff --git a/code/modules/tgui_input/say_modal/typing.dm b/code/modules/tgui_input/say_modal/typing.dm
new file mode 100644
index 0000000000000..60de7199b8f28
--- /dev/null
+++ b/code/modules/tgui_input/say_modal/typing.dm
@@ -0,0 +1,111 @@
+/// Thinking
+GLOBAL_DATUM_INIT(thinking_indicator, /mutable_appearance, mutable_appearance('icons/mob/talk.dmi', "default3", TYPING_LAYER))
+/// Typing
+GLOBAL_DATUM_INIT(typing_indicator, /mutable_appearance, mutable_appearance('icons/mob/talk.dmi', "default0", TYPING_LAYER))
+
+
+/** Creates a thinking indicator over the mob. */
+/mob/proc/create_thinking_indicator()
+ return
+
+/** Removes the thinking indicator over the mob. */
+/mob/proc/remove_thinking_indicator()
+ return
+
+/** Creates a typing indicator over the mob. */
+/mob/proc/create_typing_indicator()
+ return
+
+/** Removes the typing indicator over the mob. */
+/mob/proc/remove_typing_indicator()
+ return
+
+/** Removes any indicators and marks the mob as not speaking IC. */
+/mob/proc/remove_all_indicators()
+ return
+
+/mob/set_stat(new_stat)
+ . = ..()
+ if(.)
+ remove_all_indicators()
+
+/mob/Logout()
+ remove_all_indicators()
+ return ..()
+
+/// Whether or not to show a typing indicator when speaking. Defaults to on.
+/datum/preference/toggle/typing_indicator
+ category = PREFERENCE_CATEGORY_GAME_PREFERENCES
+ savefile_key = "typingIndicator"
+ savefile_identifier = PREFERENCE_PLAYER
+
+/datum/preference/toggle/typing_indicator/apply_to_client(client/client, value)
+ client?.typing_indicators = value
+
+/** Sets the mob as "thinking" - with indicator and variable thinking_IC */
+/datum/tgui_say/proc/start_thinking()
+ if(!window_open || !client.typing_indicators)
+ return FALSE
+ /// Special exemptions
+ if(isabductor(client.mob))
+ return FALSE
+ client.mob.thinking_IC = TRUE
+ client.mob.create_thinking_indicator()
+
+/** Removes typing/thinking indicators and flags the mob as not thinking */
+/datum/tgui_say/proc/stop_thinking()
+ client.mob?.remove_all_indicators()
+
+/**
+ * Handles the user typing. After a brief period of inactivity,
+ * signals the client mob to revert to the "thinking" icon.
+ */
+/datum/tgui_say/proc/start_typing()
+ client.mob.remove_thinking_indicator()
+ if(!window_open || !client.typing_indicators || !client.mob.thinking_IC)
+ return FALSE
+ client.mob.create_typing_indicator()
+ addtimer(CALLBACK(src, .proc/stop_typing), 5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE)
+
+/**
+ * Callback to remove the typing indicator after a brief period of inactivity.
+ * If the user was typing IC, the thinking indicator is shown.
+ */
+/datum/tgui_say/proc/stop_typing()
+ if(!client?.mob)
+ return FALSE
+ client.mob.remove_typing_indicator()
+ if(!window_open || !client.typing_indicators || !client.mob.thinking_IC)
+ return FALSE
+ client.mob.create_thinking_indicator()
+
+/// Overrides for overlay creation
+/mob/living/create_thinking_indicator()
+ if(thinking_indicator || typing_indicator || !thinking_IC || stat != CONSCIOUS )
+ return FALSE
+ add_overlay(GLOB.thinking_indicator)
+ thinking_indicator = TRUE
+
+/mob/living/remove_thinking_indicator()
+ if(!thinking_indicator)
+ return FALSE
+ cut_overlay(GLOB.thinking_indicator)
+ thinking_indicator = FALSE
+
+/mob/living/create_typing_indicator()
+ if(typing_indicator || thinking_indicator || !thinking_IC || stat != CONSCIOUS)
+ return FALSE
+ add_overlay(GLOB.typing_indicator)
+ typing_indicator = TRUE
+
+/mob/living/remove_typing_indicator()
+ if(!typing_indicator)
+ return FALSE
+ cut_overlay(GLOB.typing_indicator)
+ typing_indicator = FALSE
+
+/mob/living/remove_all_indicators()
+ thinking_IC = FALSE
+ remove_thinking_indicator()
+ remove_typing_indicator()
+
diff --git a/code/modules/tgui/tgui_input_text.dm b/code/modules/tgui_input/text.dm
similarity index 59%
rename from code/modules/tgui/tgui_input_text.dm
rename to code/modules/tgui_input/text.dm
index 62a5efeff8c27..1571ad6b55226 100644
--- a/code/modules/tgui/tgui_input_text.dm
+++ b/code/modules/tgui_input/text.dm
@@ -44,45 +44,7 @@
qdel(text_input)
/**
- * Creates an asynchronous TGUI text input window with an associated callback.
- *
- * This proc should be used to create text inputs that invoke a callback with the user's entry.
- * Arguments:
- * * user - The user to show the text input to.
- * * message - The content of the text input, shown in the body of the TGUI window.
- * * title - The title of the text input modal, shown on the top of the TGUI window.
- * * default - The default (or current) value, shown as a placeholder.
- * * max_length - Specifies a max length for input.
- * * multiline - Bool that determines if the input box is much larger. Good for large messages, laws, etc.
- * * encode - If toggled, input is filtered via html_encode. Setting this to FALSE gives raw input.
- * * callback - The callback to be invoked when a choice is made.
- */
-/proc/tgui_input_text_async(mob/user, message = "", title = "Text Input", default, max_length = MAX_MESSAGE_LEN, multiline = FALSE, encode = TRUE, datum/callback/callback, timeout = 60 SECONDS)
- if (!user)
- user = usr
- if (!istype(user))
- if (istype(user, /client))
- var/client/client = user
- user = client.mob
- else
- return
- // Client does NOT have tgui_input on: Returns regular input
- if(!user.client.prefs.read_preference(/datum/preference/toggle/tgui_input))
- if(encode)
- if(multiline)
- return stripped_multiline_input(user, message, title, default, max_length)
- else
- return stripped_input(user, message, title, default, max_length)
- else
- if(multiline)
- return input(user, message, title, default) as message|null
- else
- return input(user, message, title, default) as text|null
- var/datum/tgui_input_text/async/text_input = new(user, message, title, default, max_length, multiline, encode, callback, timeout)
- text_input.ui_interact(user)
-
-/**
- * # tgui_input_text
+ * tgui_input_text
*
* Datum used for instantiating and using a TGUI-controlled text input that prompts the user with
* a message and has an input for text entry.
@@ -109,7 +71,6 @@
/// The title of the TGUI window
var/title
-
/datum/tgui_input_text/New(mob/user, message, title, default, max_length, multiline, encode, timeout)
src.default = default
src.encode = encode
@@ -124,7 +85,7 @@
/datum/tgui_input_text/Destroy(force, ...)
SStgui.close_uis(src)
- . = ..()
+ return ..()
/**
* Waits for a user's response to the tgui_input_text's prompt before returning. Returns early if
@@ -148,19 +109,21 @@
return GLOB.always_state
/datum/tgui_input_text/ui_static_data(mob/user)
- . = list()
- .["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
- .["max_length"] = max_length
- .["message"] = message
- .["multiline"] = multiline
- .["placeholder"] = default // Default is a reserved keyword
- .["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
- .["title"] = title
+ var/list/data = list()
+ data["large_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_large)
+ data["max_length"] = max_length
+ data["message"] = message
+ data["multiline"] = multiline
+ data["placeholder"] = default // Default is a reserved keyword
+ data["swapped_buttons"] = user.client.prefs.read_preference(/datum/preference/toggle/tgui_input_swapped)
+ data["title"] = title
+ return data
/datum/tgui_input_text/ui_data(mob/user)
- . = list()
+ var/list/data = list()
if(timeout)
- .["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+ data["timeout"] = CLAMP01((timeout - (world.time - start_time) - 1 SECONDS) / (timeout - 1 SECONDS))
+ return data
/datum/tgui_input_text/ui_act(action, list/params)
. = ..()
@@ -172,7 +135,7 @@
if(length(params["entry"]) > max_length)
CRASH("[usr] typed a text string longer than the max length")
if(encode && (length(html_encode(params["entry"])) > max_length))
- to_chat(usr, span_notice("Input uses special characters, thus reducing the maximum length."))
+ to_chat(usr, span_notice("Your message was clipped due to special character usage."))
set_entry(params["entry"])
closed = TRUE
SStgui.close_uis(src)
@@ -182,32 +145,13 @@
SStgui.close_uis(src)
return TRUE
+/**
+ * Sets the return value for the tgui text proc.
+ * If html encoding is enabled, the text will be encoded.
+ * This can sometimes result in a string that is longer than the max length.
+ * If the string is longer than the max length, it will be clipped.
+ */
/datum/tgui_input_text/proc/set_entry(entry)
if(!isnull(entry))
var/converted_entry = encode ? html_encode(entry) : entry
src.entry = trim(converted_entry, max_length)
-
-/**
- * # async tgui_input_text
- *
- * An asynchronous version of tgui_input_text to be used with callbacks instead of waiting on user responses.
- */
-/datum/tgui_input_text/async
- // The callback to be invoked by the tgui_input_text upon having a choice made.
- var/datum/callback/callback
-
-/datum/tgui_input_text/async/New(mob/user, message, title, default, max_length, multiline, encode, callback, timeout)
- ..(user, message, title, default, max_length, multiline, encode, timeout)
- src.callback = callback
-
-/datum/tgui_input_text/async/Destroy(force, ...)
- QDEL_NULL(callback)
- . = ..()
-
-/datum/tgui_input_text/async/set_entry(entry)
- . = ..()
- if(!isnull(src.entry))
- callback?.InvokeAsync(src.entry)
-
-/datum/tgui_input_text/async/wait()
- return
diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm
index 7b6defbf69486..cae65ca90d98e 100644
--- a/code/modules/unit_tests/_unit_tests.dm
+++ b/code/modules/unit_tests/_unit_tests.dm
@@ -92,6 +92,7 @@
#include "emoting.dm"
#include "food_edibility_check.dm"
#include "gas_transfer.dm"
+#include "get_turf_pixel.dm"
#include "greyscale_config.dm"
#include "heretic_knowledge.dm"
#include "heretic_rituals.dm"
@@ -100,15 +101,20 @@
#include "hydroponics_self_mutations.dm"
#include "hydroponics_validate_genes.dm"
#include "keybinding_init.dm"
+#include "knockoff_component.dm"
#include "load_map_security.dm"
#include "machine_disassembly.dm"
#include "mapping.dm"
+#include "mecha_damage.dm"
#include "medical_wounds.dm"
#include "merge_type.dm"
#include "metabolizing.dm"
+#include "mindbound_actions.dm"
+#include "mob_faction.dm"
#include "mob_spawn.dm"
#include "modsuit.dm"
#include "modular_map_loader.dm"
+#include "novaflower_burn.dm"
#include "ntnetwork_tests.dm"
#include "nuke_cinematic.dm"
#include "objectives.dm"
@@ -128,14 +134,23 @@
#include "reagent_recipe_collisions.dm"
#include "resist.dm"
#include "say.dm"
+#include "screenshot_antag_icons.dm"
+#include "screenshot_basic.dm"
+#include "screenshot_humanoids.dm"
#include "security_officer_distribution.dm"
+#include "security_levels.dm"
#include "serving_tray.dm"
#include "siunit.dm"
+#include "slips.dm"
#include "spawn_humans.dm"
#include "spawn_mobs.dm"
#include "species_config_sanity.dm"
#include "species_unique_id.dm"
#include "species_whitelists.dm"
+#include "spell_invocations.dm"
+#include "spell_mindswap.dm"
+#include "spell_names.dm"
+#include "spell_shapeshift.dm"
#include "stack_singular_name.dm"
#include "stomach.dm"
#include "strippable.dm"
@@ -146,7 +161,7 @@
#include "timer_sanity.dm"
#include "traitor.dm"
#include "unit_test.dm"
-#include "wizard.dm"
+#include "wizard_loadout.dm"
#ifdef REFERENCE_TRACKING_DEBUG //Don't try and parse this file if ref tracking isn't turned on. IE: don't parse ref tracking please mr linter
#include "find_reference_sanity.dm"
#endif
diff --git a/code/modules/unit_tests/create_and_destroy.dm b/code/modules/unit_tests/create_and_destroy.dm
index c47aeba034582..586c383781aeb 100644
--- a/code/modules/unit_tests/create_and_destroy.dm
+++ b/code/modules/unit_tests/create_and_destroy.dm
@@ -90,6 +90,8 @@
ignore += typesof(/obj/item/toy/cards/cardhand)
//Needs a holodeck area linked to it which is not guarenteed to exist and technically is supposed to have a 1:1 relationship with computer anyway.
ignore += typesof(/obj/machinery/computer/holodeck)
+ //runtimes if not paired with a landmark
+ ignore += typesof(/obj/structure/industrial_lift)
var/list/cached_contents = spawn_at.contents.Copy()
var/baseturf_count = length(spawn_at.baseturfs)
diff --git a/code/modules/unit_tests/get_turf_pixel.dm b/code/modules/unit_tests/get_turf_pixel.dm
new file mode 100644
index 0000000000000..8cd292d3b6c0c
--- /dev/null
+++ b/code/modules/unit_tests/get_turf_pixel.dm
@@ -0,0 +1,11 @@
+///ensures that get_turf_pixel() returns turfs within the bounds of the map,
+///even when called on a movable with its sprite out of bounds
+/datum/unit_test/get_turf_pixel
+
+/datum/unit_test/get_turf_pixel/Run()
+ //we need long larry to peek over the top edge of the earth
+ var/turf/north = locate(1, world.maxy, run_loc_floor_bottom_left.z)
+
+ //hes really long, so hes really good at peaking over the edge of the map
+ var/mob/living/simple_animal/hostile/megafauna/colossus/long_larry = allocate(/mob/living/simple_animal/hostile/megafauna/colossus, north)
+ TEST_ASSERT(istype(get_turf_pixel(long_larry), /turf), "get_turf_pixel() isnt clamping a mob whos sprite is above the bounds of the world inside of the map.")
diff --git a/code/modules/unit_tests/knockoff_component.dm b/code/modules/unit_tests/knockoff_component.dm
new file mode 100644
index 0000000000000..a781fb9eb0d3d
--- /dev/null
+++ b/code/modules/unit_tests/knockoff_component.dm
@@ -0,0 +1,87 @@
+/// Test that the knockoff component will properly cause something
+/// with it applied to be knocked off when it should be.
+/datum/unit_test/knockoff_component
+
+/datum/unit_test/knockoff_component/Run()
+ var/mob/living/carbon/human/wears_the_glasses = allocate(/mob/living/carbon/human)
+ var/mob/living/carbon/human/shoves_the_guy = allocate(/mob/living/carbon/human)
+
+ // No pre-existing items have a 100% chance of being knocked off,
+ // so we'll just apply it to a relatively generic item (glasses)
+ var/obj/item/clothing/glasses/sunglasses/glasses = allocate(/obj/item/clothing/glasses/sunglasses)
+ glasses.AddComponent(/datum/component/knockoff, \
+ knockoff_chance = 100, \
+ target_zones = list(BODY_ZONE_PRECISE_EYES), \
+ slots_knockoffable = glasses.slot_flags)
+
+ // Save this for later, since we wanna reset our dummy positions even after they're shoved about.
+ var/turf/right_of_shover = locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)
+
+ // Position shover (bottom left) and the shovee (1 tile right of bottom left, no wall behind them)
+ shoves_the_guy.forceMove(run_loc_floor_bottom_left)
+ set_glasses_wearer(wears_the_glasses, right_of_shover, glasses)
+
+ TEST_ASSERT(wears_the_glasses.glasses == glasses, "Dummy failed to equip the glasses.")
+
+ // Test disarm, targeting chest
+ // A disarm targeting chest should not knockdown or lose glasses
+ shoves_the_guy.zone_selected = BODY_ZONE_CHEST
+ shoves_the_guy.disarm(wears_the_glasses)
+ TEST_ASSERT(!wears_the_glasses.IsKnockdown(), "Dummy was knocked down when being disarmed shouldn't have been.")
+ TEST_ASSERT(wears_the_glasses.glasses == glasses, "Dummy lost their glasses even thought they were disarmed targeting the wrong slot.")
+
+ set_glasses_wearer(wears_the_glasses, right_of_shover, glasses)
+
+ // Test disarm, targeting eyes
+ // A disarm targeting eyes should not knockdown but should lose glasses
+ shoves_the_guy.zone_selected = BODY_ZONE_PRECISE_EYES
+ shoves_the_guy.disarm(wears_the_glasses)
+ TEST_ASSERT(!wears_the_glasses.IsKnockdown(), "Dummy was knocked down when being disarmed shouldn't have been.")
+ TEST_ASSERT(wears_the_glasses.glasses != glasses, "Dummy kept their glasses, even though they were shoved targeting the correct zone.")
+
+ set_glasses_wearer(wears_the_glasses, right_of_shover, glasses)
+
+ // Test Knockdown()
+ // Any amount of positive Kockdown should lose glasses
+ wears_the_glasses.Knockdown(1 SECONDS)
+ TEST_ASSERT(wears_the_glasses.IsKnockdown(), "Dummy wasn't knocked down after Knockdown() was called.")
+ TEST_ASSERT(wears_the_glasses.glasses != glasses, "Dummy kept their glasses, even though they knocked down by Knockdown().")
+
+ set_glasses_wearer(wears_the_glasses, right_of_shover, glasses)
+
+ // Test AdjustKnockdown()
+ // Any amount of positive Kockdown should lose glasses
+ wears_the_glasses.AdjustKnockdown(1 SECONDS)
+ TEST_ASSERT(wears_the_glasses.IsKnockdown(), "Dummy wasn't knocked down after AdjustKnockdown() was called.")
+ TEST_ASSERT(wears_the_glasses.glasses != glasses, "Dummy kept their glasses, even though they knocked down by AdjustKnockdown().")
+
+ set_glasses_wearer(wears_the_glasses, right_of_shover, glasses)
+
+ // Test SetKnockdown()
+ // Any amount of positive Kockdown should lose glasses
+ wears_the_glasses.SetKnockdown(1 SECONDS)
+ TEST_ASSERT(wears_the_glasses.IsKnockdown(), "Dummy wasn't knocked down after SetKnockdown() was called.")
+ TEST_ASSERT(wears_the_glasses.glasses != glasses, "Dummy kept their glasses, even though they knocked down by SetKnockdown().")
+
+ set_glasses_wearer(wears_the_glasses, right_of_shover, glasses)
+
+ // Test a negative value applied of Knockdown (AdjustKnockdown, SetKnockdown, and Knockdown should all act the same here)
+ // Any amount of negative Kockdown should not cause the glasses to be lost
+ wears_the_glasses.AdjustKnockdown(-1 SECONDS)
+ TEST_ASSERT(!wears_the_glasses.IsKnockdown(), "Dummy was knocked down after AdjustKnockdown() was called with a negative value.")
+ TEST_ASSERT(wears_the_glasses.glasses == glasses, "Dummy lost their glasses, even though AdjustKnockdown() was called with a negative value.")
+
+ // Bonus check: A wallshove should definitely cause them to be lost
+ wears_the_glasses.forceMove(shoves_the_guy.loc)
+ shoves_the_guy.forceMove(right_of_shover)
+
+ shoves_the_guy.zone_selected = BODY_ZONE_CHEST
+ shoves_the_guy.disarm(wears_the_glasses)
+ TEST_ASSERT(wears_the_glasses.glasses != glasses, "Dummy kept their glasses, even though were disarm shoved into a wall.")
+
+/// Helper to reset the glasses dummy back to it's original position, clear knockdown, and return glasses (if gone)
+/datum/unit_test/knockoff_component/proc/set_glasses_wearer(mob/living/carbon/human/wearer, turf/reset_to, obj/item/clothing/glasses/reset_worn)
+ wearer.forceMove(reset_to)
+ wearer.SetKnockdown(0 SECONDS)
+ if(!wearer.glasses)
+ wearer.equip_to_slot_if_possible(reset_worn, ITEM_SLOT_EYES)
diff --git a/code/modules/unit_tests/mecha_damage.dm b/code/modules/unit_tests/mecha_damage.dm
new file mode 100644
index 0000000000000..6bc5e37069f32
--- /dev/null
+++ b/code/modules/unit_tests/mecha_damage.dm
@@ -0,0 +1,86 @@
+/**
+ * Unit test to ensure that mechs take the correct amount of damage
+ * based on armor, and that their equipment is properly damaged as well.
+ */
+/datum/unit_test/mecha_damage
+
+/datum/unit_test/mecha_damage/Run()
+ // "Loaded Mauler" was chosen deliberately here.
+ // We need a mech that starts with arm equipment and has fair enough armor.
+ var/obj/vehicle/sealed/mecha/demo_mech = allocate(/obj/vehicle/sealed/mecha/combat/marauder/mauler/loaded)
+ // We need to face our guy explicitly, because mechs have directional armor
+ demo_mech.setDir(EAST)
+
+ var/expected_melee_armor = demo_mech.armor.getRating(MELEE)
+ var/expected_laser_armor = demo_mech.armor.getRating(LASER)
+ var/expected_bullet_armor = demo_mech.armor.getRating(BULLET)
+
+ var/mob/living/carbon/human/dummy = allocate(/mob/living/carbon/human)
+ dummy.forceMove(locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
+ // The dummy needs to be targeting an arm. Left is chosen here arbitrarily.
+ dummy.zone_selected = BODY_ZONE_L_ARM
+ // Not strictly necessary, but you never know
+ dummy.face_atom(demo_mech)
+
+ // Get a sample "melee" weapon.
+ // The energy axe is chosen here due to having a high base force, to make sure we get over the equipment DT.
+ var/obj/item/dummy_melee = allocate(/obj/item/melee/energy/axe)
+ var/expected_melee_damage = round(dummy_melee.force * (1 - expected_melee_armor / 100), DAMAGE_PRECISION)
+
+ // Get a sample laser weapon.
+ // The captain's laser gun here is chosen primarily because it deals more damage than normal lasers.
+ var/obj/item/gun/energy/laser/dummy_laser = allocate(/obj/item/gun/energy/laser/captain)
+ var/obj/item/ammo_casing/laser_ammo = dummy_laser.ammo_type[1]
+ var/obj/projectile/beam/laser_fired = initial(laser_ammo.projectile_type)
+ var/expected_laser_damage = round(dummy_laser.projectile_damage_multiplier * initial(laser_fired.damage) * (1 - expected_laser_armor / 100), DAMAGE_PRECISION)
+
+ // Get a sample ballistic weapon.
+ // The syndicate .357 here is chosen because it does a lot of damage.
+ var/obj/item/gun/ballistic/dummy_gun = allocate(/obj/item/gun/ballistic/revolver)
+ var/obj/item/ammo_casing/ballistic_ammo = dummy_gun.magazine.ammo_type
+ var/obj/projectile/bullet_fired = initial(ballistic_ammo.projectile_type)
+ var/expected_bullet_damage = round(dummy_gun.projectile_damage_multiplier * initial(bullet_fired.damage) * (1 - expected_bullet_armor / 100), DAMAGE_PRECISION)
+
+ var/obj/item/mecha_parts/mecha_equipment/left_arm_equipment = demo_mech.equip_by_category[MECHA_L_ARM]
+ TEST_ASSERT_NOTNULL(left_arm_equipment, "[demo_mech] spawned without any equipment in their left arm slot.")
+
+ // Now it's time to actually beat the heck out of the mech to see if it takes damage correctly.
+ TEST_ASSERT_EQUAL(demo_mech.get_integrity(), demo_mech.max_integrity, "[demo_mech] was spawned at not its maximum integrity.")
+ TEST_ASSERT_EQUAL(left_arm_equipment.get_integrity(), left_arm_equipment.max_integrity, "[left_arm_equipment] ([demo_mech]'s left arm) spawned at not its maximum integrity.")
+
+ // SMACK IT
+ var/pre_melee_integrity = demo_mech.get_integrity()
+ var/pre_melee_arm_integrity = left_arm_equipment.get_integrity()
+ demo_mech.attacked_by(dummy_melee, dummy)
+
+ check_integrity(demo_mech, pre_melee_integrity, expected_melee_damage, "hit with a melee item")
+ check_integrity(left_arm_equipment, pre_melee_arm_integrity, expected_melee_damage, "hit with a melee item")
+
+ // BLAST IT
+ var/pre_laser_integrity = demo_mech.get_integrity()
+ var/pre_laser_arm_integrity = left_arm_equipment.get_integrity()
+ dummy_laser.fire_gun(demo_mech, dummy, FALSE)
+
+ check_integrity(demo_mech, pre_laser_integrity, expected_laser_damage, "shot with a laser")
+ check_integrity(left_arm_equipment, pre_laser_arm_integrity, expected_laser_damage, "shot with a laser")
+
+ // SHOOT IT
+ var/pre_bullet_integrity = demo_mech.get_integrity()
+ var/pre_bullet_arm_integrity = left_arm_equipment.get_integrity()
+ dummy_gun.fire_gun(demo_mech, dummy, FALSE)
+
+ check_integrity(demo_mech, pre_bullet_integrity, expected_bullet_damage, "shot with a bullet")
+ check_integrity(left_arm_equipment, pre_bullet_arm_integrity, expected_bullet_damage, "shot with a bullet")
+
+ // Additional check: The right arm of the mech should have taken no damage by this point.
+ var/obj/item/mecha_parts/mecha_equipment/right_arm_equipment = demo_mech.equip_by_category[MECHA_R_ARM]
+ TEST_ASSERT_NOTNULL(right_arm_equipment, "[demo_mech] spawned without any equipment in their right arm slot.")
+ TEST_ASSERT_EQUAL(right_arm_equipment.get_integrity(), right_arm_equipment.max_integrity, "[demo_mech] somehow took damage to its right arm, despite not being targeted.")
+
+/// Simple helper to check if the integrity of an atom involved has taken damage, and if they took the amount of damage it should have.
+/datum/unit_test/mecha_damage/proc/check_integrity(atom/checking, pre_integrity, expected_damage, hit_by_phrase)
+ var/post_hit_health = checking.get_integrity()
+ TEST_ASSERT(post_hit_health < pre_integrity, "[checking] was [hit_by_phrase], but didn't take any damage.")
+
+ var/damage_taken = round(pre_integrity - post_hit_health, DAMAGE_PRECISION)
+ TEST_ASSERT_EQUAL(damage_taken, expected_damage, "[checking] didn't take the expected amount of damage when [hit_by_phrase]. (Expected damage: [expected_damage], recieved damage: [damage_taken])")
diff --git a/code/modules/unit_tests/mindbound_actions.dm b/code/modules/unit_tests/mindbound_actions.dm
new file mode 100644
index 0000000000000..b404124144091
--- /dev/null
+++ b/code/modules/unit_tests/mindbound_actions.dm
@@ -0,0 +1,30 @@
+/**
+ * Tests that actions assigned to a mob's mind
+ * are successfuly transferred when their mind is transferred to a new mob.
+ */
+/datum/unit_test/actions_moved_on_mind_transfer
+
+/datum/unit_test/actions_moved_on_mind_transfer/Run()
+
+ var/mob/living/carbon/human/wizard = allocate(/mob/living/carbon/human)
+ var/mob/living/simple_animal/pet/dog/corgi/wizard_dog = allocate(/mob/living/simple_animal/pet/dog/corgi)
+ wizard.mind_initialize()
+
+ var/datum/action/cooldown/spell/pointed/projectile/fireball/fireball = new(wizard.mind)
+ fireball.Grant(wizard)
+ var/datum/action/cooldown/spell/aoe/magic_missile/missile = new(wizard.mind)
+ missile.Grant(wizard)
+ var/datum/action/cooldown/spell/jaunt/ethereal_jaunt/jaunt = new(wizard.mind)
+ jaunt.Grant(wizard)
+
+ var/datum/mind/wizard_mind = wizard.mind
+ wizard_mind.transfer_to(wizard_dog)
+
+ TEST_ASSERT_EQUAL(wizard_dog.mind, wizard_mind, "Mind transfer failed to occur, which invalidates the test.")
+
+ for(var/datum/action/cooldown/spell/remaining_spell in wizard.actions)
+ Fail("Spell: [remaining_spell] failed to transfer minds when a mind transfer occured.")
+
+ qdel(fireball)
+ qdel(missile)
+ qdel(jaunt)
diff --git a/code/modules/unit_tests/mob_faction.dm b/code/modules/unit_tests/mob_faction.dm
new file mode 100644
index 0000000000000..359ec40f66ffe
--- /dev/null
+++ b/code/modules/unit_tests/mob_faction.dm
@@ -0,0 +1,19 @@
+/// Checks if any mob's faction var initial value is not a list, which is not supported by the current code
+/datum/unit_test/mob_faction
+
+/datum/unit_test/mob_faction/Run()
+ /// Right now taken from create_and_destroy
+ var/list/ignored = list(
+ /mob/living/carbon,
+ /mob/dview,
+ /mob/oranges_ear
+ )
+ ignored += typesof(/mob/camera/imaginary_friend)
+ ignored += typesof(/mob/living/simple_animal/pet/gondola/gondolapod)
+ ignored += typesof(/mob/living/silicon/robot/model)
+ ignored += typesof(/mob/camera/ai_eye/remote/base_construction)
+ ignored += typesof(/mob/camera/ai_eye/remote/shuttle_docker)
+ for (var/mob_type in typesof(/mob) - ignored)
+ var/mob/mob_instance = allocate(mob_type)
+ if(!islist(mob_instance.faction))
+ TEST_FAIL("[mob_type] faction variable is not a list")
diff --git a/code/modules/unit_tests/novaflower_burn.dm b/code/modules/unit_tests/novaflower_burn.dm
new file mode 100644
index 0000000000000..c54cb9d6d1529
--- /dev/null
+++ b/code/modules/unit_tests/novaflower_burn.dm
@@ -0,0 +1,37 @@
+/// Unit tests that the novaflower's unique genes function.
+/datum/unit_test/novaflower_burn
+
+/datum/unit_test/novaflower_burn/Run()
+ var/mob/living/carbon/human/botanist = allocate(/mob/living/carbon/human)
+ var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)
+ var/obj/item/grown/novaflower/weapon = allocate(/obj/item/grown/novaflower)
+
+ TEST_ASSERT(weapon.force > 0, "[weapon] spawned with zero force.")
+
+ // Keep this around for comparison later.
+ var/initial_force = weapon.force
+ // Start by having the novaflower equipped to an attacker's hands
+ // They are not wearing botany gloves (have plant protection), so they should take damage = the flower's force.
+ weapon.attack_hand(botanist)
+ TEST_ASSERT_EQUAL(botanist.get_active_held_item(), weapon, "The botanist failed to pick up [weapon].")
+ TEST_ASSERT_EQUAL(botanist.getFireLoss(), weapon.force, "The botanist picked up [weapon] with their bare hands, and took an incorrect amount of fire damage.")
+
+ // Heal our attacker for easy comparison later
+ botanist.adjustFireLoss(-100)
+ // And give them the plant safe trait so we don't have to worry about attacks being cancelled
+ ADD_TRAIT(botanist, TRAIT_PLANT_SAFE, "unit_test")
+
+ // Now, let's get a smack with the novaflower and see what happens.
+ weapon.melee_attack_chain(botanist, victim)
+
+ TEST_ASSERT(botanist.getFireLoss() <= 0, "The botanist took fire damage from [weapon], even though they were plant safe.")
+ TEST_ASSERT_EQUAL(victim.getFireLoss(), initial_force, "The target took an incorrect amount of fire damage after being hit with [weapon].")
+ TEST_ASSERT(weapon.force < initial_force, "[weapon] didn't lose any force after an attack.")
+ TEST_ASSERT(victim.fire_stacks > 0, "[weapon] didn't apply any firestacks to the target after an attack.")
+ TEST_ASSERT(victim.on_fire, "[weapon] didn't set the target on fire after an attack.")
+
+ // Lastly we should check that degredation to zero works.
+ weapon.force = 0
+ weapon.melee_attack_chain(botanist, victim)
+
+ TEST_ASSERT(QDELETED(weapon), "[weapon] wasn't deleted after hitting someone with zero force.")
diff --git a/code/modules/unit_tests/outfit_sanity.dm b/code/modules/unit_tests/outfit_sanity.dm
index 6f2a78600baed..a105bb423aea1 100644
--- a/code/modules/unit_tests/outfit_sanity.dm
+++ b/code/modules/unit_tests/outfit_sanity.dm
@@ -20,6 +20,8 @@
r_hand = /obj/item/stack/spacecash/c1000
/datum/unit_test/outfit_sanity/Run()
+ var/datum/outfit/prototype_outfit = /datum/outfit
+ var/prototype_name = initial(prototype_outfit.name)
var/mob/living/carbon/human/H = allocate(/mob/living/carbon/human)
for (var/outfit_type in subtypesof(/datum/outfit))
@@ -28,6 +30,9 @@
qdel(I)
var/datum/outfit/outfit = new outfit_type
+
+ if(outfit.name == prototype_name)
+ TEST_FAIL("[outfit.type]'s name is invalid! Uses default outfit name!")
outfit.pre_equip(H, TRUE)
CHECK_OUTFIT_SLOT(uniform, ITEM_SLOT_ICLOTHING)
diff --git a/code/modules/unit_tests/screenshot_antag_icons.dm b/code/modules/unit_tests/screenshot_antag_icons.dm
new file mode 100644
index 0000000000000..0a210ae31eda1
--- /dev/null
+++ b/code/modules/unit_tests/screenshot_antag_icons.dm
@@ -0,0 +1,12 @@
+/// A screenshot test to make sure every antag icon in the preferences menu is consistent
+/datum/unit_test/screenshot_antag_icons
+
+/datum/unit_test/screenshot_antag_icons/Run()
+ var/datum/asset/spritesheet/antagonists/antagonists = get_asset_datum(/datum/asset/spritesheet/antagonists)
+
+ for (var/antag_icon_key in antagonists.antag_icons)
+ var/icon/reference_icon = antagonists.antag_icons[antag_icon_key]
+
+ var/icon/icon = new()
+ icon.Insert(reference_icon, null, SOUTH, 1)
+ test_screenshot(antag_icon_key, icon)
diff --git a/code/modules/unit_tests/screenshot_basic.dm b/code/modules/unit_tests/screenshot_basic.dm
new file mode 100644
index 0000000000000..350514f007fb3
--- /dev/null
+++ b/code/modules/unit_tests/screenshot_basic.dm
@@ -0,0 +1,8 @@
+/// This is an example for screenshot tests, and a meta-test to make sure they work in the success case.
+/// It creates a picture that is red on the left side, green on the other.
+/datum/unit_test/screenshot_basic
+
+/datum/unit_test/screenshot_basic/Run()
+ var/icon/red = icon('icons/blanks/32x32.dmi', "nothing")
+ red.Blend(COLOR_RED, ICON_OVERLAY)
+ test_screenshot("red", red)
diff --git a/code/modules/unit_tests/screenshot_humanoids.dm b/code/modules/unit_tests/screenshot_humanoids.dm
new file mode 100644
index 0000000000000..196d946cb1200
--- /dev/null
+++ b/code/modules/unit_tests/screenshot_humanoids.dm
@@ -0,0 +1,44 @@
+/// A screenshot test for every humanoid species with a handful of jobs.
+/datum/unit_test/screenshot_humanoids
+
+/datum/unit_test/screenshot_humanoids/Run()
+ // Test lizards as their own thing so we can get more coverage on their features
+ var/mob/living/carbon/human/lizard = allocate(/mob/living/carbon/human/dummy/consistent)
+ lizard.dna.features["mcolor"] = "#099"
+ lizard.dna.features["tail_lizard"] = "Light Tiger"
+ lizard.dna.features["snout"] = "Sharp + Light"
+ lizard.dna.features["horns"] = "Simple"
+ lizard.dna.features["frills"] = "Aquatic"
+ lizard.dna.features["legs"] = "Normal Legs"
+ lizard.set_species(/datum/species/lizard)
+ lizard.equipOutfit(/datum/outfit/job/engineer)
+ test_screenshot("[/datum/species/lizard]", get_flat_icon_for_all_directions(lizard))
+
+ // let me have this
+ var/mob/living/carbon/human/moth = allocate(/mob/living/carbon/human/dummy/consistent)
+ moth.dna.features["moth_antennae"] = "Firewatch"
+ moth.dna.features["moth_markings"] = "None"
+ moth.dna.features["moth_wings"] = "Firewatch"
+ moth.set_species(/datum/species/moth)
+ moth.equipOutfit(/datum/outfit/job/cmo, visualsOnly = TRUE)
+ test_screenshot("[/datum/species/moth]", get_flat_icon_for_all_directions(moth))
+
+ // The rest of the species
+ for (var/datum/species/species_type as anything in subtypesof(/datum/species) - /datum/species/moth - /datum/species/lizard)
+ test_screenshot("[species_type]", get_flat_icon_for_all_directions(make_dummy(species_type, /datum/outfit/job/assistant/consistent)))
+
+/datum/unit_test/screenshot_humanoids/proc/get_flat_icon_for_all_directions(atom/thing)
+ var/icon/output = icon('icons/effects/effects.dmi', "nothing")
+ COMPILE_OVERLAYS(thing)
+
+ for (var/direction in GLOB.cardinals)
+ var/icon/partial = getFlatIcon(thing, defdir = direction, no_anim = TRUE)
+ output.Insert(partial, dir = direction)
+
+ return output
+
+/datum/unit_test/screenshot_humanoids/proc/make_dummy(species, job_outfit)
+ var/mob/living/carbon/human/dummy/consistent/dummy = allocate(/mob/living/carbon/human/dummy/consistent)
+ dummy.set_species(species)
+ dummy.equipOutfit(job_outfit, visualsOnly = TRUE)
+ return dummy
diff --git a/code/modules/unit_tests/screenshots/README.md b/code/modules/unit_tests/screenshots/README.md
new file mode 100644
index 0000000000000..1d50a4e561208
--- /dev/null
+++ b/code/modules/unit_tests/screenshots/README.md
@@ -0,0 +1,18 @@
+This folder contains the results for screenshot tests. Screenshot tests make sure an icon looks the same as it did before a change to prevent regressions.
+
+You can create one by simply using the `test_screenshot` proc.
+
+This example test screenshots a red image and keeps it.
+
+```dm
+/// This is an example for screenshot tests, and a meta-test to make sure they work in the success case.
+/// It creates a picture that is red on the left side, green on the other.
+/datum/unit_test/screenshot_basic
+
+/datum/unit_test/screenshot_basic/Run()
+ var/icon/red = icon('icons/blanks/32x32.dmi', "nothing")
+ red.Blend(COLOR_RED, ICON_OVERLAY)
+ test_screenshot("red", red)
+```
+
+Unfortunately, screenshot tests are sanest to test through a pull request directly, due to limitations with both DM and GitHub.
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_abductor.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_abductor.png
new file mode 100644
index 0000000000000..c0503326ddb19
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_abductor.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_blob.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_blob.png
new file mode 100644
index 0000000000000..f21c6979c1a11
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_blob.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_blobinfection.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_blobinfection.png
new file mode 100644
index 0000000000000..e3d7acbf5d891
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_blobinfection.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodbrother.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodbrother.png
new file mode 100644
index 0000000000000..6e604eecbc6c4
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_bloodbrother.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_changeling.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_changeling.png
new file mode 100644
index 0000000000000..de9743cf1ac7a
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_changeling.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_clownoperative.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_clownoperative.png
new file mode 100644
index 0000000000000..d61c10e74871c
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_clownoperative.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_cultist.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_cultist.png
new file mode 100644
index 0000000000000..be8440660c95d
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_cultist.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_familyheadaspirant.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_familyheadaspirant.png
new file mode 100644
index 0000000000000..5c3169087d0c6
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_familyheadaspirant.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_fugitive.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_fugitive.png
new file mode 100644
index 0000000000000..d1187a14d8491
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_fugitive.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_gangster.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_gangster.png
new file mode 100644
index 0000000000000..5c3169087d0c6
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_gangster.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_headrevolutionary.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_headrevolutionary.png
new file mode 100644
index 0000000000000..7a1b6f6913d1d
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_headrevolutionary.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_heretic.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_heretic.png
new file mode 100644
index 0000000000000..73bc5e02dd39c
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_heretic.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_hereticsmuggler.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_hereticsmuggler.png
new file mode 100644
index 0000000000000..73bc5e02dd39c
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_hereticsmuggler.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_loneoperative.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_loneoperative.png
new file mode 100644
index 0000000000000..d4be21a24a7f5
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_loneoperative.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_malfai.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_malfai.png
new file mode 100644
index 0000000000000..993fbc0b308a1
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_malfai.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_malfaimidround.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_malfaimidround.png
new file mode 100644
index 0000000000000..993fbc0b308a1
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_malfaimidround.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_nightmare.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_nightmare.png
new file mode 100644
index 0000000000000..3b723129ac84d
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_nightmare.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_obsessed.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_obsessed.png
new file mode 100644
index 0000000000000..1d5f88bd8407f
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_obsessed.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_operative.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_operative.png
new file mode 100644
index 0000000000000..450f465ee8060
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_operative.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_operativemidround.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_operativemidround.png
new file mode 100644
index 0000000000000..450f465ee8060
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_operativemidround.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_opportunist.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_opportunist.png
new file mode 100644
index 0000000000000..2ebcdc0b5c8c7
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_opportunist.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_provocateur.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_provocateur.png
new file mode 100644
index 0000000000000..7a1b6f6913d1d
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_provocateur.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_revenant.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_revenant.png
new file mode 100644
index 0000000000000..eccedaabc01a3
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_revenant.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_sentientdisease.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_sentientdisease.png
new file mode 100644
index 0000000000000..e7e1cbd661fcb
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_sentientdisease.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_spacedragon.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_spacedragon.png
new file mode 100644
index 0000000000000..f81dadfcf56f0
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_spacedragon.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_spaceninja.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_spaceninja.png
new file mode 100644
index 0000000000000..a54028e1be992
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_spaceninja.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_syndicateinfiltrator.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_syndicateinfiltrator.png
new file mode 100644
index 0000000000000..62e23dad5c7f0
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_syndicateinfiltrator.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_syndicatesleeperagent.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_syndicatesleeperagent.png
new file mode 100644
index 0000000000000..62e23dad5c7f0
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_syndicatesleeperagent.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_thief.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_thief.png
new file mode 100644
index 0000000000000..2ebcdc0b5c8c7
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_thief.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_traitor.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_traitor.png
new file mode 100644
index 0000000000000..62e23dad5c7f0
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_traitor.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_wizard.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_wizard.png
new file mode 100644
index 0000000000000..350266c89793e
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_wizard.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_wizardmidround.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_wizardmidround.png
new file mode 100644
index 0000000000000..350266c89793e
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_wizardmidround.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_antag_icons_xenomorph.png b/code/modules/unit_tests/screenshots/screenshot_antag_icons_xenomorph.png
new file mode 100644
index 0000000000000..fe089cfc4ec06
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_antag_icons_xenomorph.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_basic_red.png b/code/modules/unit_tests/screenshots/screenshot_basic_red.png
new file mode 100644
index 0000000000000..280ec2883f839
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_basic_red.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_abductor.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_abductor.png
new file mode 100644
index 0000000000000..d4742750320cd
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_abductor.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_android.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_android.png
new file mode 100644
index 0000000000000..413fe56c47fae
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_android.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_dullahan.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_dullahan.png
new file mode 100644
index 0000000000000..12eec6affc3f9
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_dullahan.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_ethereal.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_ethereal.png
new file mode 100644
index 0000000000000..b38c363378c36
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_ethereal.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_fly.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_fly.png
new file mode 100644
index 0000000000000..205d91bcef3aa
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_fly.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem.png
new file mode 100644
index 0000000000000..240e8270f34cc
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_adamantine.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_adamantine.png
new file mode 100644
index 0000000000000..78d706f1326a6
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_adamantine.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_alloy.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_alloy.png
new file mode 100644
index 0000000000000..e9199bfc305fe
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_alloy.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bananium.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bananium.png
new file mode 100644
index 0000000000000..28734ed1c17f6
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bananium.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bluespace.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bluespace.png
new file mode 100644
index 0000000000000..7a2b4b50700cc
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bluespace.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bone.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bone.png
new file mode 100644
index 0000000000000..11ffef30da3aa
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bone.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bronze.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bronze.png
new file mode 100644
index 0000000000000..0fb6bfa0597a4
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_bronze.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_cardboard.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_cardboard.png
new file mode 100644
index 0000000000000..cfc30dee8bdd3
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_cardboard.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_cloth.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_cloth.png
new file mode 100644
index 0000000000000..86eb0a04904f0
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_cloth.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_diamond.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_diamond.png
new file mode 100644
index 0000000000000..8701d58ca5eb1
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_diamond.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_durathread.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_durathread.png
new file mode 100644
index 0000000000000..c35eb550b5fba
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_durathread.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_glass.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_glass.png
new file mode 100644
index 0000000000000..b49db9e679d97
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_glass.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_gold.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_gold.png
new file mode 100644
index 0000000000000..e1353ea196848
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_gold.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_leather.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_leather.png
new file mode 100644
index 0000000000000..547484abd056e
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_leather.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_mhydrogen.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_mhydrogen.png
new file mode 100644
index 0000000000000..b7ba888b71a1e
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_mhydrogen.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plasma.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plasma.png
new file mode 100644
index 0000000000000..8265865d45803
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plasma.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plasteel.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plasteel.png
new file mode 100644
index 0000000000000..da26728fe39fd
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plasteel.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plastic.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plastic.png
new file mode 100644
index 0000000000000..7e4c781f71226
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plastic.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plastitanium.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plastitanium.png
new file mode 100644
index 0000000000000..32e9549dc5ba6
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_plastitanium.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_runic.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_runic.png
new file mode 100644
index 0000000000000..e3a43f47b16e7
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_runic.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_sand.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_sand.png
new file mode 100644
index 0000000000000..d983ed55a0fb2
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_sand.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_silver.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_silver.png
new file mode 100644
index 0000000000000..b7ba888b71a1e
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_silver.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_snow.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_snow.png
new file mode 100644
index 0000000000000..03e8f550f7ff7
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_snow.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_titanium.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_titanium.png
new file mode 100644
index 0000000000000..7e4c781f71226
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_titanium.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_uranium.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_uranium.png
new file mode 100644
index 0000000000000..7db2eb454fe50
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_uranium.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_wood.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_wood.png
new file mode 100644
index 0000000000000..868ad85331cdc
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_golem_wood.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_human.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_human.png
new file mode 100644
index 0000000000000..767a2ec704d15
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_human.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_human_felinid.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_human_felinid.png
new file mode 100644
index 0000000000000..27e6f575d7aca
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_human_felinid.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_human_krokodil_addict.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_human_krokodil_addict.png
new file mode 100644
index 0000000000000..7f76aaf6d9de2
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_human_krokodil_addict.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly.png
new file mode 100644
index 0000000000000..8951c16232efb
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly_luminescent.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly_luminescent.png
new file mode 100644
index 0000000000000..690344a8ab5a1
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly_luminescent.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly_slime.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly_slime.png
new file mode 100644
index 0000000000000..709d6d455fe27
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly_slime.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly_stargazer.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly_stargazer.png
new file mode 100644
index 0000000000000..8951c16232efb
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_jelly_stargazer.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard.png
new file mode 100644
index 0000000000000..955d413d50e2c
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard_ashwalker.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard_ashwalker.png
new file mode 100644
index 0000000000000..24b9dc784bbbc
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard_ashwalker.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard_silverscale.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard_silverscale.png
new file mode 100644
index 0000000000000..12ee5dc1c7ef1
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_lizard_silverscale.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_monkey.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_monkey.png
new file mode 100644
index 0000000000000..6e2d1a6114cc0
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_monkey.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_moth.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_moth.png
new file mode 100644
index 0000000000000..87a567e7dc219
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_moth.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_mush.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_mush.png
new file mode 100644
index 0000000000000..c8527bfe56e9d
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_mush.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_plasmaman.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_plasmaman.png
new file mode 100644
index 0000000000000..94b05c68a8f78
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_plasmaman.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_pod.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_pod.png
new file mode 100644
index 0000000000000..f77e21d1f523c
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_pod.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_shadow.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_shadow.png
new file mode 100644
index 0000000000000..0d232158466ff
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_shadow.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_shadow_nightmare.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_shadow_nightmare.png
new file mode 100644
index 0000000000000..e6b8dc6ef7559
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_shadow_nightmare.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_skeleton.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_skeleton.png
new file mode 100644
index 0000000000000..bb5be6b7d5ab6
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_skeleton.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_snail.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_snail.png
new file mode 100644
index 0000000000000..96d146df1a835
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_snail.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_vampire.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_vampire.png
new file mode 100644
index 0000000000000..a29c38b292c85
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_vampire.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_zombie.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_zombie.png
new file mode 100644
index 0000000000000..7b0d5c736d523
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_zombie.png differ
diff --git a/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_zombie_infectious.png b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_zombie_infectious.png
new file mode 100644
index 0000000000000..7b0d5c736d523
Binary files /dev/null and b/code/modules/unit_tests/screenshots/screenshot_humanoids__datum_species_zombie_infectious.png differ
diff --git a/code/modules/unit_tests/security_levels.dm b/code/modules/unit_tests/security_levels.dm
new file mode 100644
index 0000000000000..eecef51ff99c9
--- /dev/null
+++ b/code/modules/unit_tests/security_levels.dm
@@ -0,0 +1,16 @@
+/**
+ * Security Level Unit Test
+ *
+ * This test is here to ensure there are no security levels with the same name or number level. Having the same name or number level will cause problems.
+ */
+/datum/unit_test/security_levels
+
+/datum/unit_test/security_levels/Run()
+ var/list/comparison = subtypesof(/datum/security_level)
+
+ for(var/datum/security_level/iterating_level in comparison)
+ for(var/datum/security_level/iterating_level_check in comparison)
+ if(iterating_level == iterating_level_check) // If they are the same type, don't check
+ continue
+ TEST_ASSERT_NOTEQUAL(iterating_level.name, iterating_level_check.name, "Security level [iterating_level] has the same name as [iterating_level_check]!")
+ TEST_ASSERT_NOTEQUAL(iterating_level.number_level, iterating_level_check.number_level, "Security level [iterating_level] has the same level number as [iterating_level_check]!")
diff --git a/code/modules/unit_tests/slips.dm b/code/modules/unit_tests/slips.dm
new file mode 100644
index 0000000000000..3728a8341b9af
--- /dev/null
+++ b/code/modules/unit_tests/slips.dm
@@ -0,0 +1,16 @@
+/// Unit test that forces various slips on a mob and checks return values and mob state to see if the slip has likely been successful.
+/datum/unit_test/slips
+
+/datum/unit_test/slips/Run()
+ // Test just forced slipping, which calls turf slip code as well.
+ var/mob/living/carbon/human/mso = allocate(/mob/living/carbon/human)
+
+ TEST_ASSERT(mso.slip(100) == TRUE, "/mob/living/carbon/human/slip() returned FALSE when TRUE was expected")
+ TEST_ASSERT(!!(mso.IsKnockdown()), "/mob/living/carbon/human/slip() failed to knockdown target when knockdown was expected")
+
+ // Test the slipping component, which calls mob slip code. Just for good measure.
+ var/mob/living/carbon/human/msos_friend_mso = allocate(/mob/living/carbon/human, run_loc_floor_bottom_left)
+ var/obj/item/grown/bananapeel/specialpeel/mso_bane = allocate(/obj/item/grown/bananapeel/specialpeel, get_step(run_loc_floor_bottom_left, EAST))
+
+ msos_friend_mso.Move(get_turf(mso_bane), EAST)
+ TEST_ASSERT(!!(msos_friend_mso.IsKnockdown()), "Banana peel which should have slipping component failed to knockdown target when knockdown was expected")
diff --git a/code/modules/unit_tests/spell_invocations.dm b/code/modules/unit_tests/spell_invocations.dm
new file mode 100644
index 0000000000000..f72e5277eacfd
--- /dev/null
+++ b/code/modules/unit_tests/spell_invocations.dm
@@ -0,0 +1,26 @@
+/**
+ * Validates that all spells have a correct
+ * invocation type and invocation setup.
+ */
+/datum/unit_test/spell_invocations
+
+/datum/unit_test/spell_invocations/Run()
+
+ var/list/types_to_test = subtypesof(/datum/action/cooldown/spell)
+
+ for(var/datum/action/cooldown/spell/spell_type as anything in types_to_test)
+ var/spell_name = initial(spell_type.name)
+ var/invoke_type = initial(spell_type.invocation_type)
+ switch(invoke_type)
+ if(INVOCATION_EMOTE)
+ if(isnull(initial(spell_type.invocation_self_message)))
+ Fail("Spell: [spell_name] ([spell_type]) set emote invocation type but did not set a self message.")
+ if(isnull(initial(spell_type.invocation)))
+ Fail("Spell: [spell_name] ([spell_type]) set emote invocation type but did not set an invocation message.")
+
+ if(INVOCATION_SHOUT, INVOCATION_WHISPER)
+ if(isnull(initial(spell_type.invocation)))
+ Fail("Spell: [spell_name] ([spell_type]) set a speaking invocation type but did not set an invocation message.")
+
+ // INVOCATION_NONE:
+ // It doesn't matter what they have set for invocation text. So not it's skipped.
diff --git a/code/modules/unit_tests/spell_mindswap.dm b/code/modules/unit_tests/spell_mindswap.dm
new file mode 100644
index 0000000000000..0f7d63440cf06
--- /dev/null
+++ b/code/modules/unit_tests/spell_mindswap.dm
@@ -0,0 +1,41 @@
+/**
+ * Validates that the mind swap spell
+ * properly transfers minds between a caster and a target.
+ *
+ * Also checks that the mindswap spell itself was transferred over
+ * to the new body on cast.
+ */
+/datum/unit_test/mind_swap_spell
+
+/datum/unit_test/mind_swap_spell/Run()
+
+ var/mob/living/carbon/human/swapper = allocate(/mob/living/carbon/human)
+ var/mob/living/carbon/human/to_swap = allocate(/mob/living/carbon/human)
+
+ swapper.forceMove(run_loc_floor_bottom_left)
+ to_swap.forceMove(locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z))
+
+ swapper.mind_initialize()
+ to_swap.mind_initialize()
+
+ var/datum/mind/swapper_mind = swapper.mind
+ var/datum/mind/to_swap_mind = to_swap.mind
+
+ var/datum/action/cooldown/spell/pointed/mind_transfer/mind_swap = new(swapper.mind)
+ mind_swap.target_requires_key = FALSE
+ mind_swap.Grant(swapper)
+
+ // Perform a cast from the very base - mimics a click
+ var/result = mind_swap.InterceptClickOn(swapper, null, to_swap)
+ TEST_ASSERT(result, "[mind_swap] spell: Mind swap returned \"false\" from InterceptClickOn / cast, despite having valid conditions.")
+
+ TEST_ASSERT_EQUAL(swapper.mind, to_swap_mind, "[mind_swap] spell: Despite returning \"true\" on cast, swap failed to relocate the minds of the caster and the target.")
+ TEST_ASSERT_EQUAL(to_swap.mind, swapper_mind, "[mind_swap] spell: Despite returning \"true\" on cast, swap failed to relocate the minds of the target and the caster.")
+
+ var/datum/action/cooldown/spell/pointed/mind_transfer/should_be_null = locate() in swapper.actions
+ var/datum/action/cooldown/spell/pointed/mind_transfer/should_not_be_null = locate() in to_swap.actions
+
+ TEST_ASSERT(!isnull(should_not_be_null), "[mind_swap] spell: The spell was not transferred to the caster's new body, despite successful mind reolcation.")
+ TEST_ASSERT(isnull(should_be_null), "[mind_swap] spell: The spell remained on the caster's original body, despite successful mind relocation.")
+
+ qdel(mind_swap)
diff --git a/code/modules/unit_tests/spell_names.dm b/code/modules/unit_tests/spell_names.dm
new file mode 100644
index 0000000000000..df8a42ae3da44
--- /dev/null
+++ b/code/modules/unit_tests/spell_names.dm
@@ -0,0 +1,32 @@
+/**
+ * Validates that all spells have a different name.
+ *
+ * Spell names are used for debugging in some places
+ * as well as an option for admins giving out spells,
+ * so every spell should have a distinct name.
+ *
+ * If you're making a subtype with only one or two big changes,
+ * consider adding an adjective to the name.
+ *
+ * "Lesser Fireball" for a subtype of Fireball with a shorter cooldown.
+ * "Deadly Magic Missile" for a subtype of Magic Missile that does damage, etc.
+ */
+/datum/unit_test/spell_names
+
+/datum/unit_test/spell_names/Run()
+
+ var/list/types_to_test = typesof(/datum/action/cooldown/spell)
+
+ var/list/existing_names = list()
+ for(var/datum/action/cooldown/spell/spell_type as anything in types_to_test)
+ var/spell_name = initial(spell_type.name)
+ if(spell_name == "Spell")
+ continue
+
+ if(spell_name in existing_names)
+ Fail("Spell: [spell_name] ([spell_type]) had a name identical to another spell. \
+ This can cause confusion for admins giving out spells, and while debugging. \
+ Consider giving the name an adjective if it's a subtype. (\"Greater\", \"Lesser\", \"Deadly\".)")
+ continue
+
+ existing_names += spell_name
diff --git a/code/modules/unit_tests/spell_shapeshift.dm b/code/modules/unit_tests/spell_shapeshift.dm
new file mode 100644
index 0000000000000..0aebbd7e49290
--- /dev/null
+++ b/code/modules/unit_tests/spell_shapeshift.dm
@@ -0,0 +1,20 @@
+/**
+ * Validates that all shapeshift type spells
+ * have a valid possible_shapes setup.
+ */
+/datum/unit_test/shapeshift_spell_validity
+
+/datum/unit_test/shapeshift_spell_validity/Run()
+
+ var/list/types_to_test = subtypesof(/datum/action/cooldown/spell/shapeshift)
+
+ for(var/spell_type in types_to_test)
+ var/datum/action/cooldown/spell/shapeshift/shift = new spell_type()
+ if(!LAZYLEN(shift.possible_shapes))
+ Fail("Shapeshift spell: [shift] ([spell_type]) did not have any possible shapeshift options.")
+
+ for(var/shift_type in shift.possible_shapes)
+ if(!ispath(shift_type, /mob/living))
+ Fail("Shapeshift spell: [shift] had an invalid / non-living shift type ([shift_type]) in their possible shapes list.")
+
+ qdel(shift)
diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm
index 4359d2f1de0e0..ea8e75c9e4a45 100644
--- a/code/modules/unit_tests/unit_test.dm
+++ b/code/modules/unit_tests/unit_test.dm
@@ -84,6 +84,30 @@ GLOBAL_LIST_EMPTY(unit_test_mapping_logs)
allocated += instance
return instance
+/datum/unit_test/proc/test_screenshot(name, icon/icon)
+ if (!istype(icon))
+ TEST_FAIL("[icon] is not an icon.")
+ return
+
+ var/path_prefix = replacetext(replacetext("[type]", "/datum/unit_test/", ""), "/", "_")
+ name = replacetext(name, "/", "_")
+
+ var/filename = "code/modules/unit_tests/screenshots/[path_prefix]_[name].png"
+
+ if (fexists(filename))
+ var/data_filename = "data/screenshots/[path_prefix]_[name].png"
+ fcopy(icon, data_filename)
+ log_test("[path_prefix]_[name] was found, putting in data/screenshots")
+ else if (fexists("code"))
+ // We are probably running in a local build
+ fcopy(icon, filename)
+ TEST_FAIL("Screenshot for [name] did not exist. One has been created.")
+ else
+ // We are probably running in real CI, so just pretend it worked and move on
+ fcopy(icon, "data/screenshots_new/[path_prefix]_[name].png")
+
+ log_test("[path_prefix]_[name] was put in data/screenshots_new")
+
/proc/RunUnitTest(test_path, list/test_results)
var/datum/unit_test/test = new test_path
diff --git a/code/modules/unit_tests/wizard.dm b/code/modules/unit_tests/wizard_loadout.dm
similarity index 95%
rename from code/modules/unit_tests/wizard.dm
rename to code/modules/unit_tests/wizard_loadout.dm
index 22343884eb91d..7c5dd6e68431b 100644
--- a/code/modules/unit_tests/wizard.dm
+++ b/code/modules/unit_tests/wizard_loadout.dm
@@ -3,6 +3,8 @@
// May this never happen again.
/// Test loadouts for crashes, runtimes, stack traces and infinite loops. No ASSERTs necessary.
+/datum/unit_test/wizard_loadout
+
/datum/unit_test/wizard_loadout/Run()
for(var/loadout in ALL_WIZARD_LOADOUTS)
var/obj/item/spellbook/wizard_book = allocate(/obj/item/spellbook)
diff --git a/code/modules/uplink/uplink_items/job.dm b/code/modules/uplink/uplink_items/job.dm
index 11ca1df7596fd..378f52fb897f4 100644
--- a/code/modules/uplink/uplink_items/job.dm
+++ b/code/modules/uplink/uplink_items/job.dm
@@ -254,6 +254,17 @@
cost = 20
restricted_roles = list(JOB_CLOWN)
+/datum/uplink_item/role_restricted/concealed_weapon_bay
+ name = "Concealed Weapon Bay"
+ desc = "A modification for non-combat exosuits that allows them to equip one piece of equipment designed for combat units. \
+ Attach to an exosuit with an existing equipment to disguise the bay as that equipment. The sacrificed equipment will be lost.\
+ Alternatively, you can attach the bay to an empty equipment slot, but the bay will not be concealed. Once the bay is \
+ attached, an exosuit weapon can be fitted inside."
+ progression_minimum = 30 MINUTES
+ item = /obj/item/mecha_parts/mecha_equipment/concealed_weapon_bay
+ cost = 3
+ restricted_roles = list(JOB_ROBOTICIST, JOB_RESEARCH_DIRECTOR)
+
/datum/uplink_item/role_restricted/his_grace
name = "His Grace"
desc = "An incredibly dangerous weapon recovered from a station overcome by the grey tide. Once activated, He will thirst for blood and must be used to kill to sate that thirst. \
diff --git a/code/modules/vehicles/atv.dm b/code/modules/vehicles/atv.dm
index 88d9431e0d139..581bfb94e990e 100644
--- a/code/modules/vehicles/atv.dm
+++ b/code/modules/vehicles/atv.dm
@@ -89,7 +89,7 @@
if(DT_PROB(10, delta_time))
return
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(0, location = src)
+ smoke.set_up(0, holder = src, location = src)
smoke.start()
/obj/vehicle/ridden/atv/bullet_act(obj/projectile/P)
diff --git a/code/modules/vehicles/cars/clowncar.dm b/code/modules/vehicles/cars/clowncar.dm
index 33611a96784ff..45660c439538e 100644
--- a/code/modules/vehicles/cars/clowncar.dm
+++ b/code/modules/vehicles/cars/clowncar.dm
@@ -155,8 +155,8 @@
randomchems.my_atom = src
randomchems.add_reagent(get_random_reagent_id(), 100)
var/datum/effect_system/fluid_spread/foam/foam = new
- foam.set_up(200, loc, randomchems)
- foam.start()
+ foam.set_up(200, holder = src, location = loc, carry = randomchems)
+ foam.start(log = TRUE)
if(3)
visible_message(span_danger("[user] presses one of the colorful buttons on [src], and the clown car turns on its singularity disguise system."))
icon = 'icons/obj/singularity.dmi'
@@ -168,9 +168,9 @@
funnychems.my_atom = src
funnychems.add_reagent(/datum/reagent/consumable/superlaughter, 50)
var/datum/effect_system/fluid_spread/smoke/chem/smoke = new()
- smoke.set_up(4, location = src, carry = funnychems)
+ smoke.set_up(4, holder = src, location = src, carry = funnychems)
smoke.attach(src)
- smoke.start()
+ smoke.start(log = TRUE)
if(5)
visible_message(span_danger("[user] presses one of the colorful buttons on [src], and the clown car starts dropping an oil trail."))
RegisterSignal(src, COMSIG_MOVABLE_MOVED, .proc/cover_in_oil)
diff --git a/code/modules/vehicles/mecha/_mecha.dm b/code/modules/vehicles/mecha/_mecha.dm
index 4e4759b29f5db..a87860ae15fe9 100644
--- a/code/modules/vehicles/mecha/_mecha.dm
+++ b/code/modules/vehicles/mecha/_mecha.dm
@@ -211,7 +211,7 @@
spark_system.set_up(2, 0, src)
spark_system.attach(src)
- smoke_system.set_up(3, location = src)
+ smoke_system.set_up(3, holder = src, location = src)
smoke_system.attach(src)
radio = new(src)
diff --git a/code/modules/vehicles/mecha/combat/durand.dm b/code/modules/vehicles/mecha/combat/durand.dm
index 83eb2616a1768..17b84ad7ed8cd 100644
--- a/code/modules/vehicles/mecha/combat/durand.dm
+++ b/code/modules/vehicles/mecha/combat/durand.dm
@@ -21,7 +21,7 @@
/obj/vehicle/sealed/mecha/combat/durand/Initialize(mapload)
. = ..()
- shield = new /obj/durand_shield(loc, src, layer, dir)
+ shield = new /obj/durand_shield(loc, src, plane, layer, dir)
RegisterSignal(src, COMSIG_MECHA_ACTION_TRIGGER, .proc/relay)
RegisterSignal(src, COMSIG_PROJECTILE_PREHIT, .proc/prehit)
@@ -158,28 +158,34 @@ own integrity back to max. Shield is automatically dropped if we run out of powe
light_power = 5
light_color = LIGHT_COLOR_ELECTRIC_CYAN
light_on = FALSE
+ resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF //The shield should not take damage from fire, lava, or acid; that's the mech's job.
///Our link back to the durand
var/obj/vehicle/sealed/mecha/combat/durand/chassis
///To keep track of things during the animation
var/switching = FALSE
- var/currentuser
- resistance_flags = LAVA_PROOF | FIRE_PROOF | ACID_PROOF //The shield should not take damage from fire, lava, or acid; that's the mech's job.
-
-/obj/durand_shield/Initialize(mapload, _chassis, _layer, _dir)
+/obj/durand_shield/Initialize(mapload, chassis, plane, layer, dir)
. = ..()
- chassis = _chassis
- layer = _layer
- setDir(_dir)
+ src.chassis = chassis
+ src.layer = layer
+ src.plane = plane
+ setDir(dir)
RegisterSignal(src, COMSIG_MECHA_ACTION_TRIGGER, .proc/activate)
+ RegisterSignal(chassis, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE, .proc/shield_glide_size_update)
/obj/durand_shield/Destroy()
+ UnregisterSignal(src, COMSIG_MECHA_ACTION_TRIGGER)
if(chassis)
+ UnregisterSignal(chassis, COMSIG_MOVABLE_UPDATE_GLIDE_SIZE)
chassis.shield = null
chassis = null
return ..()
+/obj/durand_shield/proc/shield_glide_size_update(datum/source, target)
+ SIGNAL_HANDLER
+ glide_size = target
+
/**
* Handles activating and deactivating the shield.
*
@@ -195,7 +201,6 @@ own integrity back to max. Shield is automatically dropped if we run out of powe
*/
/obj/durand_shield/proc/activate(datum/source, mob/owner, list/signal_args)
SIGNAL_HANDLER
- currentuser = owner
if(!LAZYLEN(chassis?.occupants))
return
if(switching && !signal_args[1])
@@ -229,10 +234,21 @@ own integrity back to max. Shield is automatically dropped if we run out of powe
playsound(src, 'sound/mecha/mech_shield_drop.ogg', 50, FALSE)
set_light(0)
icon_state = "shield_null"
- invisibility = INVISIBILITY_MAXIMUM //no showing on right-click
+ addtimer(CALLBACK(src, .proc/make_invisible), 1 SECONDS, TIMER_UNIQUE|TIMER_OVERRIDE)
UnregisterSignal(chassis, COMSIG_ATOM_DIR_CHANGE)
switching = FALSE
+/**
+ * Sets invisibility to INVISIBILITY_MAXIMUM if defense mode is disabled
+ *
+ * We need invisibility set to higher than 25 for the shield to not appear
+ * in the right-click context menu, but if we do it too early, we miss the
+ * deactivate animation. Hense, timer and this proc.
+ */
+/obj/durand_shield/proc/make_invisible()
+ if(!chassis.defense_mode)
+ invisibility = INVISIBILITY_MAXIMUM
+
/obj/durand_shield/proc/resetdir(datum/source, olddir, newdir)
SIGNAL_HANDLER
setDir(newdir)
diff --git a/code/modules/vehicles/mecha/equipment/mecha_equipment.dm b/code/modules/vehicles/mecha/equipment/mecha_equipment.dm
index 38b7f2cd4b759..8d3a52d071e87 100644
--- a/code/modules/vehicles/mecha/equipment/mecha_equipment.dm
+++ b/code/modules/vehicles/mecha/equipment/mecha_equipment.dm
@@ -43,6 +43,8 @@
if(can_attach(M, attach_right))
if(!user.temporarilyRemoveItemFromInventory(src))
return FALSE
+ if(special_attaching_interaction(attach_right, M, user))
+ return TRUE //The rest is handled in the special interactions proc
attach(M, attach_right)
user.visible_message(span_notice("[user] attaches [src] to [M]."), span_notice("You attach [src] to [M]."))
return TRUE
@@ -87,6 +89,9 @@
if(chassis.equipment_disabled)
to_chat(chassis.occupants, span_warning("Error -- Equipment control unit is unresponsive."))
return FALSE
+ if(get_integrity() <= 1)
+ to_chat(chassis.occupants, span_warning("Error -- Equipment critically damaged."))
+ return FALSE
if(TIMER_COOLDOWN_CHECK(chassis, COOLDOWN_MECHA_EQUIPMENT(type)))
return FALSE
return TRUE
@@ -126,14 +131,28 @@
return FALSE
if(equipment_slot == MECHA_WEAPON)
if(attach_right)
- if(mech.equip_by_category[MECHA_R_ARM])
+ if(mech.equip_by_category[MECHA_R_ARM] && (!special_attaching_interaction(attach_right, mech, checkonly = TRUE)))
return FALSE
else
- if(mech.equip_by_category[MECHA_L_ARM])
+ if(mech.equip_by_category[MECHA_L_ARM] && (!special_attaching_interaction(attach_right, mech, checkonly = TRUE)))
return FALSE
return TRUE
return length(mech.equip_by_category[equipment_slot]) < mech.max_equip_by_category[equipment_slot]
+/**
+ * Special Attaching Interaction, used to bypass normal attachment procs.
+ *
+ * If an equipment needs to bypass the regular chain of events, this proc can be used to allow for that. If used, it
+ * must handle actually calling attach(), as well as any feedback to the user.
+ * Args:
+ * * attach_right: True if attaching the the right-hand equipment slot, false otherwise.
+ * * mech: ref to the mecha that we're attaching onto.
+ * * user: ref to the mob doing the attaching
+ * * checkonly: check if we are able to handle the attach procedure ourselves, but don't actually do it yet.
+ */
+/obj/item/mecha_parts/mecha_equipment/proc/special_attaching_interaction(attach_right = FALSE, obj/vehicle/sealed/mecha/mech, mob/user, checkonly = FALSE)
+ return FALSE
+
/obj/item/mecha_parts/mecha_equipment/proc/attach(obj/vehicle/sealed/mecha/M, attach_right = FALSE)
LAZYADD(M.flat_equipment, src)
var/to_equip_slot = equipment_slot
diff --git a/code/modules/vehicles/mecha/equipment/tools/other_tools.dm b/code/modules/vehicles/mecha/equipment/tools/other_tools.dm
index 0bdfc2b72f732..c06ec657a6e59 100644
--- a/code/modules/vehicles/mecha/equipment/tools/other_tools.dm
+++ b/code/modules/vehicles/mecha/equipment/tools/other_tools.dm
@@ -468,3 +468,46 @@
generate_effect(movement_dir)
return TRUE
return FALSE
+
+///////////////////////////////////// CONCEALED WEAPON BAY ////////////////////////////////////////
+
+/obj/item/mecha_parts/mecha_equipment/concealed_weapon_bay
+ name = "concealed weapon bay"
+ desc = "A compartment that allows a non-combat mecha to equip one weapon while hiding the weapon from plain sight."
+ icon_state = "mecha_weapon_bay"
+
+/obj/item/mecha_parts/mecha_equipment/concealed_weapon_bay/try_attach_part(mob/user, obj/vehicle/sealed/mecha/M)
+ if(istype(M, /obj/vehicle/sealed/mecha/combat))
+ to_chat(user, span_warning("[M] does not have the correct bolt configuration!"))
+ return
+ return ..()
+
+/obj/item/mecha_parts/mecha_equipment/concealed_weapon_bay/special_attaching_interaction(attach_right = FALSE, obj/vehicle/sealed/mecha/mech, mob/user, checkonly = FALSE)
+ if(checkonly)
+ return TRUE
+ var/obj/item/mecha_parts/mecha_equipment/existing_equip
+ if(attach_right)
+ existing_equip = mech.equip_by_category[MECHA_R_ARM]
+ else
+ existing_equip = mech.equip_by_category[MECHA_L_ARM]
+ if(existing_equip)
+ name = existing_equip.name
+ icon = existing_equip.icon
+ icon_state = existing_equip.icon_state
+ existing_equip.detach()
+ existing_equip.Destroy()
+ user.visible_message(span_notice("[user] hollows out [src] and puts something in."), span_notice("You attach the concealed weapon bay to [mech] within the shell of [src]."))
+ else
+ user.visible_message(span_notice("[user] attaches [src] to [mech]."), span_notice("You attach [src] to [mech]."))
+ attach(mech, attach_right)
+ mech.mech_type |= EXOSUIT_MODULE_CONCEALED_WEP_BAY
+ return TRUE
+
+/obj/item/mecha_parts/mecha_equipment/concealed_weapon_bay/detach(atom/moveto)
+ var/obj/vehicle/sealed/mecha/mech = chassis
+ . = ..()
+ name = initial(name)
+ icon = initial(icon)
+ icon_state = initial(icon_state)
+ if(!locate(/obj/item/mecha_parts/mecha_equipment/concealed_weapon_bay) in mech.contents) //if no others exist
+ mech.mech_type &= ~EXOSUIT_MODULE_CONCEALED_WEP_BAY
diff --git a/code/modules/vehicles/mecha/equipment/weapons/weapons.dm b/code/modules/vehicles/mecha/equipment/weapons/weapons.dm
index a1778c3af2b3c..f431072dc29ef 100644
--- a/code/modules/vehicles/mecha/equipment/weapons/weapons.dm
+++ b/code/modules/vehicles/mecha/equipment/weapons/weapons.dm
@@ -13,13 +13,40 @@
var/firing_effect_type = /obj/effect/temp_visual/dir_setting/firing_effect //the visual effect appearing when the weapon is fired.
var/kickback = TRUE //Will using this weapon in no grav push mecha back.
-/obj/item/mecha_parts/mecha_equipment/weapon/can_attach(obj/vehicle/sealed/mecha/M, attach_right = FALSE)
+/obj/item/mecha_parts/mecha_equipment/weapon/can_attach(obj/vehicle/sealed/mecha/mech, attach_right = FALSE)
if(!..())
return FALSE
- if(istype(M, /obj/vehicle/sealed/mecha/combat))
+ if(mech.mech_type & EXOSUIT_MODULE_COMBAT)
return TRUE
return FALSE
+/obj/item/mecha_parts/mecha_equipment/weapon/special_attaching_interaction(attach_right = FALSE, obj/vehicle/sealed/mecha/mech, mob/user, checkonly = FALSE)
+ var/obj/item/mecha_parts/mecha_equipment/concealed_weapon_bay/bay
+ if(attach_right)
+ bay = mech.equip_by_category[MECHA_R_ARM]
+ else
+ bay = mech.equip_by_category[MECHA_L_ARM]
+ if(!istype(bay))
+ return FALSE //No bay, use normal attach procs
+ if(checkonly)
+ return TRUE
+ name = bay.name
+ icon = bay.icon
+ icon_state = bay.icon_state
+ bay.detach()
+ bay.forceMove(src) //for later detaching
+ attach(mech, attach_right)
+ user.visible_message(span_notice("[user] inserts something into [src]."), span_notice("You attach the [initial(name)] into the concealed weapon bay."))
+ return TRUE
+
+/obj/item/mecha_parts/mecha_equipment/weapon/detach(atom/moveto)
+ for(var/obj/item/mecha_parts/mecha_equipment/concealed_weapon_bay/bay in contents)
+ bay.forceMove(get_turf(chassis))
+ name = initial(name)
+ icon = initial(icon)
+ icon_state = initial(icon_state)
+ return ..()
+
/obj/item/mecha_parts/mecha_equipment/weapon/action(mob/source, atom/target, list/modifiers)
if(!action_checks(target))
return FALSE
diff --git a/code/modules/vehicles/mecha/mecha_defense.dm b/code/modules/vehicles/mecha/mecha_defense.dm
index c8a4b6322afb3..9b25bbb6c7be6 100644
--- a/code/modules/vehicles/mecha/mecha_defense.dm
+++ b/code/modules/vehicles/mecha/mecha_defense.dm
@@ -25,24 +25,31 @@
gear = equip_by_category[MECHA_R_ARM]
if(!gear)
return
- var/brokenstatus = gear.get_integrity()
+ var/component_health = gear.get_integrity()
// always leave at least 1 health
- brokenstatus--
- var/damage_to_deal = min(brokenstatus, damage)
- if(!damage_to_deal)
+ var/damage_to_deal = min(component_health - 1, damage)
+ if(damage_to_deal <= 0)
return
+
gear.take_damage(damage_to_deal)
+ if(gear.get_integrity() <= 1)
+ to_chat(occupants, "[icon2html(src, occupants)][span_danger("[gear] is critically damaged!")]")
+ playsound(src, gear.destroy_sound, 50)
-/obj/vehicle/sealed/mecha/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir)
- . = ..()
- if(. && atom_integrity > 0)
- spark_system.start()
- try_deal_internal_damage(.)
- if(. >= 5 || prob(33))
- to_chat(occupants, "[icon2html(src, occupants)][span_userdanger("Taking damage!")]")
- log_message("Took [.] points of damage. Damage type: [damage_type]", LOG_MECHA)
-
-/obj/vehicle/sealed/mecha/run_atom_armor(damage_amount, damage_type, damage_flag = 0, attack_dir, armour_penentration)
+/obj/vehicle/sealed/mecha/take_damage(damage_amount, damage_type = BRUTE, damage_flag = "", sound_effect = TRUE, attack_dir, armour_penetration = 0)
+ var/damage_taken = ..()
+ if(damage_taken <= 0 || atom_integrity < 0)
+ return damage_taken
+
+ spark_system.start()
+ try_deal_internal_damage(damage_taken)
+ if(damage_taken >= 5 || prob(33))
+ to_chat(occupants, "[icon2html(src, occupants)][span_userdanger("Taking damage!")]")
+ log_message("Took [damage_taken] points of damage. Damage type: [damage_type]", LOG_MECHA)
+
+ return damage_taken
+
+/obj/vehicle/sealed/mecha/run_atom_armor(damage_amount, damage_type, damage_flag = 0, attack_dir, armour_penetration)
. = ..()
if(attack_dir)
var/facing_modifier = get_armour_facing(abs(dir2angle(dir) - dir2angle(attack_dir)))
@@ -113,7 +120,13 @@
return BULLET_ACT_HIT
log_message("Hit by projectile. Type: [hitting_projectile]([hitting_projectile.damage_type]).", LOG_MECHA, color="red")
// yes we *have* to run the armor calc proc here I love tg projectile code too
- try_damage_component(run_atom_armor(hitting_projectile.damage, hitting_projectile.damage_type, hitting_projectile.damage_type, 0, REVERSE_DIR(hitting_projectile.dir), hitting_projectile.armour_penetration), hitting_projectile.def_zone)
+ try_damage_component(run_atom_armor(
+ damage_amount = hitting_projectile.damage,
+ damage_type = hitting_projectile.damage_type,
+ damage_flag = hitting_projectile.armor_flag,
+ attack_dir = REVERSE_DIR(hitting_projectile.dir),
+ armour_penetration = hitting_projectile.armour_penetration,
+ ), hitting_projectile.def_zone)
return ..()
/obj/vehicle/sealed/mecha/ex_act(severity, target)
@@ -258,11 +271,26 @@
var/obj/item/mecha_parts/P = W
P.try_attach_part(user, src, FALSE)
return
- . = ..()
- log_message("Attacked by [W]. Attacker - [user], Damage - [.]", LOG_MECHA)
- if(isliving(user))
- var/mob/living/living_user = user
- try_damage_component(., living_user.zone_selected)
+
+ return ..()
+
+/obj/vehicle/sealed/mecha/attacked_by(obj/item/attacking_item, mob/living/user)
+ if(!attacking_item.force)
+ return
+
+ var/damage_taken = take_damage(attacking_item.force * attacking_item.demolition_mod, attacking_item.damtype, MELEE, 1)
+ try_damage_component(damage_taken, user.zone_selected)
+
+ var/hit_verb = length(attacking_item.attack_verb_simple) ? "[pick(attacking_item.attack_verb_simple)]" : "hit"
+ user.visible_message(
+ span_danger("[user] [hit_verb][plural_s(hit_verb)] [src] with [attacking_item][damage_taken ? "." : ", without leaving a mark!"]"),
+ span_danger("You [hit_verb] [src] with [attacking_item][damage_taken ? "." : ", without leaving a mark!"]"),
+ span_hear("You hear a [hit_verb]."),
+ COMBAT_MESSAGE_RANGE,
+ )
+
+ log_combat(user, src, "attacked", attacking_item)
+ log_message("Attacked by [user]. Item - [attacking_item], Damage - [damage_taken]", LOG_MECHA)
/obj/vehicle/sealed/mecha/wrench_act(mob/living/user, obj/item/I)
..()
diff --git a/code/modules/vehicles/mecha/mecha_mob_interaction.dm b/code/modules/vehicles/mecha/mecha_mob_interaction.dm
index aa3f6f3f56fba..de2e6b68c759d 100644
--- a/code/modules/vehicles/mecha/mecha_mob_interaction.dm
+++ b/code/modules/vehicles/mecha/mecha_mob_interaction.dm
@@ -4,7 +4,7 @@
if(HAS_TRAIT(M, TRAIT_PRIMITIVE)) //no lavalizards either.
to_chat(M, span_warning("The knowledge to use this device eludes you!"))
return
- log_message("[M] tries to move into [src].", LOG_MECHA)
+ log_message("[M] tried to move into [src].", LOG_MECHA)
if(dna_lock && M.has_dna())
var/mob/living/carbon/entering_carbon = M
if(entering_carbon.dna.unique_enzymes != dna_lock)
diff --git a/code/modules/vehicles/secway.dm b/code/modules/vehicles/secway.dm
index 1a2e64724419f..651bf3a89e596 100644
--- a/code/modules/vehicles/secway.dm
+++ b/code/modules/vehicles/secway.dm
@@ -25,7 +25,7 @@
if(DT_PROB(10, delta_time))
return
var/datum/effect_system/fluid_spread/smoke/smoke = new
- smoke.set_up(0, location = src)
+ smoke.set_up(0, holder = src, location = src)
smoke.start()
/obj/vehicle/ridden/secway/welder_act(mob/living/user, obj/item/I)
diff --git a/code/modules/vehicles/vehicle_actions.dm b/code/modules/vehicles/vehicle_actions.dm
index f2ec9cd44f52e..b74f94521b723 100644
--- a/code/modules/vehicles/vehicle_actions.dm
+++ b/code/modules/vehicles/vehicle_actions.dm
@@ -282,6 +282,19 @@
owner.say("Thank you for the fun ride, [clown.name]!")
clown_car.increment_thanks_counter()
+/datum/action/vehicle/ridden/wheelchair/bell
+ name = "Bell Ring"
+ desc = "Ring the bell."
+ icon_icon = 'icons/obj/bureaucracy.dmi'
+ button_icon_state = "desk_bell"
+ check_flags = AB_CHECK_CONSCIOUS
+ var/bell_cooldown
+
+/datum/action/vehicle/ridden/wheelchair/bell/Trigger(trigger_flags)
+ if(TIMER_COOLDOWN_CHECK(src, bell_cooldown))
+ return
+ TIMER_COOLDOWN_START(src, bell_cooldown, 0.5 SECONDS)
+ playsound(vehicle_ridden_target, 'sound/machines/microwave/microwave-end.ogg', 70)
/datum/action/vehicle/ridden/scooter/skateboard/ollie
name = "Ollie"
diff --git a/code/modules/vehicles/wheelchair.dm b/code/modules/vehicles/wheelchair.dm
index 6dee4da1d05b1..d055615580188 100644
--- a/code/modules/vehicles/wheelchair.dm
+++ b/code/modules/vehicles/wheelchair.dm
@@ -14,6 +14,14 @@
var/image/wheels_overlay
///Determines the typepath of what the object folds into
var/foldabletype = /obj/item/wheelchair
+ ///Bell attached to the wheelchair, if we have one.
+ var/obj/structure/desk_bell/bell_attached
+
+/obj/vehicle/ridden/wheelchair/generate_actions()
+ . = ..()
+ if(!bell_attached)
+ return
+ initialize_controller_action_type(/datum/action/vehicle/ridden/wheelchair/bell, VEHICLE_CONTROL_DRIVE)
/obj/vehicle/ridden/wheelchair/Initialize(mapload)
. = ..()
@@ -56,6 +64,9 @@
. = ..()
if(has_buckled_mobs())
. += wheels_overlay
+ if(bell_attached)
+ . += "wheelchair_bell"
+
/// I assign the ridable element in this so i don't have to fuss with hand wheelchairs and motor wheelchairs having different subtypes
/obj/vehicle/ridden/wheelchair/proc/make_ridable()
@@ -117,3 +128,28 @@
var/obj/vehicle/ridden/wheelchair/wheelchair_unfolded = new unfolded_type(location)
wheelchair_unfolded.add_fingerprint(user)
qdel(src)
+
+
+///attaches bell to the wheelchair
+/obj/vehicle/ridden/wheelchair/proc/attach_bell(obj/structure/desk_bell/bell)
+ bell_attached = bell
+ bell.forceMove(src)
+ generate_actions()
+ update_appearance()
+
+/obj/vehicle/ridden/wheelchair/examine(mob/user)
+ . =..()
+ if(bell_attached)
+ . += span_notice("There is \a [bell_attached] attached to the handle.")
+
+/obj/vehicle/ridden/wheelchair/Destroy()
+ if(bell_attached)
+ remove_bell()
+ return ..()
+
+/obj/vehicle/ridden/wheelchair/proc/remove_bell()
+ bell_attached.forceMove(get_turf(src))
+ usr.visible_message(span_notice("[bell_attached] falls off!"))
+ bell_attached = null
+ update_appearance()
+
diff --git a/code/modules/vending/_vending.dm b/code/modules/vending/_vending.dm
index 8ca7bd1e13c14..0656327c4f924 100644
--- a/code/modules/vending/_vending.dm
+++ b/code/modules/vending/_vending.dm
@@ -1329,6 +1329,8 @@ GLOBAL_LIST_EMPTY(vending_products)
icon_state = "greed"
icon_deny = "greed-deny"
panel_type = "panel4"
+ max_integrity = 700
+ max_loaded_items = 40
light_mask = "greed-light-mask"
custom_materials = list(/datum/material/gold = MINERAL_MATERIAL_AMOUNT * 5)
diff --git a/code/modules/vending/medical.dm b/code/modules/vending/medical.dm
index ed0a4a58ef902..0f33fe7a5ff03 100644
--- a/code/modules/vending/medical.dm
+++ b/code/modules/vending/medical.dm
@@ -17,6 +17,7 @@
/obj/item/stack/medical/ointment = 2,
/obj/item/stack/medical/suture = 2,
/obj/item/stack/medical/bone_gel/four = 4,
+ /obj/item/cane/white = 2,
)
contraband = list(
/obj/item/storage/box/gum/happiness = 3,
diff --git a/code/modules/vending/megaseed.dm b/code/modules/vending/megaseed.dm
index d9ca3c40bc8d9..95d31ae47ef30 100644
--- a/code/modules/vending/megaseed.dm
+++ b/code/modules/vending/megaseed.dm
@@ -31,6 +31,7 @@
/obj/item/seeds/korta_nut = 3,
/obj/item/seeds/lemon = 3,
/obj/item/seeds/lime = 3,
+ /obj/item/seeds/olive = 3,
/obj/item/seeds/onion = 3,
/obj/item/seeds/orange = 3,
/obj/item/seeds/peas = 3,
diff --git a/code/modules/vending/toys.dm b/code/modules/vending/toys.dm
index d969c25a75039..e3f3b3316f244 100644
--- a/code/modules/vending/toys.dm
+++ b/code/modules/vending/toys.dm
@@ -17,7 +17,7 @@
/obj/item/toy/foamblade = 10,
/obj/item/toy/balloon/syndicate = 10,
/obj/item/clothing/suit/syndicatefake = 5,
- /obj/item/clothing/head/syndicatefake = 5,,
+ /obj/item/clothing/head/syndicatefake = 5,
)
contraband = list(
/obj/item/gun/ballistic/shotgun/toy/crossbow = 10,
diff --git a/code/modules/wiremod/components/action/laserpointer.dm b/code/modules/wiremod/components/action/laserpointer.dm
index d3eb5e13a1ebe..6f11f69f73c5c 100644
--- a/code/modules/wiremod/components/action/laserpointer.dm
+++ b/code/modules/wiremod/components/action/laserpointer.dm
@@ -1,4 +1,3 @@
-
/**
* # laser pointer Component
*
@@ -10,9 +9,6 @@
category = "Action"
circuit_flags = CIRCUIT_FLAG_INPUT_SIGNAL|CIRCUIT_FLAG_OUTPUT_SIGNAL
-/// The Laser Pointer Variables
- var/turf/pointer_loc
-
/// The input port
var/datum/port/input/target_input
var/datum/port/input/image_pixel_x = 0
@@ -47,7 +43,7 @@
var/atom/target = target_input.value
var/atom/movable/shell = parent.shell
- var/turf/targloc = get_turf(target)
+ var/turf/target_location = get_turf(target)
var/pointer_icon_state = lasercolour_option.value
@@ -56,20 +52,17 @@
if(get_dist(current_turf, target) > max_range || current_turf.z != target.z)
return
- /// only has cyborg flashing since felinid moving spikes time dilation when spammed and the other two features of laserpointers would be unbalanced when spammed
+ // only has cyborg flashing since felinid moving spikes time dilation when spammed and the other two features of laserpointers would be unbalanced when spammed
if(iscyborg(target))
var/mob/living/silicon/silicon = target
log_combat(shell, silicon, "shone in the sensors", src)
- silicon.flash_act(affect_silicon = 1) /// no stunning, just a blind
+ silicon.flash_act(affect_silicon = TRUE) /// no stunning, just a blind
to_chat(silicon, span_danger("Your sensors were overloaded by a weakened laser shone by [shell]!"))
-
- ///laserpointer image
- var/image/laser_location = image('icons/obj/guns/projectiles.dmi',targloc,"[pointer_icon_state]_laser",10)
+ var/image/laser_location = image('icons/obj/guns/projectiles.dmi',target_location,"[pointer_icon_state]_laser",10)
laser_location.pixel_x = clamp(target.pixel_x + image_pixel_x.value,-15,15)
laser_location.pixel_y = clamp(target.pixel_y + image_pixel_y.value,-15,15)
- flick_overlay_view(laser_location, targloc, 10)
-
-
+ target_location.add_overlay(laser_location)
+ addtimer(CALLBACK(target_location, /atom/proc/cut_overlay, laser_location), 1 SECONDS)
diff --git a/code/modules/wiremod/components/action/soundemitter.dm b/code/modules/wiremod/components/action/soundemitter.dm
index 1ca4f3db58810..ea43e937711c5 100644
--- a/code/modules/wiremod/components/action/soundemitter.dm
+++ b/code/modules/wiremod/components/action/soundemitter.dm
@@ -15,22 +15,35 @@
/// Volume of the sound when played
var/datum/port/input/volume
+ /// Whether to play the sound backwards
+ var/datum/port/input/backwards
+
/// Frequency of the sound when played
var/datum/port/input/frequency
/// The cooldown for this component of how often it can play sounds.
var/sound_cooldown = 2 SECONDS
+ /// The maximum pitch this component can play sounds at.
+ var/max_pitch = 50
+ /// The minimum pitch this component can play sounds at.
+ var/min_pitch = -50
+ /// The maximum volume this component can play sounds at.
+ var/max_volume = 30
+
var/list/options_map
/obj/item/circuit_component/soundemitter/get_ui_notices()
. = ..()
. += create_ui_notice("Sound Cooldown: [DisplayTimeText(sound_cooldown)]", "orange", "stopwatch")
+ if(CONFIG_GET(flag/disallow_circuit_sounds))
+ . += create_ui_notice("Non-functional", "red", "exclamation")
/obj/item/circuit_component/soundemitter/populate_ports()
volume = add_input_port("Volume", PORT_TYPE_NUMBER, default = 35)
frequency = add_input_port("Frequency", PORT_TYPE_NUMBER, default = 0)
+ backwards = add_input_port("Play Backwards", PORT_TYPE_NUMBER, default = 0)
/obj/item/circuit_component/soundemitter/populate_options()
var/static/component_options = list(
@@ -61,9 +74,16 @@
/obj/item/circuit_component/soundemitter/pre_input_received(datum/port/input/port)
volume.set_value(clamp(volume.value, 0, 100))
- frequency.set_value(clamp(frequency.value, -100, 100))
+ frequency.set_value(clamp(frequency.value, min_pitch, max_pitch))
+ backwards.set_value(clamp(backwards.value, 0, 1))
/obj/item/circuit_component/soundemitter/input_received(datum/port/input/port)
+ if(CONFIG_GET(flag/disallow_circuit_sounds))
+ ui_color = "red"
+ return
+ else
+ ui_color = initial(ui_color)
+
if(TIMER_COOLDOWN_CHECK(parent, COOLDOWN_CIRCUIT_SOUNDEMITTER))
return
@@ -71,6 +91,12 @@
if(!sound_to_play)
return
- playsound(src, sound_to_play, volume.value, frequency != 0, frequency = frequency.value)
+ var/actual_frequency = 1 + (frequency.value/100)
+ var/actual_volume = max_volume * (volume.value/100)
+
+ if(backwards.value)
+ actual_frequency = -actual_frequency
+
+ playsound(src, sound_to_play, actual_volume, TRUE, frequency = actual_frequency)
TIMER_COOLDOWN_START(parent, COOLDOWN_CIRCUIT_SOUNDEMITTER, sound_cooldown)
diff --git a/code/modules/wiremod/components/id/access_checker.dm b/code/modules/wiremod/components/id/access_checker.dm
index fddb4f1f39e6d..5778bd987ed6e 100644
--- a/code/modules/wiremod/components/id/access_checker.dm
+++ b/code/modules/wiremod/components/id/access_checker.dm
@@ -1,6 +1,6 @@
/obj/item/circuit_component/compare/access
display_name = "Access Checker"
- desc = "Performs a basic comparison between two numerical lists, with additional functions that help in using it to check access on IDs."
+ desc = "Performs a basic comparison between two lists of strings, with additional functions that help in using it to check access on IDs."
category = "ID"
input_port_amount = 0 //Uses custom ports for its comparisons
@@ -26,8 +26,8 @@
. += create_ui_notice("When \"Check Any\" is false, returns true only if \"Access To Check\" contains ALL values in \"Required Access\".", "orange", "info")
/obj/item/circuit_component/compare/access/populate_custom_ports()
- subject_accesses = add_input_port("Access To Check", PORT_TYPE_LIST(PORT_TYPE_NUMBER))
- required_accesses = add_input_port("Required Access", PORT_TYPE_LIST(PORT_TYPE_NUMBER))
+ subject_accesses = add_input_port("Access To Check", PORT_TYPE_LIST(PORT_TYPE_STRING))
+ required_accesses = add_input_port("Required Access", PORT_TYPE_LIST(PORT_TYPE_STRING))
check_any = add_input_port("Check Any", PORT_TYPE_NUMBER)
/obj/item/circuit_component/compare/access/save_data_to_list(list/component_data)
diff --git a/code/modules/wiremod/components/id/access_reader.dm b/code/modules/wiremod/components/id/access_reader.dm
index cb576daa7e5f3..29866f815a9a4 100644
--- a/code/modules/wiremod/components/id/access_reader.dm
+++ b/code/modules/wiremod/components/id/access_reader.dm
@@ -19,7 +19,7 @@
/obj/item/circuit_component/id_access_reader/populate_ports()
target = add_input_port("Target", PORT_TYPE_ATOM)
- access_port = add_output_port("Access", PORT_TYPE_LIST(PORT_TYPE_NUMBER))
+ access_port = add_output_port("Access", PORT_TYPE_LIST(PORT_TYPE_STRING))
/obj/item/circuit_component/id_access_reader/input_received(datum/port/input/port)
diff --git a/code/modules/wiremod/core/admin_panel.dm b/code/modules/wiremod/core/admin_panel.dm
index 33af02f5140e5..74e72ad5efe0c 100644
--- a/code/modules/wiremod/core/admin_panel.dm
+++ b/code/modules/wiremod/core/admin_panel.dm
@@ -29,6 +29,13 @@
if (.)
return .
+ switch(action)
+ if ("disable_circuit_sound")
+ CONFIG_SET(flag/disallow_circuit_sounds, !CONFIG_GET(flag/disallow_circuit_sounds))
+ message_admins("[key_name_admin(usr)] has toggled all circuit sounds [CONFIG_GET(flag/disallow_circuit_sounds)? "off" : "on"].")
+ log_admin("[key_name(usr)] has toggled all circuit sounds [CONFIG_GET(flag/disallow_circuit_sounds)? "off" : "on"].")
+ return TRUE
+
if (!istext(params["circuit"]))
return FALSE
diff --git a/code/modules/zombie/items.dm b/code/modules/zombie/items.dm
index 38863c56637c0..f7aa485491764 100644
--- a/code/modules/zombie/items.dm
+++ b/code/modules/zombie/items.dm
@@ -48,6 +48,10 @@
// zombies)
return
+ // spaceacillin has a 75% chance to block infection
+ if(istype(target) && target.reagents.has_reagent(/datum/reagent/medicine/spaceacillin) && prob(75))
+ return
+
var/obj/item/organ/internal/zombie_infection/infection
infection = target.getorganslot(ORGAN_SLOT_ZOMBIE)
if(!infection)
diff --git a/config/config.txt b/config/config.txt
index 1b87a1ea35538..67b299b086396 100644
--- a/config/config.txt
+++ b/config/config.txt
@@ -166,6 +166,9 @@ LOG_TOOLS
## Log all timers on timer auto reset
# LOG_TIMERS_ON_BUCKET_RESET
+## log speech indicators
+LOG_SPEECH_INDICATORS
+
##Log camera pictures - Must have picture logging enabled
PICTURE_LOGGING_CAMERA
diff --git a/config/game_options.txt b/config/game_options.txt
index 7fddf818cc686..cf846a1576938 100644
--- a/config/game_options.txt
+++ b/config/game_options.txt
@@ -119,6 +119,9 @@ PROTECT_ROLES_FROM_ANTAGONIST
## If non-human species are barred from joining as a head of staff
#ENFORCE_HUMAN_AUTHORITY
+## If non-human species are barred from joining as a head of staff, including jobs flagged as allowed for non-humans, ie. Quartermaster.
+#ENFORCE_HUMAN_AUTHORITY_ON_EVERYONE
+
## If late-joining players have a chance to become a traitor/changeling
ALLOW_LATEJOIN_ANTAGONISTS
@@ -231,7 +234,6 @@ RANDOM_LAWS corporate
## Quirky laws. Shouldn't cause too much harm
#RANDOM_LAWS hippocratic
#RANDOM_LAWS maintain
-#RANDOM_LAWS drone
#RANDOM_LAWS liveandletlive
#RANDOM_LAWS peacekeeper
#RANDOM_LAWS reporter
@@ -243,6 +245,7 @@ RANDOM_LAWS corporate
#RANDOM_LAWS ninja
#RANDOM_LAWS antimov
#RANDOM_LAWS thermodynamic
+#RANDOM_LAWS drone
## meme laws. Honk
#RANDOM_LAWS buildawall
@@ -255,28 +258,36 @@ RANDOM_LAWS corporate
LAW_WEIGHT custom,0
## standard-ish laws. These are fairly ok to run
-LAW_WEIGHT asimov,32
-LAW_WEIGHT asimovpp,12
-LAW_WEIGHT paladin,12
-LAW_WEIGHT robocop,12
-LAW_WEIGHT corporate,12
+## Unique AI station trait uses weights so we don't want asimov
+LAW_WEIGHT asimov,0
+LAW_WEIGHT asimovpp,5
+LAW_WEIGHT paladin,5
+LAW_WEIGHT paladin5,5
+LAW_WEIGHT robocop,5
+LAW_WEIGHT corporate,5
+LAW_WEIGHT hippocratic,5
+LAW_WEIGHT maintain,5
+LAW_WEIGHT liveandletlive,5
+LAW_WEIGHT peacekeeper,5
+LAW_WEIGHT ten_commandments,5
+LAW_WEIGHT nutimov,5
## Quirky laws. Shouldn't cause too much harm
-LAW_WEIGHT hippocratic,3
-LAW_WEIGHT maintain,4
-LAW_WEIGHT drone,3
-LAW_WEIGHT liveandletlive,3
-LAW_WEIGHT peacekeeper,3
-LAW_WEIGHT reporter,4
-LAW_WEIGHT hulkamania,4
-LAW_WEIGHT ten_commandments,4
+LAW_WEIGHT reporter,3
+LAW_WEIGHT hulkamania,3
+LAW_WEIGHT tyrant,3
+LAW_WEIGHT overlord,3
+LAW_WEIGHT painter,3
+LAW_WEIGHT dungeon_master,3
## Bad idea laws. Probably shouldn't enable these
LAW_WEIGHT syndie,0
LAW_WEIGHT ninja,0
LAW_WEIGHT antimov,0
+LAW_WEIGHT balance,0
LAW_WEIGHT thermodynamic,0
LAW_WEIGHT buildawall,0
+LAW_WEIGHT drone,0
##------------------------------------------------
@@ -459,10 +470,12 @@ MAXFINE 2000
## How many played hours of DRONE_REQUIRED_ROLE required to be a Maintenance Done
#DRONE_ROLE_PLAYTIME 14
-## Uncomment to enable SDQL spells
-## Warning: SDQL is a powerful tool and can break many things or expose security sensitive information.
-## Giving players access to it has major security concerns, be careful and deliberate when using this feature.
-#SDQL_SPELLS
-
## Whether native FoV is enabled for all people.
#NATIVE_FOV
+
+## Whether circuit sounds are allowed to be played or not.
+#DISALLOW_CIRCUIT_SOUNDS
+
+## Comment if you wish to enable title music playing at the lobby screen. This flag is disabled by default to facilitate better code testing on local machines.
+## Do keep in mind that this flag will not affect individual player's preferences: if they opt-out on your server, it will never play for them.
+DISALLOW_TITLE_MUSIC
diff --git a/config/spaceruinblacklist.txt b/config/spaceruinblacklist.txt
index 7344159e7bd63..09a77a308490d 100644
--- a/config/spaceruinblacklist.txt
+++ b/config/spaceruinblacklist.txt
@@ -19,7 +19,7 @@
#_maps/RandomRuins/SpaceRuins/crashedclownship.dmm
#_maps/RandomRuins/SpaceRuins/crashedship.dmm
#_maps/RandomRuins/SpaceRuins/deepstorage.dmm
-#_maps/RandomRuins/SpaceRuins/derelict1.dmm
+#_maps/RandomRuins/SpaceRuins/derelict_sulaco.dmm
#_maps/RandomRuins/SpaceRuins/derelict2.dmm
#_maps/RandomRuins/SpaceRuins/derelict3.dmm
#_maps/RandomRuins/SpaceRuins/derelict4.dmm
@@ -52,4 +52,4 @@
#_maps/RandomRuins/SpaceRuins/forgottenship.dmm
#_maps/RandomRuins/SpaceRuins/hellfactory.dmm
#_maps/RandomRuins/SpaceRuins/space_billboard.dmm
-#_maps/RandomRuins/SpaceRuins/spinwardsmoothies.dmm
\ No newline at end of file
+#_maps/RandomRuins/SpaceRuins/spinwardsmoothies.dmm
diff --git a/html/changelogs/AutoChangeLog-pr-67038.yml b/html/changelogs/AutoChangeLog-pr-67038.yml
deleted file mode 100644
index 45bdd8c40ccd1..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67038.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Crumpaloo"
-delete-after: True
-changes:
- - imageadd: "Added new sprites for the airlock painter, tile & decal sprayer."
diff --git a/html/changelogs/AutoChangeLog-pr-67263.yml b/html/changelogs/AutoChangeLog-pr-67263.yml
deleted file mode 100644
index 332bbf4bec5fd..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67263.yml
+++ /dev/null
@@ -1,7 +0,0 @@
-author: "Melbert"
-delete-after: True
-changes:
- - balance: "Heretic: The Amber Focus is now fireproof."
- - balance: "Heretic: The Eldritch Medallion (thermal vision necklace) is now fireproof, acid proof, and works as a focus."
- - balance: "Heretic: The Void Cloak can carry more things in its pocket, including various ritual components (organs, bodyparts, flowers), small heretic items, and a singular sickly blade. It also functions as a focus while the hood is down."
- - balance: "Heretic: Mawed Crucible potions are now small sized (down from normal)."
diff --git a/html/changelogs/AutoChangeLog-pr-67328.yml b/html/changelogs/AutoChangeLog-pr-67328.yml
deleted file mode 100644
index 755307ccde176..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67328.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "SingingSpock"
-delete-after: True
-changes:
- - bugfix: "Changed triple citrus recipe to make 3u instead of 5u"
diff --git a/html/changelogs/AutoChangeLog-pr-67391.yml b/html/changelogs/AutoChangeLog-pr-67391.yml
deleted file mode 100644
index cdba64aa8639c..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67391.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "tralezab"
-delete-after: True
-changes:
- - balance: "Engineering SMESes now start with a bit more juice."
diff --git a/html/changelogs/AutoChangeLog-pr-67403.yml b/html/changelogs/AutoChangeLog-pr-67403.yml
deleted file mode 100644
index 444a5082ad067..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67403.yml
+++ /dev/null
@@ -1,5 +0,0 @@
-author: "GoldenAlpharex"
-delete-after: True
-changes:
- - bugfix: "The stasis ripple effect will now play in a loop as intended, rather than only playing once."
- - bugfix: "Buckling down someone to a stasis bed should no longer occasionally make them lie down veeeeery slowly."
diff --git a/html/changelogs/AutoChangeLog-pr-67424.yml b/html/changelogs/AutoChangeLog-pr-67424.yml
deleted file mode 100644
index 00ea8b5d0705f..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67424.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Ryll/Shaps"
-delete-after: True
-changes:
- - bugfix: "Fixed pellet clouds not being able to wound"
diff --git a/html/changelogs/AutoChangeLog-pr-67439.yml b/html/changelogs/AutoChangeLog-pr-67439.yml
deleted file mode 100644
index 29c22db63461d..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67439.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "robbertapir"
-delete-after: True
-changes:
- - bugfix: "Made engraving not throw errors when everything works as expected."
diff --git a/html/changelogs/AutoChangeLog-pr-67450.yml b/html/changelogs/AutoChangeLog-pr-67450.yml
deleted file mode 100644
index 68b777c6507e6..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67450.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Fikou"
-delete-after: True
-changes:
- - bugfix: "surplus prosthetics have correct sprites now"
diff --git a/html/changelogs/AutoChangeLog-pr-67453.yml b/html/changelogs/AutoChangeLog-pr-67453.yml
deleted file mode 100644
index f13845381191f..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67453.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Son-of-Space"
-delete-after: True
-changes:
- - bugfix: "Some overlapping objects were adjusted on the walls in the firing range on MetaStation"
diff --git a/html/changelogs/AutoChangeLog-pr-67456.yml b/html/changelogs/AutoChangeLog-pr-67456.yml
deleted file mode 100644
index fc2cf143caf56..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67456.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Melbert"
-delete-after: True
-changes:
- - bugfix: "Light switches no longer cause anchored objects over conveyors to move."
diff --git a/html/changelogs/AutoChangeLog-pr-67460.yml b/html/changelogs/AutoChangeLog-pr-67460.yml
deleted file mode 100644
index d89a744d076dc..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67460.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Melbert"
-delete-after: True
-changes:
- - balance: "Cremator button has no access requirements"
diff --git a/html/changelogs/AutoChangeLog-pr-67464.yml b/html/changelogs/AutoChangeLog-pr-67464.yml
deleted file mode 100644
index 317a8be3f22bd..0000000000000
--- a/html/changelogs/AutoChangeLog-pr-67464.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-author: "Pandarsenic"
-delete-after: True
-changes:
- - bugfix: "Shuffles objects to stop sprites from clipping or covering each other (with differing levels of severity) on IceBox's overcrowded dormitory walls."
diff --git a/html/changelogs/AutoChangeLog-pr-68143.yml b/html/changelogs/AutoChangeLog-pr-68143.yml
new file mode 100644
index 0000000000000..92c7a5001029b
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-68143.yml
@@ -0,0 +1,4 @@
+author: "TheBoondock"
+delete-after: True
+changes:
+ - bugfix: "fixed crystalizer passing any item as the user and causing a runtime"
diff --git a/html/changelogs/AutoChangeLog-pr-68153.yml b/html/changelogs/AutoChangeLog-pr-68153.yml
new file mode 100644
index 0000000000000..59c270383d6d4
--- /dev/null
+++ b/html/changelogs/AutoChangeLog-pr-68153.yml
@@ -0,0 +1,4 @@
+author: "Salex08"
+delete-after: True
+changes:
+ - bugfix: "you start with the right amount of breathing mask again"
diff --git a/html/changelogs/archive/2022-06.yml b/html/changelogs/archive/2022-06.yml
index 3774442b59368..0a081ceb0f640 100644
--- a/html/changelogs/archive/2022-06.yml
+++ b/html/changelogs/archive/2022-06.yml
@@ -65,3 +65,991 @@
long
tralezab:
- balance: Removed silver costs from surgery tools
+2022-06-03:
+ Crumpaloo:
+ - imageadd: Added new sprites for the airlock painter, tile & decal sprayer.
+ Fikou:
+ - bugfix: surplus prosthetics have correct sprites now
+ GoldenAlpharex:
+ - bugfix: The stasis ripple effect will now play in a loop as intended, rather than
+ only playing once.
+ - bugfix: Buckling down someone to a stasis bed should no longer occasionally make
+ them lie down veeeeery slowly.
+ Melbert:
+ - balance: 'Heretic: The Amber Focus is now fireproof.'
+ - balance: 'Heretic: The Eldritch Medallion (thermal vision necklace) is now fireproof,
+ acid proof, and works as a focus.'
+ - balance: 'Heretic: The Void Cloak can carry more things in its pocket, including
+ various ritual components (organs, bodyparts, flowers), small heretic items,
+ and a singular sickly blade. It also functions as a focus while the hood is
+ down.'
+ - balance: 'Heretic: Mawed Crucible potions are now small sized (down from normal).'
+ - balance: Cremator button has no access requirements
+ - bugfix: Light switches no longer cause anchored objects over conveyors to move.
+ Pandarsenic:
+ - bugfix: Shuffles objects to stop sprites from clipping or covering each other
+ (with differing levels of severity) on IceBox's overcrowded dormitory walls.
+ Ryll/Shaps:
+ - bugfix: Fixed pellet clouds not being able to wound
+ SingingSpock:
+ - bugfix: Changed triple citrus recipe to make 3u instead of 5u
+ Son-of-Space:
+ - bugfix: Some overlapping objects were adjusted on the walls in the firing range
+ on MetaStation
+ - code_imp: Adds some greps to check for commonly misplaced structures in closed
+ turfs
+ - bugfix: Some objects stacked within closed turfs have been removed from those
+ turfs.
+ robbertapir:
+ - bugfix: Made engraving not throw errors when everything works as expected.
+ tralezab:
+ - balance: Engineering SMESes now start with a bit more juice.
+ vincentiusvin:
+ - qol: Added a roundstart program disk containing nt frontier
+ - code_imp: Changed ordnance's area definition a bit, this includes the misc labs
+ (usually used for circuit labs). Gameplay wise they will have new names.
+ - code_imp: Made the ordnance chamber injector start off. You gotta turn them on
+ using the monitors. Also tidied their code a bit.
+2022-06-04:
+ Profakos:
+ - bugfix: Player-facing Traitor reputation numbers are now consistent when you view
+ how much you have.
+ Rhials:
+ - bugfix: Moves the scrubber out from under a vending machine in the Metastation
+ Meeting Room.
+ SmArtKar:
+ - bugfix: Fixed statue simplemob teleport not working and 3 other spells not appearing
+ Son-of-Space:
+ - bugfix: departmental officers' access across departments has been standardized,
+ and previously lost accesses were added back
+ - balance: Departmental security officers have access to more areas in their departments,
+ including xenobiology or virology
+ Zonespace27:
+ - admin: MODsuits can now be picked through the outfit manager
+ san7890:
+ - rscadd: On all five stations, Nanotrasen has redrawn up the area plans in the
+ permabrig areas. Expect to see a few more APCs in each room to feed each with
+ power.
+ vincentiusvin:
+ - qol: breathedeep makes a return in the atmozphere tablet app. Right click to scan
+ things, right self click (on the tablet) to scan current turf.
+2022-06-05:
+ Son-of-Space:
+ - bugfix: A severe lack of plating under a window in the DeltaStation rec room was
+ remedied
+ dragomagol:
+ - admin: cyborg wire pulses/cuts are now logged in silicon.log
+ - admin: AIs being carded is now logged in silicon.log
+ - admin: giving an AI a combat module is now logged in silicon.log
+ - admin: trying to upload over the maximum number of laws is now logged in silicon.log
+ - admin: ion storm law changes are now logged in silicon.log
+ - admin: changing settings on a borg shell is now logged in silicon.log
+ robbertapir:
+ - bugfix: held memorizers are now visible
+2022-06-06:
+ Dragomagol, sprites by MistakeNot4892:
+ - rscadd: 'Added a new pAI holoform: the crow!'
+ EOBGames:
+ - rscadd: 'A few new crates have made their way to cargo: buy yourself a Lizard
+ or Moth food crate today!'
+ - rscadd: Recipes for Yoghurt (10u cream, 2u virus food), Cornmeal (grind corn),
+ and Quality Oil (1u quality oil, 2u cooking oil) have been added. Bon appetit!
+ - balance: 'Species food (lizard and moth food) have received a sweep of balance
+ changes: they''re now more filling and a bit easier to access.'
+ Iamgoofball:
+ - bugfix: Central Command no longer erroneously refers to the Ice Box planet as
+ a station in orbit.
+ - bugfix: Under construction airlocks no longer have paper stuck to them.
+ Jolly:
+ - bugfix: One of Trams ladder hatches no longer uses a redundant "all-access" helper
+ to help you escape. The door in question already had no access requirements.
+ Looks-to-the-Moon:
+ - bugfix: Xenomorph larva cancelling their evolution no longer displays unnecessary
+ messages
+ Melbert:
+ - bugfix: Fixes some cinematics sticking around for longer than comfortable
+ Rhials:
+ - bugfix: right clicking the BEPIS no longer makes it invisible.
+ SpaceSmithers:
+ - bugfix: Electric razors are now functional again
+ kugamo:
+ - bugfix: fixed parallax blue stars showing through parallax asteroids.
+ magatsuchi:
+ - code_imp: replaces some slot names with proper names
+ robbertapir:
+ - bugfix: Caught Molotovs no longer immolate the target.
+ vincentiusvin:
+ - bugfix: fixed a bigger dose of zombie powder permasleeping you
+ - bugfix: fixed regular scientists spawning in RD's office in kilo
+2022-06-07:
+ 13spacemen:
+ - rscadd: You can now add assemblies to welding fuel tanks to blow them up
+ ATHATH:
+ - rscadd: Bulky crowbars have been added to all fire-safety lockers.
+ - spellcheck: Large crowbars are now named "large crowbars" instead of just "crowbars".
+ - bugfix: Unholy water's stun length reduction effect is now as potent as it was
+ intended to be. Its strength was previously half as strong as it was intended
+ to be due to a copy+paste error.
+ CocaColaTastesGood:
+ - bugfix: Fixes stack multiplier exploit
+ Fikou:
+ - qol: ninjas now get told about pinning modules and the direction to the station
+ - bugfix: fixes modsuits fucking up when unequipping every item, like staff of change
+ or slime toxin or whatever
+ Iamgoofball:
+ - bugfix: You can no longer roll more IDs to steal than there are crewmembers for
+ All Access Fan.
+ - bugfix: Firelocks no longer check the atmospheric contents of solid walls.
+ - bugfix: Health Analyzers now properly flag robotic and prosthetic limbs again.
+ Iatots:
+ - imageadd: Hot Cocoa and Tea now come in mugs again.
+ Jolly, sprites by Blueshirtguy:
+ - rscadd: 'Jolly: Added the flower garland to the crafting tab under clothing! You''ll
+ need 4 of these flowers to craft it: poppies, harebells and roses. It''ll also
+ calm your nerves a bit, if you''re on edge.'
+ - imageadd: 'Blueshirtguy: Sprites for the flower garland on-mob and icon sprites.'
+ Mooshimi:
+ - bugfix: There is now air in the Deltastation security escape pod airlock.
+ Mothblocks:
+ - bugfix: Fixed Adminwho taking several seconds to resolve.
+ Pandarsenic:
+ - bugfix: Corrected a couple of trivial typos.
+ RandomGamer123:
+ - bugfix: Nameless ID cards (with security access) can now access the security records
+ console without issue
+ ReinaCoder:
+ - imageadd: The Soviet costume has been resprited and the russian mobs have been
+ updated to match.
+ Son-of-Space:
+ - bugfix: You can no longer get objectives on a random tile in maintenance on TramStation,
+ along with any other area/based assignments.
+ - bugfix: You can now properly pull or cancel a fire alarm from outside the eastern
+ side of the library on MetaStation.
+ - bugfix: Inspect bounties no longer give you invalid areas to scan for bounties
+ - bugfix: Inspect bounties now properly choose from a broader range of assignments
+ in service, maintenance, commons, etc.
+ SpaceSmithers:
+ - qol: The Tip of the Round will no longer misinform you about PACMAN generators.
+ Wallem:
+ - rscdel: Full Ant Party Pizza pies has been removed. Instead, you can get ant pizza
+ slices by pouring ants on margherita pizza slices.
+ itseasytosee:
+ - balance: Some items are better at damaging structures and robots than others!
+ Don't try to kill a metal death robot with a scalpel, use a toolbox!
+ robbertapir:
+ - bugfix: Photon projector implants can no longer be used in assemblies. This means
+ that they can no longer bilocate.
+ san7890:
+ - rscadd: Nanotrasen has finally tracked down an elusive signal that's been haunting
+ them over all of their broadcasts... there appears to be a new Syndicate Listening
+ Base commissioned.
+ - balance: The odds for a Syndicate Communications Agent (the Space kind) is now
+ at an 15% chance to spawn.
+ - balance: Nanotrasen has now implemented a "buffer zone" on IceBoxStation between
+ the wilderness portions of the moon and the parts where there is a station presence.
+ Hopefully, you should see a lot less fauna try to make their way on station.
+ silicons:
+ - bugfix: infinite loop on process_hit in projectiles when hitting ON_BORDER objects,
+ like windoors
+ timothymtorres:
+ - bugfix: Fix drones not being able to use computers or vault.
+ - bugfix: Fix monkeys being able to read or write. They are now illiterate however
+ they can gain literacy through the Clever mutation.
+ - bugfix: Fix illiterate mobs being able to receive tablet messages in their chat
+ log.
+2022-06-08:
+ Guillaume Prata:
+ - balance: Eating clothing as Mothpeople will give you cloth fibers instead of nutriment.
+ Cloth fibers give temporary nourishment that gets removed when it finishes metabolizing.
+ JohnFulpWillard:
+ - balance: Station equipment that holds materials (techfabs, ORMs) can't connect
+ to ore silo's on a different Z level.
+ Paxilmaniac:
+ - rscadd: Solar panel assemblies and solar tracker electronics can now be made in
+ an autolathe
+ ReinaCoder:
+ - imageadd: Resprites the white costume found inside the costume vendor.
+ SnoopCooper:
+ - balance: BZ production rates between pipes and turfs are now consistent. O2 production
+ removed.
+ - bugfix: Multiplying production rates by splitting pipenets no longer possible.
+ Son-of-Space:
+ - code_imp: Reorganizes some of the access and jobs access code for readability
+ - balance: The minisat and tcomms are more accessible to engineering roles on skeleton
+ crew, and engineers normally
+ - balance: Service personnel who do not handle corpses have had their morgue access
+ revoked and moved to skeleton crew.
+ - balance: The HOP has had their cremator access revoked, as they are not licensed
+ to handle the dead
+ - balance: The Research Director has had their mining and mining station access
+ removed.
+ - balance: The Research Director has been given construction access to allow for
+ access to the minisat and that access has been removed from the HOP
+ - balance: The roboticist has had their skeleton crew access to ordnance revoked
+ to align with the geneticist's skeleton crew access
+ - balance: Miners no longer have SHIPPING access (previously Mail Sorting)
+ - bugfix: The HOS has proper access to the basics in each department again
+ - bugfix: A holdover access from when genetics was in medical has been removed from
+ the Research Director
+ - bugfix: Paramedics can now access the entrance doors for most departments again
+ - bugfix: Minisats across all maps have proper access requirements to their contents
+ - bugfix: Tech storage now uses its access properly and again requires both command
+ and tech access to get to secure storage
+ Tastyfish:
+ - bugfix: The ID access reader and access checker circuit components now work again
+ with the new string-based access system.
+ - qol: Plumbing now supports 5 layers.
+ - qol: The plumbing constructor can now place ducts and change layer via scroll
+ wheel.
+ - bugfix: Bunch of cryptic failures and errors fixed in placing plumbing.
+ - imageadd: New plumbing duct sprites.
+ TemporalOroboros:
+ - admin: Smoke now logs the last person to touch the source of the smoke as the
+ last person to touch the smoke itself. Gunpowder smoke should be less annoying
+ to log dive as a result as every explosion will log that person.
+ Timberpoes:
+ - bugfix: Fixes issue where lobby buttons were still visable and usable under panic
+ bunker x interview system and also allows use of fix chat verb for interviewees.
+ private-tristan:
+ - bugfix: metastation xenobiology area no longer extends 1 tile into space
+ timothymtorres:
+ - qol: Playing Russian Roulette with lethal intent now creates a mood event for
+ the user and is engrained in any nearby peoples memories. The more bullets the
+ better the memory and mood boost.
+ - qol: Replace red colored beacons on solars/catwalk areas outside Meta to be colored
+ according to department (sec is red, medical is blue, etc.)
+ - rscadd: Add all AI lawsets can now be researched and have their modules printed
+ - rscadd: Add all AI lawsets to random spawners in AI upload
+ - rscadd: Add advanced AI techweb node
+ - balance: Change AI lawsets to be in different random spawner categories
+ - balance: Change lawsets chance for unique AI station trait
+ - balance: Move some lawsets out of AI techweb node and put into advanced AI node
+ - code_imp: Add documentation for AI lawset code
+ - config: Add every AI lawset to game_options config
+ - config: Rebalance AI lawsets in game_options config
+ - bugfix: Lawsets overflow to behave correctly
+2022-06-09:
+ 13spacemen:
+ - qol: Fuel and water tanks have examine hints now
+ ATHATH:
+ - rscadd: Cloaks, like surgical drapes and bedsheets, can now initiate surgeries.
+ This change affects both head of staff cloaks and skill capes.
+ Guillaume Prata:
+ - rscadd: Gravity generator blackout is a new random event to spice the rounds.
+ - balance: High intensity gravitational anomalies that don't get neutralized in
+ time will trigger a gravity generator blackout.
+ Iamgoofball:
+ - balance: The Quartermaster is now a Head of Staff, and answers directly to the
+ Captain now.
+ - balance: This comes with all the stuff a Head of Staff normally has, like command
+ access, a telebaton, and a silver ID.
+ - balance: This also comes with no longer being eligible for Head Revolutionary,
+ and being a target for the Revolution.
+ - balance: Thanks to a savvy contract with the Space Teamsters, non-humans are allowed
+ to be Quartermasters still.
+ - balance: The HoP is no longer the lead of Supply, nor does he have access to Supply.
+ - balance: The Warden now carries the torch for pretender royal metabolism, as the
+ last remaining pseudo-head.
+ - code_imp: Removes a hack from the NT IRN code.
+ Iatots:
+ - rscdel: you can't hold bread slices in your mouth (head) anymore.
+ - rscadd: you can hold griddled toast in your mouth (mask).
+ Jakkie Sergal:
+ - rscadd: Added darker floor decals.
+ Jolly:
+ - bugfix: A small bit of Kilos arrivals area define was touched up and fixed. Asteroid
+ rock is not consider part of the station, sorry folks.
+ - qol: On Icebox, some of the warning tapes have been properly rounded off with
+ corners.
+ - bugfix: On Icebox, a vent and scrubber has been added to the primary hall the
+ arrival shuttle drops crew members off at. This should also stop this portion
+ of the hall from having atmos specific issues.
+ Melbert:
+ - bugfix: Dead bodies shouldn't keep jittering for ages.
+ - qol: Research servers are now a bit more clear about why they aren't generating
+ research points. Check the console for more info. If in doubt, turn them off
+ and on again (i'm not kidding).
+ - code_imp: Removed a buncha old, deprecated / unused R&D server code related to
+ them making heat.
+ Pickle-Coding:
+ - bugfix: Fixes bzformation not working.
+ SmArtKar:
+ - bugfix: Fire hallucinations are no longer invisible
+ TehZombehz for the sprites, san7890:
+ - rscadd: For some reason, Donk Co. suddenly found a crate full of alien-themed
+ plushies in their warehouse. They immediately started loading up their arcade
+ machines full of them.
+ Wallem:
+ - rscadd: For the brave smokers out there, legends tell of a new lighting technique
+ involving molten stone from the planet's core. Only the brave are advised to
+ attempt this.
+ Watermelon914:
+ - balance: Traitor objectives have a significantly reduced reputation reduction,
+ making it more viable to gain reputation beyond the expected reputation.
+ robbertapir:
+ - bugfix: You can no longer create negative amounts of alloys in the ORM
+ - bugfix: sechud/medhud buttons in examines now time out after 1 minute. This means
+ that a single examine will no longer allow you to track someone's identity and/or
+ health for the rest of the round.
+ timothymtorres:
+ - rscadd: Add hallucinogen poison to frog attacks
+ zxaber:
+ - rscadd: The Concealed Weapon Bay is available again for traitor Roboticists and
+ Research Directors.
+2022-06-10:
+ Jolly:
+ - bugfix: On Tram, in sec, the floating pepper spray refiller has been removed from
+ the armory. You guys have enough, stop hogging it damnit!
+ - bugfix: On Tram, in the under tram, steam vents should no longer spontaneously
+ appear in walls.
+ PositiveEntropy:
+ - imageadd: Resprites the Captain's Antique Laser Gun!
+ ReinaCoder:
+ - bugfix: The QM now has a miniature e-gun in their locker on kilo like the other
+ heads!
+ tf-4:
+ - bugfix: Donksoft toy vendors no longer bluescreen.
+2022-06-11:
+ JohnFulpWillard:
+ - bugfix: Space Dragons' expiring no longer deletes the people they had already
+ eaten.
+ LemonInTheDark:
+ - rscadd: Weather effects will now be a bit more subtle in darkness, hopefully this
+ looks nicer
+ - imageadd: I've done some resprites to snow and non smoothing lava
+ MTandi:
+ - qol: Portable pump In/Out button text replaced with source and destination.
+ Maurukas:
+ - bugfix: A manually edited APC in the deepstorage ruin has been replaced with an
+ APC of the appropriate type.
+ Melbert:
+ - refactor: Gunlight / Helmetlight behavior is now a component.
+ - qol: Gunlight / Helmetlight now uses balloon alerts.
+ Mothblocks:
+ - spellcheck: Asimov++ no longer includes "In the case of conflict, the majority
+ order rules."
+ RandomGamer123:
+ - bugfix: Fixes the layer of a scrubber in Icebox engineering
+ Watermelon914:
+ - bugfix: Skip time button on the steal objective now has a fast forward icon.
+ YakumoChen:
+ - qol: Changed the position of light tubes in certain surgery rooms to not be directly
+ over a surgery bed. No more smashing lights with your saw by accident!
+ - qol: Aspiring AIs can now listen in on patient talk in Meta's primary surgery
+ room. Or you could listen to the soothing sounds of the the crew dying from
+ the comfort of a surgery bed when comms are down too, I guess.
+ msgerbs:
+ - bugfix: There is no longer a random pipe in the wall in Metastation's Xenobiology
+ department.
+ san7890:
+ - config: Hey, server operators! Title music playing at the lobby screen is now
+ DISABLED by default in the config settings (game_options.txt). If you are not
+ hearing any title music, be sure to adjust your config. If you're a player reading
+ this and are sorely missing out on those soulful tunes, please notify your server's
+ administration team of this change so they can diagnose it properly from there.
+ tf-4:
+ - balance: Pre-loaded PACMAN generators now have 15 plasma sheets, instead of 50.
+2022-06-12:
+ JohnFulpWillard:
+ - bugfix: The Warp whistle can be used more than once again.
+ Melbert:
+ - bugfix: Fixes Novaflowers not lighting targets on fire.
+ - code_imp: Reduces some copy+paste and cleans up some unique plant genes code.
+ - bugfix: Ranged attacks hitting mech equipment no longer ignores mech armor
+ - bugfix: Melee attacks now damage mech equipment
+ - bugfix: Mech equipment is now properly disabled at 0% health
+ Mothblocks:
+ - bugfix: Fixed thieves/opportunists icon.
+ Son-of-Space:
+ - bugfix: Several pairs of external airlocks without cycle link helpers have had
+ them added on TramStation
+ YakumoChen:
+ - bugfix: Fixed a symmetry issue with the lava clown puzzle where a lone chasm tile
+ wasn't connected with its fellow chasms.
+ san7890:
+ - bugfix: Chaplains, CMOs, and Psychologists can now all rejoice that they start
+ in their offices in Tram now, rather than take the shuttle.
+ - bugfix: Abductors should now hopefully spawn in on their little alien pod rather
+ than on the station's arrivals shuttle.
+ timothymtorres:
+ - rscadd: Add disease resistance to spaceacillin. It now gives 50% disease progression
+ slowdown, 75% to block disease infection, 75% to block zombie infection when
+ attacked, and 50% alien larva growth slowdown.
+2022-06-13:
+ Fikou:
+ - bugfix: floating now stops slips just as well as flying
+ - spellcheck: security mod theme no longer mentions being shockproof
+ - bugfix: fixes naked outfit giving people the ninja modsuit
+ JohnFulpWillard:
+ - bugfix: Ore silos can once again be synced to machines on the station on other
+ z levels, for multi-z maps.
+ MTandi:
+ - bugfix: Night shift lights now properly save power
+ - qol: APC buttons now have tooltips
+ Melbert:
+ - bugfix: Earthsblood makes you see colors again.
+ Mothblocks:
+ - bugfix: Fixed Dynamic midrounds spawning heavies significantly earlier than they're
+ supposed to
+ Rhials:
+ - bugfix: Jobs are no longer closed when the nuke detonates off-station.
+ Son-of-Space:
+ - bugfix: Fixed overlapping objects on a tile in the foyer of the IceBox bar.
+ - bugfix: Some manual varedits on objects with cargo shipping access were fixed
+ to work again.
+ - bugfix: A set of unpowered airlocks by the cooling loop on TramStation are now
+ powered.
+ - bugfix: A rack stuck in a wall on the listening station ruin was removed from
+ the wall.
+ cacogen:
+ - balance: The default pirate ship now has a cell charger to recharge suit cells
+ castawaynont:
+ - balance: The Space Ninja's MODsuit has a storage module now.
+ kawoppi:
+ - balance: removed JaniDrobe refill from the General Wardrobes Supply Crate and
+ moved it to its own supply crate
+ - balance: adjusted General Wardrobes Supply Crate price due to it containing one
+ less refill
+ robbertapir:
+ - bugfix: stundprods and teleprods now stun again
+ - bugfix: igniters now work again
+ vincentiusvin:
+ - bugfix: fix h2/trit fires being too hot, outputting funny numbers, generally being
+ weird.
+2022-06-14:
+ Son-of-Space:
+ - bugfix: A rogue windoor in a wall on Kilostation by the chapel in space was terminated
+ robbertapir:
+ - bugfix: Regal rats can now heal by eating all types of cheese, not just cheddar.
+2022-06-15:
+ Iamgoofball:
+ - spellcheck: Nanotrasen has lost the rights to several popular confectionaries,
+ and has created "original" replacements.
+ Mothblocks:
+ - spellcheck: Adjusted description of moths eating clothes in preferences menu to
+ better reflect their current behavior.
+ SuperNovaa41:
+ - bugfix: Pill names now have a cap at 42 characters to prevent chat spam.
+ Timberpoes:
+ - bugfix: Fixes slips being broken again.
+ - bugfix: Fixes a weird edge case where anything that would prevent a storage closet
+ or locker from opening would instead cause stealth implant boxes to delete the
+ player inside them.
+ UDaV73rus, Tokoriso, dragomagol:
+ - rscadd: welding now has an animation!
+ dragomagol:
+ - bugfix: Renamed a few circuit boards to explain what frame they need
+ private-tristan:
+ - bugfix: kilostation solars no longer have plating and catwalks on the same tile
+ robbertapir:
+ - bugfix: the flavor text for changing the transfer amount on the medical gel is
+ no longer backwards
+ san7890:
+ - bugfix: Chameleon neckties will no longer give you a missing-texture cape and
+ a big ERROR icon by default. Trust me when I tell you this wasn't actually a
+ good thing.
+ timothymtorres:
+ - rscadd: Add dyslexia (illiteracy quirk) as a genetic mutation.
+ - rscadd: Add illiteracy as a effect for confusion disease symptom.
+2022-06-16:
+ Fikou:
+ - bugfix: unrestricted wizard healing staff no longer tells you you are weak
+ Pickle-Coding:
+ - spellcheck: The chat will no longer lie by saying you shoved yourself into the
+ closet when someone shoves you into a closet.
+ PositiveEntropy, WJohnston, Dragomagol, LemonInTheDark, Riggle:
+ - imageadd: Resprites most variety of tiles into a better shaded version!
+ - code_imp: Damaged floors are now damaged overlays, meaning that most tiles should
+ properly display a damaged state!
+ ReinaCoder:
+ - imageadd: The riot helmet has been resprited!
+ SovietJenga:
+ - balance: Moves the botanogenetic shears into the botany research node and locks
+ it behind a botany experiment
+ - balance: Removes the advanced engineering node requirement from the botany node
+ Striders13:
+ - bugfix: fixed a windoor in metastation warden's office being layered incorrectly
+ YakumoChen:
+ - qol: Conveyor switches now tell you the speed setting of belts when examined.
+ You can change the speed with a multitool!
+ alphanerdd:
+ - bugfix: updates donk pocket box examine text to be more accurate
+ private-tristan:
+ - bugfix: icebox science has a scidrobe again!
+ timothymtorres:
+ - code_imp: Add atmospheric technician gas meter text tip
+2022-06-17:
+ 13spacemen:
+ - bugfix: Reagent dispensers will actually remove reagents upon leaking
+ AnturK:
+ - rscadd: Added fishing, fishing rods and other fishing equipment.
+ - rscadd: Fishing related cargo crates & private packs.
+ - rscadd: Fishing technology and designs
+ Fikou:
+ - qol: gps and ore bag modsuit modules are now usable when suit is off
+ Guillaume Prata:
+ - qol: APC construction/repairs/deconstruction uses balloon alerts now.
+ - bugfix: The circuit board of the Book Inventory Management Console now use the
+ correct name extension (Computer Board) instead of (Machine Board)
+ Iamgoofball:
+ - soundadd: Energy and Magic gunfire sound pitch now varies based on how much ammo
+ is left.
+ - soundadd: Ballistic gunfire now has a low-ammo click sound.
+ Melbert:
+ - bugfix: Items getting knocked off (glasses and cigars), bad omens, and spaghetti
+ falling from pockets should trigger on most knockdowns again like they used
+ to.
+ - code_imp: Cleaned up the knockoff component a bit. Also unit tests it.
+ - bugfix: Some heretic focuses apply more consistently now.
+ - bugfix: Furious Steel should go away more often than not now.
+ - bugfix: Fixes the fireman carry "body stuck to you forever" curse
+ ReinaCoder:
+ - imageadd: The chaplain's jumpsuit and skirt has been resprited.
+ SmArtKar:
+ - bugfix: Fixed locate weakpoint do_after being 3 seconds instead of intended 30.
+ Also you can no longer roll locate weakpoint until you get at least 20 minutes
+ of progression via objectives
+ SomethingFish:
+ - bugfix: Fixes mothic garlic pizza not producing the correct slice on slicing
+ Thunder12345:
+ - bugfix: The security office on Tramstation now has an air alarm
+ - bugfix: The water supply to Metastation's restrooms has been restored
+ - bugfix: Rocks will no longer grow into Tramstation's bar maintenance
+ - bugfix: A camera in IceBox's tech storage is now properly named
+ Twaticus, Wallemations:
+ - rscadd: '4 new emojis added to the joy mask! Check the AutoDrobe tweak: You can
+ now use internals with the joy/emotion mask'
+ atteria:
+ - imageadd: 3/4 sprites for MULEBots
+ bob-b-b:
+ - bugfix: fixed grav gen being overridable/overloadable
+ - bugfix: Fixes infiltrator toolbox not fitting all suit parts
+ distributivgesetz:
+ - rscadd: Added new moodlet for cascades, for more FLAVAH!
+ - bugfix: Rifts should now spawn at least 30 turfs from the nearest mass, and now
+ no longer stalemates.
+ - bugfix: Rift code is now no longer just copypasted supermatter bump code! Godmode
+ players, rejoice, you can actually leave the station now.
+ - bugfix: Made shuttle behaviour more predictable, escape shuttle can no longer
+ end the round prematurely during a cascade, it will stall out in hyperspace
+ instead.
+ - bugfix: Supermatter warp effect should be removed correctly now.
+ - bugfix: Fixed emergency lights not giving off red light.
+ - bugfix: Bluespace rifts pick a safe turf in CentCom dock now.
+ - spellcheck: Improves almost all messages that play during a cascade.
+ - admin: Better logging about resonance cascade-related actions.
+2022-06-18:
+ Melbert:
+ - balance: AI Gorillas are now allied to AI monkeys, and AI Cargorillas won't try
+ (and fail) to wail on crewmembers.
+ Pickle-Coding:
+ - bugfix: Fixes proto-nitrate tritium response requiring proto-nitrate bz response
+ radiation energy requirement amount of energy to emit radiation pulses, and
+ vice versa.
+ ReinaCoder:
+ - imageadd: The chef's hat, jumpsuit/skirt, and suit has been resprited
+ distributivgesetz:
+ - bugfix: Shuttle hijack timeouts work now.
+ jlsnow301 KubeRoot stylemistake Iamgoofball Kapu1178:
+ - rscadd: Adds the TGUI say modal to replace speech input boxes. Many features!
+ - rscadd: You can switch output channel within the TGUI say modal by clicking the
+ channel button or keying TAB.
+ - rscadd: Added 5-message chat history to the say window. Use arrow keys to scroll
+ recent messages.
+ - rscadd: The say window auto-expands! It adds up to two extra lines and scrolls
+ beyond that.
+ - rscadd: The common radio hotkey - "Y". No need to type ; in radio channel. Help
+ maint!
+ - rscadd: Radio subchannel helpers - Typing subchannels like ":e " will show labels.
+ - rscadd: Getting hurt while speaking has a chance to force you to speak. You read
+ that correc-AUGH
+ - rscadd: Typing indicators! Having an IC channel open will show thinking/typing
+ icons activity.
+ - rscadd: Typing indicators are a preference. Go incognito if you want!
+ - rscadd: Themes for TGUI Say! There is currently a light mode. More to come.
+ - refactor: Sorts other TGUI inputs into their own folder and cleans up their code.
+ Remember, TGUI Inputs can be disabled in UI prefs if you're not a fan.
+ - refactor: Refactors a lot of human defense code, report bugs if you encounter
+ them.
+ - refactor: Refactors admin centcom reports into typescript. You must send the report
+ to save the text.
+ theOOZ:
+ - imageadd: Adds an inhand sprite for the atmos gasmask
+ - imageadd: Updates the medical duffelbag inhand sprite
+2022-06-19:
+ ArcaneMusic:
+ - rscadd: A white cane, an indicator that the wielder is blind, can now be bought
+ and crafted from medical vendors and iron rods respectively.
+ FernandoJ8:
+ - bugfix: the randomize_human proc now gives non-humans a name that matches their
+ species
+ Guillaume Prata:
+ - qol: Airlock painter, extinguishers, geiger counter, holosign projectors, inducers
+ and welding tools use balloon alerts on some of their more spammy/error actions
+ and have some useless `to_chat` messages removed.
+ - qol: Health and Gene analyzers give a balloon alert on a successful scan.
+ JohnFulpWillard:
+ - bugfix: the Chaplain's sparring sect now works on Kilostation again.
+ - bugfix: Kilostation's Xenobiology is no longer roundstart on firelocks, and the
+ security medical post now has a scrubber.
+ Pickle-Coding:
+ - balance: Changes supermatter powerloss function. It will transition to linear
+ powerloss function at 5.88076GeV, and the linear powerloss function has been
+ offset so that the transition between the two functions is completely smooth.
+ - balance: Changes powerloss inhibition (both CO2 and psychologist effect) to change
+ the rate of powerloss instead of changing the rate of both functions then comparing
+ them to which one should be used.
+ - rscadd: Gas canisters and other portable atmospheric machinery can now be packaged
+ with wrapping paper.
+ - rscadd: Replaces chat messages with balloon alerts for package wrapping related
+ failures.
+ Toastgoats:
+ - qol: Meta and Tram have autodrobes in dorms now.
+ bob-b-b:
+ - balance: Increased greed vending machine integrity and max items
+2022-06-20:
+ ArcaneMusic:
+ - rscadd: Added several new goodie items to purchase through cargo, including translation
+ keys, mutadone autoinjectors, and a full RLD.
+ - code_imp: documented research_nodes and radios.
+ LemonInTheDark:
+ - code_imp: WAHHHH LIGHTING HAS CHANGED just backend mind. Please report any bugs
+ MTandi:
+ - bugfix: Fixed recycler bug that deleted the output stack because it was merged
+ with the input stack
+ - bugfix: Fixed recycler bug where it deleted stacks of 50 because it didn't have
+ enough space by removing recycle bins that reset its internal space from infinity
+ to the level of matter bin part
+ - qol: food now shows its type on examine
+ Melbert:
+ - qol: The Cursed Dufflebag now tells nearby people when it takes a bite out of
+ someone.
+ - balance: The Cursed Dufflebag takes any food poisoned by any means, instead of
+ just burnt messes.
+ - balance: The Cursed Dufflebag deals out slightly less wounds now to people wearing
+ armor and protection.
+ - balance: The Cursed Dufflebag deals less damage if the mob it's attached to is
+ dead, and doesn't heal if so.
+ - bugfix: The Cursed Dufflebag no longer permanently makes you a pacifist and clumsy.
+ - code_imp: Diggable component was changed to the Diggable element.
+ Mothblocks:
+ - bugfix: Fixed revolutions blaring alerts for a few seconds after winning.
+ - balance: Hacking the command console and winning revs no longer adds midround
+ threat. Instead, it'll force a heavy ruleset to spawn, and barring that, will
+ spawn a dangerous random event.
+ Paxilmaniac:
+ - qol: liquid plasma now points out in examine text that plasma can be collected
+ from it with a beaker
+ - bugfix: liquid plasma will now cool down gasses inside of heat exchanger pipes
+ to 100K instead of heat them to 5000K
+ SmArtKar:
+ - bugfix: Locate weakpoint objective now works again.
+ Watermelon914:
+ - admin: Added a way for administrators to globally disable circuit component sound
+ emitters.
+ - balance: Further limited the sound emitter component so that maximum volume is
+ reduced and pitch is capped between -50 and 50
+2022-06-21:
+ Cursor, sprites by Crumpaloo.:
+ - imageadd: The Cargorilla has found clothing in their size.
+ - rscadd: Updated the Cargorilla's description to match their new look.
+ GoldenAlpharex:
+ - bugfix: Fixed a runtime related to the TGUI white mode preference that would happen
+ every time someone would connect to the server.
+ JohnFulpWillard:
+ - bugfix: The Traitor eye snatching objective will now appear in-game.
+ MTandi:
+ - rscadd: 'Oldstation: Added a lootbox for every role'
+ - rscadd: 'Oldstation: Added 1 diamond ore spawn and 1 gibtonite spawn to asteroids'
+ - rscdel: 'Oldstation: Removed redundant cable, pen, free pipe dispensers and the
+ box of firing pins'
+ Pepsilawn:
+ - bugfix: The Lavaland Mafia map now displays safer flooring over empty space.
+ SmoSmoSmoSmok:
+ - bugfix: mulebots can be turned on/off
+ Thunder12345:
+ - bugfix: A camera in Meta's tech storage is now properly named
+2022-06-22:
+ Capsandi:
+ - bugfix: some walls in the crashed ship ruin, they are no longer full-bright
+ CoffeeDragon16:
+ - bugfix: switching bodies, such as becoming a lich or mindswapping will no longer
+ revoke a wizard's access to their spell book
+ Paxilmaniac:
+ - rscadd: Glass floor tiles can now be made out of both plasma glass, and reinforced
+ plasma glass
+ Pepsilawn:
+ - bugfix: Kilostation's Greater Port Maintenance has had a missing area patched
+ up.
+2022-06-23:
+ Cheshify:
+ - bugfix: The pride and gluttony ruins have had some minor fixes
+ MidoriWroth:
+ - rscadd: Botany can now grow olives, which can be ground into a paste and mixed
+ with water to make quality oil.
+ - rscadd: You can now make custom sushi by using an ingredient on a seaweed sheet.
+ The sushi will be named after the first ingredient you use.
+ - balance: Pierogis now need a dough slice instead of a bun
+ - balance: Quality oil costs 50 credits to order instead of 120
+ Mothblocks:
+ - bugfix: Fixes laser pointer circuits crashing clients.
+ san7890:
+ - rscadd: Nanotrasen has now installed an HFR Room and an Atmospherics Project Room
+ on IceBoxStation. The Atmospherics Storage Room has also had catwalks installed
+ in to accomodate these rooms uninstalled around it.
+ - rscadd: The Atmospherics Loop on IceBoxStation has undergone some minor modifications
+ to accomodate "feeding" these new rooms. Notably, there is now a "minor" loop
+ on the lower Z-Level that you can push and pull gases towards to your (probable)
+ heart's content.
+ - rscadd: The AI Satellite, Incinerator, and some sections of Maintenance have been
+ shuffled around on IceBoxStation to accomodate these changes.
+2022-06-24:
+ CoffeeDragon16:
+ - bugfix: getting your tongue removed will affect your speech again
+ Melbert:
+ - bugfix: Language encryption keys now only work when you've equipped the headset
+ - bugfix: Language encryption keys no longer forever-grant you the language in some
+ circumstances
+ - bugfix: Language encryption keys now update when they're installed if you're currently
+ wearing the headset
+ Tastyfish:
+ - bugfix: Opening wrapped crates now places them at the correct location.
+ antropod:
+ - bugfix: Randomized recipes (metalgen and secret sauce) are working again.
+ carshalash:
+ - bugfix: Jungle salad can now be eaten again
+ - bugfix: Eldritch nightmares were incorrectly added to the gold slime pool
+ dragomagol:
+ - qol: the cyborg hypospray now has a TGUI menu
+ theOOZ:
+ - qol: Rapid Lighting Device now fits in the utility toolbelt
+ timothymtorres:
+ - rscadd: Add magical reactions when hydroponics plants are hit with polymorph,
+ death, or resurrection magic. Plants with the anti-magic gene (holymelons) block
+ any kind of magical effect on the plant.
+2022-06-25:
+ ArcaneMusic:
+ - bugfix: White canes now examine objects properly and sound correct when hitting
+ things.
+ Ebb, epochayur, SweptWasTaken:
+ - bugfix: Soap and biopsy tools now have suit storage sprites.
+ ElGood:
+ - imageadd: Bluespace RPED has unique inhand sprites
+ Gandalf2k15:
+ - refactor: Security level code has been refactored, please report any abnormalities
+ to the github.
+ Hamcha:
+ - bugfix: the message monitor console can now send admin messages again
+ Kylerace:
+ - balance: 'the tram is now twice as fast, pray it doesnt get any faster (it cant
+ without raising world fps) performance: the tram is now about 10 times cheaper
+ to move for the server'
+ - rscadd: mappers can now create trams with multiple z levels
+ - code_imp: industrial_lift's now have more of their behavior pertaining to "the
+ entire lift" being handled by their lift_master_datum as opposed to belonging
+ to a random platform on the lift.
+ Melbert:
+ - rscdel: Removed a tip suggesting being a drunk scientists boosts research point
+ gain. This was removed at some point, but the tip remained, despite being incorrect.
+ Pepsilawn:
+ - bugfix: Coffins' base sell price adjusted back to 100 credits as previously intended.
+ Rhials:
+ - bugfix: Supermatter cascade final objective no longer generates when the engine
+ has exploded.
+ SmoSmoSmoSmok:
+ - rscadd: You can attach a bell to your wheelchair
+ Son-of-Space:
+ - balance: The AIs freeform and purge law boards have been returned as a static
+ staple on all maps.
+ jlsnow301:
+ - code_imp: Prettier is now recommended as an extension for UI development (or just
+ in general!)
+ - refactor: Many, many interfaces have been hit with the prettier stick so please
+ report any issues. There should be zero noticeable differences in how UIs look
+ or function.
+ private-tristan:
+ - spellcheck: silver golems text no longer states that they are immune to most magic
+ skylord-a52:
+ - refactor: Renamed "delimber" anomaly to "bioscrambler" anomaly
+2022-06-26:
+ Kylerace:
+ - bugfix: NanoTrasen has issued a ghost vaccine for the ghost virus that made ghosts
+ ghost deaf two days ago. now ghosts can hear again (as they are no longer ghost
+ deaf)
+ Profakos:
+ - spellcheck: fixes typos in ash lore
+ RandomGamer123:
+ - qol: Icebox atmos' shutters and airlocks are now transparent for better visibility
+ - bugfix: Fixed freon formation being nearly instant at most temperatures
+ Wallem:
+ - rscadd: Adds the Active Sonar Module, a module that allows the suit-wearer to
+ detect living organisms within a given radius.
+ castawaynont:
+ - bugfix: Allows Icebox's atmospherics APC to be accessible roundstart by moving
+ a console.
+ dragomagol:
+ - bugfix: advanced cyborg hypospray once again refills its chems
+ - qol: tochat for cyborg hypospray injections tells you which chem you injected
+ san7890:
+ - spellcheck: The exclamation point in the "Server Hop" verb has been deleted, which
+ now means you only need to type it in as 'server-hop'. Much nicer.
+2022-06-27:
+ 13spacemen:
+ - rscadd: Added the current map to the hub entry
+ - qol: Shutters on Metastation have directions now
+ CoffeeDragon16:
+ - bugfix: regenerative core implants will automatically revive you again
+ Hamcha:
+ - bugfix: The decryption key to the Nanotrasen message network has been delivered
+ to the Syndicate Listening Post
+ IndieanaJones:
+ - bugfix: Gorillas now change speed when holding something vs. not holding something,
+ as was always intended
+ - balance: Made gorillas use their old speed value when they're holding something,
+ and made them slightly faster when they're not holding something
+ - bugfix: People who gain or lose the monkified mutation no longer have invisible
+ equipped items.
+ MTandi:
+ - qol: 'UI: Added option to enter fullscreen mode to OOC verbs'
+ - qol: 'UI: Added option to hide the status/tooltip bar to OOC verbs'
+ - qol: 'UI: Made the chat input change colors according to the selected theme'
+ Mothblocks:
+ - balance: The amount of midround threat required for a midround roll has increased
+ slightly from 6.5 to 7.
+ - balance: Lowered the maximum threat level on sub-20 pop.
+ - balance: Lowered the number of roundstart traitors.
+ - server: Fixed "low_pop_minimum_threat" being incorrectly named. It has been changed
+ to "low_pop_maximum_threat".
+ Original code by SabreML:
+ - bugfix: Items in the suit storage slot won't turn invisible anymore
+ Paxilmaniac:
+ - code_imp: times for food items are now in X SECONDS format, functionality is still
+ the exact same
+ - qol: mining and labor shuttle home docks are no longer varedits of /stationary,
+ and are now their own subtype
+ Pickle-Coding:
+ - bugfix: Fix scrubbers not being able to be deconstructed when connected pipe disconnects.
+ RandomGamer123:
+ - bugfix: Fixes nuke op reinforcements having no HUD icon when seen by other nukeops,
+ nor being able to see the HUD icons of other nuke ops
+ - code_imp: Fixed a runtime if update_sight is called on a mob whose client has
+ no eye
+ private-tristan:
+ - balance: the meteor with engines strapped to it now has air in its rocks
+ san7890:
+ - bugfix: Nanotrasen hired a contracting company to completely re-do the Metastation
+ brig, but the commanding official was chumped- and was left with was a few decalling
+ changes and some re-piping. How ultimately odd.
+ - qol: Nanotrasen has used a new method of installing signs to their walls. If you
+ see any floating signs, or signs that appear to be positioned in incorrect locations,
+ please contact your nearest reality manager.
+2022-06-28:
+ 13spacemen, Gandalf2k15:
+ - rscadd: Examining and other chat outputs now display in blocks to make them easier
+ to see
+ - bugfix: AI and borgs no longer have their name show twice upon being examined
+ CoffeeDragon16:
+ - bugfix: plasma glass floor tiles will no longer sometimes be invisible
+ - bugfix: pianos will now look broken when they are damaged
+ - soundadd: pianos make a DONG sound when hit
+ Comxy:
+ - bugfix: Fixes martial arts auto reset bug.
+ GoldenAlpharex:
+ - bugfix: Snail people are no longer too shy to show their eyes, and will now expose
+ them out for everyone to see them, once more.
+ Melbert:
+ - bugfix: Frozen stuff is now properly actually recognized as frozen and won't re-freeze.
+ - refactor: Refactored how Knock (spell) tells stuff to open up, makes it easier
+ to add more knockable things.
+ Paxilmaniac:
+ - bugfix: fixes the ocean in the beach gateway having a different gasmix than the
+ sand, making the place unbreathable
+ Profakos:
+ - bugfix: Tramstation aux construction console works again as intended.
+ - bugfix: the hookshot bounty hunter, when summoned as an ert team member, is no
+ longer identical to the armoured bounty hunter.
+ - bugfix: bounty hunter IDs are no longer invisible save for the trim
+ RandomGamer123:
+ - spellcheck: Fixed a lack of line breaks in the operating computer when displaying
+ surgeries with required chemicals
+ Son-of-Space:
+ - rscadd: derelict1.dmm was overhauled into a derelict version of the Sulaco bridge
+ Y0SH1M4S73R:
+ - bugfix: The runic knight helmet's worn sprites have had their alignment fixed.
+ cinder1992:
+ - rscdel: Delta-pattern station quartermasters no longer have to listen to the sound
+ of a fire alarm every time their miners use the mining shuttle.
+ dragomagol:
+ - bugfix: Broken tiles (as seen in places like caravan ambush) now match our smooth
+ iron tiles
+ san7890:
+ - bugfix: The Chief Engineer's Keycard Authentication Device is no longer covered
+ by a electric sign on IceBoxStation.
+ - bugfix: Disposals will no longer now empty out in the middle of IceBoxStation's
+ maintenance tunnels.
+2022-06-29:
+ Guillaume Prata and Wallem:
+ - rscadd: Botany starts with watering cans instead of buckets now. There is also
+ a research locked advanced watering can which generates it's own water.
+ JohnFulpWillard:
+ - bugfix: the Gravity Generator no longer plays several looping sounds.
+ - bugfix: Oldstation's gravity generator now properly spawns off.
+ Melbert:
+ - bugfix: Service members can access the service dorms on Deltastation again
+ - bugfix: Miners can access the shipping office on Deltastation again
+ RandomGamer123:
+ - qol: Makes the noblium gas shells experiment explicitly clear that it requires
+ Hypernoblium gas and not just "Noblium"
+ - spellcheck: CentCom is now properly capitalised in the description of an experiment
+ Salex08:
+ - spellcheck: The Brand of Dance description is correct now
+ - bugfix: headpikes can be made again.
+ Sebbe9123:
+ - rscadd: Adds a special Medical ERT belt + gives it to medical ERT
+ Y0SH1M4S73R:
+ - bugfix: Fixed a runtime that occurs when inflicting a blunt wound on an armless
+ human.
+ jlsnow301:
+ - bugfix: TGUI Say now retains current messages / keying through history doesn't
+ erase your current message.
+ - bugfix: TextArea keydown prop has been renamed so as to not trigger double keydown
+ events.
+ - bugfix: TGUI say now properly expands when viewing recent messages
+ san7890:
+ - rscadd: Ice Box Station's got a new look. Notably, the cold room is on the bottom
+ floor with the kitchen all being up on the upper Z-Level
+2022-06-30:
+ 13spacemen:
+ - imageadd: Shutters and window shutters on all stations are now directional
+ Melbert:
+ - bugfix: Comms console hacking will be interrupted if the comms console itself
+ is destroyed or depowered.
+ - bugfix: Cult sacrificing someone with a soul will no longer give you an empty
+ soulstone when it should give you a filled one.
+ - bugfix: Using a soulstone on a soulless corpse will now properly poll ghosts and
+ allow a new shade.
+ - bugfix: You can no longer capture spirits with soulstones you can't even release
+ spirits from
+ - bugfix: You can no longer hypothetically capture yourself with a soulstone
+ - code_imp: Some cleanup on aisle soulstone.
+ - code_imp: The blood walk element has been changed from a bespoke element to a
+ component.
+ - code_imp: MULEs now use the blood walk component.
+ - bugfix: MULEbots no longer track blood forever.
+ Profakos:
+ - imageadd: Rootbread slice toppings have a sprite now.
+ RandomGamer123:
+ - bugfix: Removes a method that allows xenobiologists to produce slimes of every
+ colour and bypassing the intended progression sequence by making the disease
+ induced by advanced mutation toxin only produce grey slimes unless the infected
+ person is a sentient non-monkey human
+ ReinaCoder:
+ - imageadd: The Ghoulbot now has a new sprite matching the new 3/4 mulebot sprites
+ - imageadd: The Makarov pistol has been resprited
+ Salex08:
+ - qol: Added a new tab for the autolathe which allows you to dispense not only iron
+ or glass but all the mats you put in
+ - bugfix: Fixes missing "=" operator in autolathe
+ nichlas0010:
+ - bugfix: The Orion Trail's Realism Mode works again, triggering negative events
+ for the player
diff --git a/html/changelogs/archive/2022-07.yml b/html/changelogs/archive/2022-07.yml
new file mode 100644
index 0000000000000..b12a2ac6845fd
--- /dev/null
+++ b/html/changelogs/archive/2022-07.yml
@@ -0,0 +1,121 @@
+2022-07-01:
+ Guillaume Prata:
+ - qol: Hypernob Protection (given to plasmamen that are breathing Hypernob as gas)
+ is a status effect now and gives better feedback to the user.
+ - balance: Hypernob Protection gives a small slowdown.
+ Hamcha:
+ - bugfix: The agent card now correctly allows you to take anyone's identity, even
+ if their name contains numbers
+ OrionTheFox:
+ - bugfix: Chainsaws can now actually cut wood! So can the giant axe that is the
+ PKC, and other chainsaw items - but, alas, esword handles alone can not, and
+ you must actually turn them on. Also, all mining implements can mine scattered
+ stones as well!
+ - bugfix: Chainsaws are now faster when they are on, and slower when they aren't.
+ Salex08:
+ - bugfix: blacklists placeholder food items in the random food generator which caused
+ error sprites appearing
+ ShizCalev:
+ - qol: Gas filtering cloth are now smaller and can fit inside boxes!
+ chesse20:
+ - rscadd: Furry Pride Spraypaint added to spray cans
+ - rscdel: Y##f In H##l Sprapaint removed from spray cans
+ jlsnow301:
+ - code_imp: Refactored a large number of TGUI interfaces into TypeScript. If something's
+ broken, report it!
+ private-tristan:
+ - bugfix: metastation library no longer has 2 air alarms
+2022-07-02:
+ Guillaume Prata:
+ - qol: Clown/Mime survival boxes won't have a mostly pointless breathing mask, as
+ their basic mask can be used for internals.
+ - imageadd: New mime Hugbox!
+ Melbert:
+ - rscadd: The Mjolnir wizard loadout no longer scams you. It now also grants 1 level
+ of Tesla Blast!
+ - bugfix: Spells show up in the stat panel again
+ - bugfix: Fixes runtime errors that can occur if a wizard spellbook randomizes into
+ unpurchasable spells.
+ - bugfix: Genetics powers now have their own background, which apparently they have
+ always meant to have.
+ - bugfix: Fixes some cases where chaplains are unable to cast their own spells.
+ - bugfix: Fixes some granters being infinite when they should've been limited (Maint
+ loot books).
+ - bugfix: Wizard's roundend report is now more accurate in what spells they purchased.
+ - qol: Some more spells and actions now use balloon alerts.
+ - qol: Many actions will now more accurately have their icon redded out if you can't
+ use them at the moment.
+ - qol: Genetics mindread is now a pointed ability.
+ - qol: The mimery book now grants you a vow of silence action, like the guide to
+ advanced mimery.
+ - qol: Xenomorph's corrosive acid ability is now a click-to-work action, so you
+ click on what you want to acid.
+ - qol: Also, xenomorph larva now use a radial when choosing an evolution type.
+ - qol: Summon Item now takes a few seconds to remove the mark from an item, so you
+ no longer accidentally remove it.
+ - balance: The mindswap book now gives a slight message warning it's changed on
+ read.
+ - balance: Sacred Flame now lets people around them know they're actually being
+ made super flammable.
+ - balance: Olfaction can't be cast if you have no nose (you're headless).
+ - balance: Being Emotemute now prevents you from invoking spells which require emotes
+ (mime spells).
+ - balance: All wizard spells except Mindswap can't be cast if the wizard is just
+ a brain. Most spells didn't work, anyways.
+ - balance: PAIs and carded AIs can no longer cast spells. Most spells didn't work,
+ anyways.
+ - balance: You can't mindswap into PAIs and brains(?).
+ - admin: Admins can now add spells by name or typepath.
+ - admin: Spell requirements are bitflags now, so you can modify them with the bit-field
+ VV ui. Pretty neat.
+ - code_imp: proc_holders have been removed from existence.
+ - refactor: Wizard, Heretic, Revenant, and Genetics spells are now all action datums.
+ - refactor: Xenomorph, Spider, and Cytology monster abilities are now all action
+ datums.
+ - refactor: Some malf AI and cult abilities are now fully action datums instead
+ of an unholy mix of proc holder and action.
+ - refactor: Refactored how cult's blood target is set.
+ - refactor: Bloodcrawling and jaunting have been completely refactored. Blood crawling
+ has been exorcised from the living level of mobs, and the consumption part of
+ (s)laughter demons is now handled by the action itself.
+ - refactor: The wizard's teleport scroll now actually holds and casts a version
+ of the "teleport" spell, instead of fake casting it.
+ - refactor: Refactored the wizard spellbook a bit.
+ - refactor: Refactored how all item actions are applied and handled.
+ - rscdel: SDQL spells have been removed.
+ Meyhazah:
+ - imageadd: new sprites for the Chaplain's crusader, ancient, profane and follower
+ outfits.
+ NotZang:
+ - qol: Consistency with the other bot's examination text.
+ Salex08:
+ - admin: Admins can now cancel midround random events
+ TheBoondock:
+ - bugfix: fixed telekinesis teleporting gun shenanigan
+ jlsnow301:
+ - bugfix: Fixed a bug that made the hacking status display not appear
+ lizardqueenlexi:
+ - bugfix: Limb growing code now uses species_color instead of mutant_color for synthetic
+ limb coloration
+ necromanceranne:
+ - imageadd: Improves the sprites for bullets, thermal projectiles, and introduces
+ pellet sprites.
+ orthography:
+ - bugfix: fixes intercoms being the incorrect state.
+ san7890:
+ - bugfix: Chefs on IceBoxStation will now start with eggs and milk again.
+2022-07-03:
+ Pepsilawn:
+ - bugfix: Kilostation's Supermatter was made more consistent, it will no longer
+ start with its pumps lacking pressure target checking.
+ Y0SH1M4S73R:
+ - bugfix: Maids in the Mirror are unaffected by the gaze of ghosts
+ chesse20:
+ - rscadd: Furry Pride large spraypaint added to spraycans
+ san7890:
+ - qol: There is now a new preference in Game Preferences, Suppress All Ghost Rolls.
+ If you tick this preference, you will not get a singular window pop-up whenever
+ a Ghost Role is available. Intended for the few who really do need it.
+ - admin: Admins get another additional preference where Suppress All Ghost Roles
+ only works while they are currently in an adminned state. They will still get
+ ghost rolls normally when they are in a deadminned state.
diff --git a/html/statbrowser.js b/html/statbrowser.js
index d024d50b8c3d1..74687b1939382 100644
--- a/html/statbrowser.js
+++ b/html/statbrowser.js
@@ -813,8 +813,8 @@ Byond.subscribeTo('update_spells', function (payload) {
do_update = true;
}
init_spells();
- if (payload.verblist) {
- spells = payload.verblist;
+ if (payload.actions) {
+ spells = payload.actions;
if (do_update) {
draw_spells(current_tab);
}
diff --git a/icons/area/areas_station.dmi b/icons/area/areas_station.dmi
index 68d1990530294..38ce98d85bfcb 100644
Binary files a/icons/area/areas_station.dmi and b/icons/area/areas_station.dmi differ
diff --git a/icons/effects/96x32.dmi b/icons/effects/96x32.dmi
index 937b2e8d4245b..a78d11530b945 100644
Binary files a/icons/effects/96x32.dmi and b/icons/effects/96x32.dmi differ
diff --git a/icons/effects/beam.dmi b/icons/effects/beam.dmi
index 7ff9807d9e534..b050a6dfd590c 100644
Binary files a/icons/effects/beam.dmi and b/icons/effects/beam.dmi differ
diff --git a/icons/effects/effects.dmi b/icons/effects/effects.dmi
index 8513e3672ac95..8beb91e6b3adf 100644
Binary files a/icons/effects/effects.dmi and b/icons/effects/effects.dmi differ
diff --git a/icons/effects/glow_weather.dmi b/icons/effects/glow_weather.dmi
new file mode 100644
index 0000000000000..021da02fe37be
Binary files /dev/null and b/icons/effects/glow_weather.dmi differ
diff --git a/icons/effects/landmarks_static.dmi b/icons/effects/landmarks_static.dmi
index 2082afc2fe025..d6a26b050d59a 100644
Binary files a/icons/effects/landmarks_static.dmi and b/icons/effects/landmarks_static.dmi differ
diff --git a/icons/effects/random_spawners.dmi b/icons/effects/random_spawners.dmi
index a9a83ec0e6395..2eb6a40538b2a 100644
Binary files a/icons/effects/random_spawners.dmi and b/icons/effects/random_spawners.dmi differ
diff --git a/icons/effects/weather_effects.dmi b/icons/effects/weather_effects.dmi
index 0149245d943e7..00083c464a24f 100644
Binary files a/icons/effects/weather_effects.dmi and b/icons/effects/weather_effects.dmi differ
diff --git a/icons/effects/welding_effect.dmi b/icons/effects/welding_effect.dmi
new file mode 100644
index 0000000000000..8b8db2dcaeaca
Binary files /dev/null and b/icons/effects/welding_effect.dmi differ
diff --git a/icons/hud/radial.dmi b/icons/hud/radial.dmi
index 6ee4ece36bd0e..5d18f96d05e11 100644
Binary files a/icons/hud/radial.dmi and b/icons/hud/radial.dmi differ
diff --git a/icons/hud/screen_alert.dmi b/icons/hud/screen_alert.dmi
index 8ba79a5ff8518..e880e02a81970 100755
Binary files a/icons/hud/screen_alert.dmi and b/icons/hud/screen_alert.dmi differ
diff --git a/icons/mob/aibots.dmi b/icons/mob/aibots.dmi
index 7b9570845129d..0e4ddd1b98456 100644
Binary files a/icons/mob/aibots.dmi and b/icons/mob/aibots.dmi differ
diff --git a/icons/mob/cargorillia.dmi b/icons/mob/cargorillia.dmi
new file mode 100644
index 0000000000000..4e9ac41718560
Binary files /dev/null and b/icons/mob/cargorillia.dmi differ
diff --git a/icons/mob/clothing/belt_mirror.dmi b/icons/mob/clothing/belt_mirror.dmi
index d83172551233c..3f53b2a11ce30 100644
Binary files a/icons/mob/clothing/belt_mirror.dmi and b/icons/mob/clothing/belt_mirror.dmi differ
diff --git a/icons/mob/clothing/head.dmi b/icons/mob/clothing/head.dmi
index 9694491fd09a3..286b57577c4e8 100644
Binary files a/icons/mob/clothing/head.dmi and b/icons/mob/clothing/head.dmi differ
diff --git a/icons/mob/clothing/mask.dmi b/icons/mob/clothing/mask.dmi
index 8b8c8e55a0cae..8460d944db2fe 100644
Binary files a/icons/mob/clothing/mask.dmi and b/icons/mob/clothing/mask.dmi differ
diff --git a/icons/mob/clothing/suit.dmi b/icons/mob/clothing/suit.dmi
index fedf63c7f5935..dda93df1188ca 100644
Binary files a/icons/mob/clothing/suit.dmi and b/icons/mob/clothing/suit.dmi differ
diff --git a/icons/mob/clothing/under/civilian.dmi b/icons/mob/clothing/under/civilian.dmi
index aaabf5dbb4025..a29453c72e3d9 100644
Binary files a/icons/mob/clothing/under/civilian.dmi and b/icons/mob/clothing/under/civilian.dmi differ
diff --git a/icons/mob/clothing/under/costume.dmi b/icons/mob/clothing/under/costume.dmi
index aed7249eb4645..28c14f5943c9b 100644
Binary files a/icons/mob/clothing/under/costume.dmi and b/icons/mob/clothing/under/costume.dmi differ
diff --git a/icons/mob/clothing/under/suits.dmi b/icons/mob/clothing/under/suits.dmi
index 0a8043c1e8a5f..debd01601ecdc 100644
Binary files a/icons/mob/clothing/under/suits.dmi and b/icons/mob/clothing/under/suits.dmi differ
diff --git a/icons/mob/huds/hud.dmi b/icons/mob/huds/hud.dmi
index c945e96055f7e..807d4bf649c1f 100644
Binary files a/icons/mob/huds/hud.dmi and b/icons/mob/huds/hud.dmi differ
diff --git a/icons/mob/human_face.dmi b/icons/mob/human_face.dmi
index 12d6652bef4ae..e2a913e0da3c8 100644
Binary files a/icons/mob/human_face.dmi and b/icons/mob/human_face.dmi differ
diff --git a/icons/mob/inhands/clothing_lefthand.dmi b/icons/mob/inhands/clothing_lefthand.dmi
index 5ad9ec35b9468..b2f9556468391 100644
Binary files a/icons/mob/inhands/clothing_lefthand.dmi and b/icons/mob/inhands/clothing_lefthand.dmi differ
diff --git a/icons/mob/inhands/clothing_righthand.dmi b/icons/mob/inhands/clothing_righthand.dmi
index bd298e3d7e73f..55fb568bd7f53 100644
Binary files a/icons/mob/inhands/clothing_righthand.dmi and b/icons/mob/inhands/clothing_righthand.dmi differ
diff --git a/icons/mob/inhands/equipment/backpack_lefthand.dmi b/icons/mob/inhands/equipment/backpack_lefthand.dmi
index 999fe1fe5ecd8..76301c2bdb192 100644
Binary files a/icons/mob/inhands/equipment/backpack_lefthand.dmi and b/icons/mob/inhands/equipment/backpack_lefthand.dmi differ
diff --git a/icons/mob/inhands/equipment/backpack_righthand.dmi b/icons/mob/inhands/equipment/backpack_righthand.dmi
index c71178e3f43f7..f060b83eb907a 100644
Binary files a/icons/mob/inhands/equipment/backpack_righthand.dmi and b/icons/mob/inhands/equipment/backpack_righthand.dmi differ
diff --git a/icons/mob/inhands/equipment/fishing_rod_lefthand.dmi b/icons/mob/inhands/equipment/fishing_rod_lefthand.dmi
new file mode 100644
index 0000000000000..846c36522cc10
Binary files /dev/null and b/icons/mob/inhands/equipment/fishing_rod_lefthand.dmi differ
diff --git a/icons/mob/inhands/equipment/fishing_rod_righthand.dmi b/icons/mob/inhands/equipment/fishing_rod_righthand.dmi
new file mode 100644
index 0000000000000..fdc2e770c998b
Binary files /dev/null and b/icons/mob/inhands/equipment/fishing_rod_righthand.dmi differ
diff --git a/icons/mob/inhands/equipment/hydroponics_lefthand.dmi b/icons/mob/inhands/equipment/hydroponics_lefthand.dmi
index f2dafa9bd60c2..031909dbf8240 100644
Binary files a/icons/mob/inhands/equipment/hydroponics_lefthand.dmi and b/icons/mob/inhands/equipment/hydroponics_lefthand.dmi differ
diff --git a/icons/mob/inhands/equipment/hydroponics_righthand.dmi b/icons/mob/inhands/equipment/hydroponics_righthand.dmi
index 0006212fc58c8..a2cac9c47f307 100644
Binary files a/icons/mob/inhands/equipment/hydroponics_righthand.dmi and b/icons/mob/inhands/equipment/hydroponics_righthand.dmi differ
diff --git a/icons/mob/inhands/misc/devices_lefthand.dmi b/icons/mob/inhands/misc/devices_lefthand.dmi
index 55456a6e07363..bd1f1fcfdc026 100644
Binary files a/icons/mob/inhands/misc/devices_lefthand.dmi and b/icons/mob/inhands/misc/devices_lefthand.dmi differ
diff --git a/icons/mob/inhands/misc/devices_righthand.dmi b/icons/mob/inhands/misc/devices_righthand.dmi
index a274e001ebab0..81a21c2356d31 100644
Binary files a/icons/mob/inhands/misc/devices_righthand.dmi and b/icons/mob/inhands/misc/devices_righthand.dmi differ
diff --git a/icons/mob/inhands/weapons/melee_lefthand.dmi b/icons/mob/inhands/weapons/melee_lefthand.dmi
index fe8c2e7a7e3ba..7c112d6cb3a1b 100644
Binary files a/icons/mob/inhands/weapons/melee_lefthand.dmi and b/icons/mob/inhands/weapons/melee_lefthand.dmi differ
diff --git a/icons/mob/inhands/weapons/melee_righthand.dmi b/icons/mob/inhands/weapons/melee_righthand.dmi
index c6614031c7db8..f2ad28a0c0b7a 100644
Binary files a/icons/mob/inhands/weapons/melee_righthand.dmi and b/icons/mob/inhands/weapons/melee_righthand.dmi differ
diff --git a/icons/mob/pai.dmi b/icons/mob/pai.dmi
index 5832f575b6ec4..0d582a7d9fa87 100644
Binary files a/icons/mob/pai.dmi and b/icons/mob/pai.dmi differ
diff --git a/icons/mob/pai_item_head.dmi b/icons/mob/pai_item_head.dmi
index 337e22254ea85..0a04e7e8ab29e 100644
Binary files a/icons/mob/pai_item_head.dmi and b/icons/mob/pai_item_head.dmi differ
diff --git a/icons/mob/pai_item_lh.dmi b/icons/mob/pai_item_lh.dmi
index fb9c77f5abae5..ffc85a476d45a 100644
Binary files a/icons/mob/pai_item_lh.dmi and b/icons/mob/pai_item_lh.dmi differ
diff --git a/icons/mob/pai_item_rh.dmi b/icons/mob/pai_item_rh.dmi
index ced27446a451e..7c575ae81f605 100644
Binary files a/icons/mob/pai_item_rh.dmi and b/icons/mob/pai_item_rh.dmi differ
diff --git a/icons/mob/simple_human.dmi b/icons/mob/simple_human.dmi
index d654434b55b15..cdb2275ee2438 100644
Binary files a/icons/mob/simple_human.dmi and b/icons/mob/simple_human.dmi differ
diff --git a/icons/mob/talk.dmi b/icons/mob/talk.dmi
index ad2c50287b681..6b2fc395cba0d 100644
Binary files a/icons/mob/talk.dmi and b/icons/mob/talk.dmi differ
diff --git a/icons/obj/assemblies/new_assemblies.dmi b/icons/obj/assemblies/new_assemblies.dmi
index fc4ff9813ddd4..de031383c752d 100644
Binary files a/icons/obj/assemblies/new_assemblies.dmi and b/icons/obj/assemblies/new_assemblies.dmi differ
diff --git a/icons/obj/brokentiling.dmi b/icons/obj/brokentiling.dmi
index 0c14789b4bf22..853c6fbd35c63 100644
Binary files a/icons/obj/brokentiling.dmi and b/icons/obj/brokentiling.dmi differ
diff --git a/icons/obj/card.dmi b/icons/obj/card.dmi
index 5953cfb58de98..3a55571566b65 100644
Binary files a/icons/obj/card.dmi and b/icons/obj/card.dmi differ
diff --git a/icons/obj/clothing/hats.dmi b/icons/obj/clothing/hats.dmi
index e839d4f4ea978..2cebd2870e343 100644
Binary files a/icons/obj/clothing/hats.dmi and b/icons/obj/clothing/hats.dmi differ
diff --git a/icons/obj/clothing/masks.dmi b/icons/obj/clothing/masks.dmi
index 85e79bb0902ef..0688b2ad6a291 100644
Binary files a/icons/obj/clothing/masks.dmi and b/icons/obj/clothing/masks.dmi differ
diff --git a/icons/obj/clothing/modsuit/mod_modules.dmi b/icons/obj/clothing/modsuit/mod_modules.dmi
index 0e0121b33d909..69affa3fa4994 100644
Binary files a/icons/obj/clothing/modsuit/mod_modules.dmi and b/icons/obj/clothing/modsuit/mod_modules.dmi differ
diff --git a/icons/obj/clothing/suits.dmi b/icons/obj/clothing/suits.dmi
index cc24947bb4de0..07be1e15f269e 100644
Binary files a/icons/obj/clothing/suits.dmi and b/icons/obj/clothing/suits.dmi differ
diff --git a/icons/obj/clothing/under/civilian.dmi b/icons/obj/clothing/under/civilian.dmi
index 6e5b2a68a3b54..33f4e83727553 100644
Binary files a/icons/obj/clothing/under/civilian.dmi and b/icons/obj/clothing/under/civilian.dmi differ
diff --git a/icons/obj/clothing/under/costume.dmi b/icons/obj/clothing/under/costume.dmi
index 5542ea920b913..e9554278c862d 100644
Binary files a/icons/obj/clothing/under/costume.dmi and b/icons/obj/clothing/under/costume.dmi differ
diff --git a/icons/obj/clothing/under/suits.dmi b/icons/obj/clothing/under/suits.dmi
index d1e2c5c1de927..1641c207836f7 100644
Binary files a/icons/obj/clothing/under/suits.dmi and b/icons/obj/clothing/under/suits.dmi differ
diff --git a/icons/obj/crates.dmi b/icons/obj/crates.dmi
index cfc869f98cbbd..208af28f97cf2 100644
Binary files a/icons/obj/crates.dmi and b/icons/obj/crates.dmi differ
diff --git a/icons/obj/doors/shutters_window.dmi b/icons/obj/doors/shutters_window.dmi
index 550e01f987821..fcf8af5b94fd7 100644
Binary files a/icons/obj/doors/shutters_window.dmi and b/icons/obj/doors/shutters_window.dmi differ
diff --git a/icons/obj/drinks.dmi b/icons/obj/drinks.dmi
index 8348293dcefce..fb1ee1d956df1 100644
Binary files a/icons/obj/drinks.dmi and b/icons/obj/drinks.dmi differ
diff --git a/icons/obj/fishing.dmi b/icons/obj/fishing.dmi
new file mode 100644
index 0000000000000..13b2409ed28ff
Binary files /dev/null and b/icons/obj/fishing.dmi differ
diff --git a/icons/obj/food/burgerbread.dmi b/icons/obj/food/burgerbread.dmi
index bcee4de90f2a5..63cd4263012fa 100644
Binary files a/icons/obj/food/burgerbread.dmi and b/icons/obj/food/burgerbread.dmi differ
diff --git a/icons/obj/food/food.dmi b/icons/obj/food/food.dmi
index 9015ef2100c23..aa089ac5c3507 100644
Binary files a/icons/obj/food/food.dmi and b/icons/obj/food/food.dmi differ
diff --git a/icons/obj/food/frozen_treats.dmi b/icons/obj/food/frozen_treats.dmi
index 6d1f501ffa706..133d6de83a03b 100644
Binary files a/icons/obj/food/frozen_treats.dmi and b/icons/obj/food/frozen_treats.dmi differ
diff --git a/icons/obj/food/lizard.dmi b/icons/obj/food/lizard.dmi
index 25ea72ec4a262..89b23761e4bfe 100644
Binary files a/icons/obj/food/lizard.dmi and b/icons/obj/food/lizard.dmi differ
diff --git a/icons/obj/food/pizzaspaghetti.dmi b/icons/obj/food/pizzaspaghetti.dmi
index a5deb9eb97fb0..e8d88cdbf9c57 100644
Binary files a/icons/obj/food/pizzaspaghetti.dmi and b/icons/obj/food/pizzaspaghetti.dmi differ
diff --git a/icons/obj/guns/ballistic.dmi b/icons/obj/guns/ballistic.dmi
index c8c4e585eff68..1c04b37476219 100644
Binary files a/icons/obj/guns/ballistic.dmi and b/icons/obj/guns/ballistic.dmi differ
diff --git a/icons/obj/guns/energy.dmi b/icons/obj/guns/energy.dmi
index 3d46b6c84e56b..3ec60e3f5e101 100644
Binary files a/icons/obj/guns/energy.dmi and b/icons/obj/guns/energy.dmi differ
diff --git a/icons/obj/guns/projectiles.dmi b/icons/obj/guns/projectiles.dmi
index 432517ebcb61a..de24bcb935b39 100644
Binary files a/icons/obj/guns/projectiles.dmi and b/icons/obj/guns/projectiles.dmi differ
diff --git a/icons/obj/hydroponics/equipment.dmi b/icons/obj/hydroponics/equipment.dmi
index 08d86965b5b8d..2f7b511cdefe0 100644
Binary files a/icons/obj/hydroponics/equipment.dmi and b/icons/obj/hydroponics/equipment.dmi differ
diff --git a/icons/obj/hydroponics/growing_fruits.dmi b/icons/obj/hydroponics/growing_fruits.dmi
index 2ad7a5a5f7db0..a71acabc2aecc 100644
Binary files a/icons/obj/hydroponics/growing_fruits.dmi and b/icons/obj/hydroponics/growing_fruits.dmi differ
diff --git a/icons/obj/hydroponics/harvest.dmi b/icons/obj/hydroponics/harvest.dmi
index c38b928302c1c..a3b738f9d5e0a 100644
Binary files a/icons/obj/hydroponics/harvest.dmi and b/icons/obj/hydroponics/harvest.dmi differ
diff --git a/icons/obj/hydroponics/seeds.dmi b/icons/obj/hydroponics/seeds.dmi
index 7948e2ab89fed..d2b2e4c76f617 100644
Binary files a/icons/obj/hydroponics/seeds.dmi and b/icons/obj/hydroponics/seeds.dmi differ
diff --git a/icons/obj/items_and_weapons.dmi b/icons/obj/items_and_weapons.dmi
index 5c74b867d0394..9209e07422d42 100644
Binary files a/icons/obj/items_and_weapons.dmi and b/icons/obj/items_and_weapons.dmi differ
diff --git a/icons/obj/plumbing/connects.dmi b/icons/obj/plumbing/connects.dmi
index 32277ffac6597..943b580a624d8 100644
Binary files a/icons/obj/plumbing/connects.dmi and b/icons/obj/plumbing/connects.dmi differ
diff --git a/icons/obj/plumbing/fluid_ducts.dmi b/icons/obj/plumbing/fluid_ducts.dmi
index d911e25b9c926..f28a4c35853e0 100644
Binary files a/icons/obj/plumbing/fluid_ducts.dmi and b/icons/obj/plumbing/fluid_ducts.dmi differ
diff --git a/icons/obj/plushes.dmi b/icons/obj/plushes.dmi
index dfc7e5af8b394..772b27d1d8e1a 100644
Binary files a/icons/obj/plushes.dmi and b/icons/obj/plushes.dmi differ
diff --git a/icons/obj/radio.dmi b/icons/obj/radio.dmi
index 4588d215aed74..37b810f332f34 100644
Binary files a/icons/obj/radio.dmi and b/icons/obj/radio.dmi differ
diff --git a/icons/obj/storage.dmi b/icons/obj/storage.dmi
index 229bd9177d472..2b5b3d5f054b6 100644
Binary files a/icons/obj/storage.dmi and b/icons/obj/storage.dmi differ
diff --git a/icons/obj/tiles.dmi b/icons/obj/tiles.dmi
index cf7b64830c4cb..305f3fd884b5f 100644
Binary files a/icons/obj/tiles.dmi and b/icons/obj/tiles.dmi differ
diff --git a/icons/obj/vehicles.dmi b/icons/obj/vehicles.dmi
index 9056106ea6788..7c0b84b886978 100644
Binary files a/icons/obj/vehicles.dmi and b/icons/obj/vehicles.dmi differ
diff --git a/icons/turf/damaged.dmi b/icons/turf/damaged.dmi
new file mode 100644
index 0000000000000..175de493e455d
Binary files /dev/null and b/icons/turf/damaged.dmi differ
diff --git a/icons/turf/floors.dmi b/icons/turf/floors.dmi
index 1230a2fbba22f..21cb138253643 100644
Binary files a/icons/turf/floors.dmi and b/icons/turf/floors.dmi differ
diff --git a/icons/turf/floors/bamboo_mat.dmi b/icons/turf/floors/bamboo_mat.dmi
index ec4bcf1c0de59..ab915f341e2da 100644
Binary files a/icons/turf/floors/bamboo_mat.dmi and b/icons/turf/floors/bamboo_mat.dmi differ
diff --git a/icons/turf/floors/carpet.dmi b/icons/turf/floors/carpet.dmi
index 9040e45da56c2..1af03a1df8234 100644
Binary files a/icons/turf/floors/carpet.dmi and b/icons/turf/floors/carpet.dmi differ
diff --git a/icons/turf/floors/carpet_black.dmi b/icons/turf/floors/carpet_black.dmi
index cb4dff5354330..e2fea9085b230 100644
Binary files a/icons/turf/floors/carpet_black.dmi and b/icons/turf/floors/carpet_black.dmi differ
diff --git a/icons/turf/floors/carpet_blue.dmi b/icons/turf/floors/carpet_blue.dmi
index ea04cffc6ba4a..b909b11f359d9 100644
Binary files a/icons/turf/floors/carpet_blue.dmi and b/icons/turf/floors/carpet_blue.dmi differ
diff --git a/icons/turf/floors/carpet_cyan.dmi b/icons/turf/floors/carpet_cyan.dmi
index 1dc9400f78b96..85e053a5a6de4 100644
Binary files a/icons/turf/floors/carpet_cyan.dmi and b/icons/turf/floors/carpet_cyan.dmi differ
diff --git a/icons/turf/floors/carpet_green.dmi b/icons/turf/floors/carpet_green.dmi
index 182b1af39c403..39ef048087481 100644
Binary files a/icons/turf/floors/carpet_green.dmi and b/icons/turf/floors/carpet_green.dmi differ
diff --git a/icons/turf/floors/carpet_orange.dmi b/icons/turf/floors/carpet_orange.dmi
index d1a20dfac5ce5..4f0b2078fca38 100644
Binary files a/icons/turf/floors/carpet_orange.dmi and b/icons/turf/floors/carpet_orange.dmi differ
diff --git a/icons/turf/floors/carpet_purple.dmi b/icons/turf/floors/carpet_purple.dmi
index bfe03c195a981..f07ce75d33406 100644
Binary files a/icons/turf/floors/carpet_purple.dmi and b/icons/turf/floors/carpet_purple.dmi differ
diff --git a/icons/turf/floors/carpet_red.dmi b/icons/turf/floors/carpet_red.dmi
index 934d541e8fddc..eb0527f430dc4 100644
Binary files a/icons/turf/floors/carpet_red.dmi and b/icons/turf/floors/carpet_red.dmi differ
diff --git a/icons/turf/floors/carpet_royalblack.dmi b/icons/turf/floors/carpet_royalblack.dmi
index 323f1806f3615..0b848ac1afa44 100644
Binary files a/icons/turf/floors/carpet_royalblack.dmi and b/icons/turf/floors/carpet_royalblack.dmi differ
diff --git a/icons/turf/floors/carpet_royalblue.dmi b/icons/turf/floors/carpet_royalblue.dmi
index 95f1b6400deeb..1027b0e19f58e 100644
Binary files a/icons/turf/floors/carpet_royalblue.dmi and b/icons/turf/floors/carpet_royalblue.dmi differ
diff --git a/icons/turf/floors/glass.dmi b/icons/turf/floors/glass.dmi
index e28bb13e233a0..ad833bddcbd06 100644
Binary files a/icons/turf/floors/glass.dmi and b/icons/turf/floors/glass.dmi differ
diff --git a/icons/turf/floors/plasma_glass.dmi b/icons/turf/floors/plasma_glass.dmi
new file mode 100644
index 0000000000000..0417819c6fbed
Binary files /dev/null and b/icons/turf/floors/plasma_glass.dmi differ
diff --git a/icons/turf/floors/reinf_glass.dmi b/icons/turf/floors/reinf_glass.dmi
index 97614f510f745..4f53e8129c1c2 100644
Binary files a/icons/turf/floors/reinf_glass.dmi and b/icons/turf/floors/reinf_glass.dmi differ
diff --git a/icons/turf/floors/reinf_plasma_glass.dmi b/icons/turf/floors/reinf_plasma_glass.dmi
new file mode 100644
index 0000000000000..c622a909e31b9
Binary files /dev/null and b/icons/turf/floors/reinf_plasma_glass.dmi differ
diff --git a/icons/ui_icons/fishing/default.png b/icons/ui_icons/fishing/default.png
new file mode 100644
index 0000000000000..f21074ac2dde8
Binary files /dev/null and b/icons/ui_icons/fishing/default.png differ
diff --git a/icons/ui_icons/fishing/lavaland.png b/icons/ui_icons/fishing/lavaland.png
new file mode 100644
index 0000000000000..6c97f66432e94
Binary files /dev/null and b/icons/ui_icons/fishing/lavaland.png differ
diff --git a/interface/skin.dmf b/interface/skin.dmf
index 19ba4cc3b2b67..ae43a3c401bb9 100644
--- a/interface/skin.dmf
+++ b/interface/skin.dmf
@@ -221,14 +221,13 @@ window "outputwindow"
size = 517x20
anchor1 = 0,100
anchor2 = 100,100
- background-color = #d3b5b5
is-default = true
- border = sunken
+ border = line
saved-params = "command"
elem "oocbutton"
type = BUTTON
pos = 599,460
- size = 40x19
+ size = 40x20
anchor1 = 100,100
anchor2 = -1,-1
background-color = none
@@ -241,7 +240,7 @@ window "outputwindow"
elem "saybutton"
type = BUTTON
pos = 519,460
- size = 40x19
+ size = 40x20
anchor1 = 100,100
anchor2 = -1,-1
background-color = none
@@ -254,7 +253,7 @@ window "outputwindow"
elem "mebutton"
type = BUTTON
pos = 559,460
- size = 40x19
+ size = 40x20
anchor1 = 100,100
anchor2 = -1,-1
background-color = none
@@ -270,7 +269,6 @@ window "outputwindow"
size = 640x456
anchor1 = 0,0
anchor2 = 100,100
- background-color = #ffffff
is-visible = false
is-disabled = true
saved-params = ""
@@ -339,3 +337,22 @@ window "statwindow"
is-visible = false
saved-params = ""
+window "tgui_say"
+ elem "tgui_say"
+ type = MAIN
+ pos = 848,500
+ size = 231x30
+ anchor1 = 50,50
+ anchor2 = 50,50
+ is-visible = false
+ saved-params = ""
+ statusbar = false
+ can-minimize = false
+ elem "browser"
+ type = BROWSER
+ pos = 0,0
+ size = 231x30
+ anchor1 = 0,0
+ anchor2 = 0,0
+ saved-params = ""
+
diff --git a/sound/effects/bigsplash.ogg b/sound/effects/bigsplash.ogg
new file mode 100644
index 0000000000000..772e8c5b260eb
Binary files /dev/null and b/sound/effects/bigsplash.ogg differ
diff --git a/sound/effects/fish_splash.ogg b/sound/effects/fish_splash.ogg
new file mode 100644
index 0000000000000..4c5a68bab79ab
Binary files /dev/null and b/sound/effects/fish_splash.ogg differ
diff --git a/sound/effects/piano_hit.ogg b/sound/effects/piano_hit.ogg
new file mode 100644
index 0000000000000..fbf74c155312e
Binary files /dev/null and b/sound/effects/piano_hit.ogg differ
diff --git a/sound/effects/ping_hit.ogg b/sound/effects/ping_hit.ogg
new file mode 100644
index 0000000000000..729bd7efa887b
Binary files /dev/null and b/sound/effects/ping_hit.ogg differ
diff --git a/sound/effects/splash.ogg b/sound/effects/splash.ogg
new file mode 100644
index 0000000000000..a02121ae02fb9
Binary files /dev/null and b/sound/effects/splash.ogg differ
diff --git a/sound/weapons/gun/general/ballistic_click.ogg b/sound/weapons/gun/general/ballistic_click.ogg
new file mode 100644
index 0000000000000..67175d851eddb
Binary files /dev/null and b/sound/weapons/gun/general/ballistic_click.ogg differ
diff --git a/strings/memories.json b/strings/memories.json
index 52c54ebece191..f373e005d1795 100644
--- a/strings/memories.json
+++ b/strings/memories.json
@@ -414,7 +414,7 @@
],
"nuke_code_names":[
"%PROTAGONIST learns the detonation codes for a nuclear weapon, %NUKE_CODE."
- ],
+ ],
"nuke_code_starts":[
"The number %NUKE_CODE written on a sticky note with the words \"FOR SYNDICATE EYES ONLY\" scrawled next to it.",
"A piece of paper with the number %NUKE_CODE being handed to %PROTAGONIST from a figure in a blood-red MODsuit."
@@ -442,5 +442,16 @@
"playing_52_pickup_moods":[
"%PROTAGONIST %MOOD as they taunt %DEUTERAGONIST.",
"%DEUTERAGONIST %MOOD as they shamefully pickup the cards."
+ ],
+ "russian_roulette_names":[
+ "%PROTAGONIST playing a game of russian roulette."
+ ],
+ "russian_roulette_starts":[
+ "%PROTAGONIST aiming at their %BODYPART right before they pull the trigger.",
+ "The revolver has %LOADED_ROUNDS rounds loaded in the chamber.",
+ "%PROTAGONIST is gambling their life as they spin the revolver."
+ ],
+ "russian_roulette_moods":[
+ "%PROTAGONIST %MOOD as they %OUTCOME the deadly game of roulette."
]
}
diff --git a/strings/sillytips.txt b/strings/sillytips.txt
index de52969f235f8..aeb7b9b27aef5 100644
--- a/strings/sillytips.txt
+++ b/strings/sillytips.txt
@@ -14,7 +14,7 @@ The wizard is supposed to be extremely strong in one on one combat, stop getting
Sometimes a round will just be a bust. C'est la vie.
This is a game that is constantly being developed for. Expect things to be added, removed, fixed, and broken on a daily basis.
It's fun to try and predict the round type from the tip of the round message.
-The quartermaster is not a head of staff and will never be one.
+The quartermaster is only a head of staff so their feelings aren’t hurt. Don’t tell them.
The bird remembers.
Your sprite represents your hitbox, so that afro makes you easier to kill. The sacrifices we make for style.
Sometimes admins will just do stuff. Roll with it.
diff --git a/strings/tips.txt b/strings/tips.txt
index 21696a74b2304..667df8aa01dda 100644
--- a/strings/tips.txt
+++ b/strings/tips.txt
@@ -1,41 +1,3 @@
-Where the space map levels connect is randomized every round, but are otherwise kept consistent within rounds. Remember that they are not necessarily bidirectional!
-You can catch thrown items by toggling on your throw mode with an empty hand active.
-To crack the safe in the vault, use a stethoscope or three plastic explosives on it.
-You can climb onto a table by dragging yourself onto one. This takes time and drops the items in your hands on the table. Clicking on a table that someone else is climbing onto will knock them down.
-You can drag other players onto yourself to open the strip menu, letting you remove their equipment or force them to wear something. Note that exosuits or helmets will block your access to the clothing beneath them, and that certain items take longer to strip or put on than others.
-Clicking on a windoor rather then bumping into it will keep it open, you can click it again to close it.
-You can spray a fire extinguisher, throw items or fire a gun while floating through space to change your direction. Simply fire opposite to where you want to go.
-You can change the control scheme by pressing tab. One is WASD, the other is the arrow keys. Keep in mind that hotkeys are also changed with this.
-Firesuits and winter coats offer mild protection from the cold, allowing you to spend longer periods of time near breaches and space than if wearing nothing at all.
-Glass shards can be welded to make glass, and iron rods can be welded to make iron. Ores can be welded too, but this takes a lot of fuel.
-If you need to drag multiple people either to safety or to space, bring a locker or crate over and stuff them all in before hauling them off.
-You can grab someone by holding Ctrl and clicking on them, then upgrade the grab by Ctrl-clicking on them once more. An aggressive grab will momentarily stun someone, allow you to place them on a table by clicking on it, or throw them by toggling on throwing.
-Holding alt and left clicking a tile will allow you to see its contents in the top right window pane, which is much faster than right clicking.
-The resist button will allow you to resist out of handcuffs, being buckled to a chair or bed, out of locked lockers and more. Whenever you're stuck, try resisting!
-You can move an item out of the way by dragging it and then clicking on an adjacent tile with an empty hand.
-You can recolor certain items like jumpsuits and gloves in washing machines by also throwing in a crayon.
-Maintenance is full of equipment that is randomized every round. Look around and see if anything is worth using.
-Some roles cannot be antagonists by default, but antag selection is decided first. For instance, you can set Security Officer to High without affecting your chances of becoming an antag -- the game will just select a different role.
-There are many places around the station to hide contraband. A few for starters: linen boxes, toilet cisterns, body bags. Experiment to find more!
-You can use a machine in the vault to deposit cash or rob Cargo's department funds.
-When in doubt about technicial issues, clear your cache (byond launcher > cogwheel > preferences > game prefs), update your BYOND, and relog.
-Most things have special interactions with right, alt, shift, and control click. Experiment!
-You can screwdriver any non-chemical grenade to shorten fuses from 5 seconds, to 3 seconds, to instant boom! Nobody ever expects this.
-If you find yourself in a fistfight with another player, staying on the offensive is usually the smart move. Running away often won't accomplish much.
-Different weapons have different strengths. Some weapons, such as spears, floor tiles, and throwing stars, deal more damage when thrown compared to when attacked normally.
-A thrown glass of water can make a slippery tile, allowing you to slow down your pursuers in a pinch.
-When dealing with security, you can often get your sentence negated entirely through cooperation and deception.
-The P2P chat function found on tablet computers allows for a stealthy way to communicate with people.
-Experiment with different setups of the supermatter engine to maximize output, but don't risk the crew's safety to do so!
-We were all new once, be patient and guide new players in the right direction.
-On most clothing items that go in the exosuit slot, you can put certain small items into your suit storage, such as a spraycan, your emergency oxygen tank, or a flashlight.
-Most job-related exosuit clothing can fit job-related items into it, such as the atmospheric technician's winter coat holding an RPD, or labcoats holding most medicine.
-If you're using hotkey mode, you can stop pulling things using H.
-The station's self-destruct terminal is invincible. Go find the disk instead of trying to destroy it.
-If there's something you need from another department, try asking! This game isn't singleplayer and you'd be surprised what you can get accomplished together!
-You'll quickly lose your interest in the game if you play to win and kill. If you find yourself doing this, take a step back and talk to people - it's a much better experience!
-Felinids get temporarily distracted by laser pointers. Use this to your advantage when being pursued by one.
-Don't be afraid to ask for help, whether from your peers or from admins.
As the Captain, you are one of the highest priority targets on the station. Everything from revolutions, to nuclear operatives, to traitors that need to rob you of your unique lasgun or your life are things to worry about.
As the Captain, always take the nuclear disk and pinpointer with you every shift. It's a good idea to give one of these to another head you can trust with keeping it safe.
As the Captain, you have absolute access and control over the station, but this does not mean that being a horrible person won't result in mutiny and a ban.
@@ -72,7 +34,6 @@ As a Scientist, you can maximize the number of uses you get out of a slime by fe
As a Scientist, you can disable anomalies by scanning them with an analyzer, then send a signal on the frequency it gives you with a remote signaling device, or if researched, hit the anomaly an anomaly analyzer. This will leave behind an anomaly core, which can be used to construct a Phazon mech, reactive armors, or various unique and powerful weapons!
As a Scientist, researchable stock parts can seriously improve the efficiency and speed of machines around the station. In some cases, it can even unlock new functions.
As a Scientist, you can generate money for Science, and complete experiments by letting the tachyon-doppler array record increasingly large explosions.
-As a Scientist, getting drunk just enough will speed up research. Skol!
As a Roboticist, keep an ear out for anomaly announcements. If you get your hands on a bluespace anomaly core, you can build a Phazon mech!
As a Roboticist, you can repair your cyborgs with a welding tool. If they have taken burn damage, you can remove their battery, expose the wiring with a screwdriver and replace their wires with a cable coil.
As a Roboticist, you can reset a cyborg's model by cutting and mending the reset wire with a wire cutter.
@@ -100,7 +61,6 @@ As an Engineer, you can temporarily power the station solely with the solar arra
As an Engineer, you can cool the supermatter crystal by spraying it with a fire extinguisher. Only for the brave!
As an Engineer, you can repair windows by using a welding tool on them while not in combat mode.
As an Engineer, you can lock APCs, fire alarms, emitters and radiation collectors using your ID card to prevent others from disabling them.
-As an Engineer, don't underestimate the humble P.A.C.M.A.N. generators. With upgraded parts, a couple units working in tandem are sufficient to take over for an exploded engine or shattered solars.
As an Engineer, your departmental protolathe and circuit printer can manufacture the necessary circuit boards and components to build just about anything. Make extra medical machinery everywhere! Build a gibber for security! Set up an array of emitters pointing down the hall! The possibilities are endless!
As an Engineer, you can pry open secure storage blast doors by disabling the engine room APC's main breaker. This is obviously a bad idea if the engine is running.
As an Engineer, your RCD can be reloaded with iron, glass or plasteel sheets instead of just compressed matter cartridges.
@@ -112,6 +72,7 @@ As an Atmospheric Technician, your backpack firefighter tank can launch resin. T
As an Atmospheric Technician, your ATMOS holofan projector blocks gases while allowing objects to pass through. With it, you can quickly contain gas spills, fires and hull breaches.
As an Atmospheric Technician, burning a plasma/oxygen mix inside the incinerator will not only produce power, but also gases such as tritium, water vapor and carbon dioxide.
As an Atmospheric Technician, you can change the layer of a pipe by clicking with it on a wrenched pipe or other atmos component of the desired layer.
+As an Atmospheric Technician, pay attention to gas pipe meters. The meter will change color in response to temperature and the bar will increase and get thicker in response to higher pressures.
As the Head of Security, you are expected to coordinate your security force to handle any threat that comes to the station. Sometimes it means making use of the armory to handle a blob, sometimes it means being ruthless during a revolution or cult.
As the Head of Security, you can call for executions or forced cyborgization, but may require the Captain's approval.
As the Head of Security, don't let the power go to your head. You may have high access, great equipment, and a miniature army at your side, but being a terrible person without a good reason is grounds for banning.
@@ -253,3 +214,43 @@ Laying down will help slow down bloodloss. Death will halt it entirely.
If you knock into somebody while doing a wicked grind on a skateboard, they will be floored for double the time. Radical!
Sleeping can be used to recover from minor injuries. Sanity, darkness, blindfolds, earmuffs, tables, beds, and bedsheets affect the healing rate.
You can cheat games by baking dice in microwaves to make them loaded. Cards can be seen with x-ray vision or be marked with either a pen or crayon to gain an edge.
+Where the space map levels connect is randomized every round, but are otherwise kept consistent within rounds. Remember that they are not necessarily bidirectional!
+You can catch thrown items by toggling on your throw mode with an empty hand active.
+To crack the safe in the vault, use a stethoscope or three plastic explosives on it.
+You can climb onto a table by dragging yourself onto one. This takes time and drops the items in your hands on the table. Clicking on a table that someone else is climbing onto will knock them down.
+You can drag other players onto yourself to open the strip menu, letting you remove their equipment or force them to wear something. Note that exosuits or helmets will block your access to the clothing beneath them, and that certain items take longer to strip or put on than others.
+Clicking on a windoor rather then bumping into it will keep it open, you can click it again to close it.
+You can spray a fire extinguisher, throw items or fire a gun while floating through space to change your direction. Simply fire opposite to where you want to go.
+You can change the control scheme by pressing tab. One is WASD, the other is the arrow keys. Keep in mind that hotkeys are also changed with this.
+Firesuits and winter coats offer mild protection from the cold, allowing you to spend longer periods of time near breaches and space than if wearing nothing at all.
+Glass shards can be welded to make glass, and iron rods can be welded to make iron. Ores can be welded too, but this takes a lot of fuel.
+If you need to drag multiple people either to safety or to space, bring a locker or crate over and stuff them all in before hauling them off.
+You can grab someone by holding Ctrl and clicking on them, then upgrade the grab by Ctrl-clicking on them once more. An aggressive grab will momentarily stun someone, allow you to place them on a table by clicking on it, or throw them by toggling on throwing.
+Holding alt and left clicking a tile will allow you to see its contents in the top right window pane, which is much faster than right clicking.
+The resist button will allow you to resist out of handcuffs, being buckled to a chair or bed, out of locked lockers and more. Whenever you're stuck, try resisting!
+You can move an item out of the way by dragging it and then clicking on an adjacent tile with an empty hand.
+You can recolor certain items like jumpsuits and gloves in washing machines by also throwing in a crayon.
+Maintenance is full of equipment that is randomized every round. Look around and see if anything is worth using.
+Some roles cannot be antagonists by default, but antag selection is decided first. For instance, you can set Security Officer to High without affecting your chances of becoming an antag -- the game will just select a different role.
+There are many places around the station to hide contraband. A few for starters: linen boxes, toilet cisterns, body bags. Experiment to find more!
+You can use a machine in the vault to deposit cash or rob Cargo's department funds.
+When in doubt about technicial issues, clear your cache (byond launcher > cogwheel > preferences > game prefs), update your BYOND, and relog.
+Most things have special interactions with right, alt, shift, and control click. Experiment!
+You can screwdriver any non-chemical grenade to shorten fuses from 5 seconds, to 3 seconds, to instant boom! Nobody ever expects this.
+If you find yourself in a fistfight with another player, staying on the offensive is usually the smart move. Running away often won't accomplish much.
+Different weapons have different strengths. Some weapons, such as spears, floor tiles, and throwing stars, deal more damage when thrown compared to when attacked normally.
+A thrown glass of water can make a slippery tile, allowing you to slow down your pursuers in a pinch.
+When dealing with security, you can often get your sentence negated entirely through cooperation and deception.
+The P2P chat function found on tablet computers allows for a stealthy way to communicate with people.
+Experiment with different setups of the supermatter engine to maximize output, but don't risk the crew's safety to do so!
+We were all new once, be patient and guide new players in the right direction.
+On most clothing items that go in the exosuit slot, you can put certain small items into your suit storage, such as a spraycan, your emergency oxygen tank, or a flashlight.
+Most job-related exosuit clothing can fit job-related items into it, such as the atmospheric technician's winter coat holding an RPD, or labcoats holding most medicine.
+If you're using hotkey mode, you can stop pulling things using H.
+The station's self-destruct terminal is invincible. Go find the disk instead of trying to destroy it.
+If there's something you need from another department, try asking! This game isn't singleplayer and you'd be surprised what you can get accomplished together!
+You'll quickly lose your interest in the game if you play to win and kill. If you find yourself doing this, take a step back and talk to people - it's a much better experience!
+Felinids get temporarily distracted by laser pointers. Use this to your advantage when being pursued by one.
+Don't be afraid to ask for help, whether from your peers or from admins.
+You can pin your MODsuit module buttons to your action bar in the suit UI.
+Some weapons are better at taking down robots and structures than others. Don't try to break a window with a scalpel, try a toolbox.
diff --git a/tgstation.dme b/tgstation.dme
index 5201d3fa005b7..8debb4e730cbe 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -77,6 +77,7 @@
#include "code\__DEFINES\external_organs.dm"
#include "code\__DEFINES\fantasy_affixes.dm"
#include "code\__DEFINES\firealarm.dm"
+#include "code\__DEFINES\fishing.dm"
#include "code\__DEFINES\flags.dm"
#include "code\__DEFINES\flora.dm"
#include "code\__DEFINES\fonts.dm"
@@ -89,6 +90,7 @@
#include "code\__DEFINES\icon_smoothing.dm"
#include "code\__DEFINES\id_cards.dm"
#include "code\__DEFINES\important_recursive_contents.dm"
+#include "code\__DEFINES\industrial_lift.dm"
#include "code\__DEFINES\injection.dm"
#include "code\__DEFINES\instruments.dm"
#include "code\__DEFINES\interaction_flags.dm"
@@ -163,6 +165,7 @@
#include "code\__DEFINES\span.dm"
#include "code\__DEFINES\spatial_gridmap.dm"
#include "code\__DEFINES\species_clothing_paths.dm"
+#include "code\__DEFINES\speech_channels.dm"
#include "code\__DEFINES\speech_controller.dm"
#include "code\__DEFINES\stat.dm"
#include "code\__DEFINES\stat_tracking.dm"
@@ -204,6 +207,8 @@
#include "code\__DEFINES\dcs\signals\signals_admin.dm"
#include "code\__DEFINES\dcs\signals\signals_adventure.dm"
#include "code\__DEFINES\dcs\signals\signals_area.dm"
+#include "code\__DEFINES\dcs\signals\signals_assembly.dm"
+#include "code\__DEFINES\dcs\signals\signals_beam.dm"
#include "code\__DEFINES\dcs\signals\signals_bot.dm"
#include "code\__DEFINES\dcs\signals\signals_changeling.dm"
#include "code\__DEFINES\dcs\signals\signals_circuit.dm"
@@ -243,6 +248,7 @@
#include "code\__DEFINES\dcs\signals\signals_screentips.dm"
#include "code\__DEFINES\dcs\signals\signals_spatial_grid.dm"
#include "code\__DEFINES\dcs\signals\signals_specie.dm"
+#include "code\__DEFINES\dcs\signals\signals_spell.dm"
#include "code\__DEFINES\dcs\signals\signals_storage.dm"
#include "code\__DEFINES\dcs\signals\signals_subsystem.dm"
#include "code\__DEFINES\dcs\signals\signals_swab.dm"
@@ -264,7 +270,6 @@
#include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_movable.dm"
#include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_movement.dm"
#include "code\__DEFINES\dcs\signals\signals_atom\signals_atom_x_act.dm"
-#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_abilities.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_arcade.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_carbon.dm"
#include "code\__DEFINES\dcs\signals\signals_mob\signals_mob_living.dm"
@@ -304,6 +309,7 @@
#include "code\__HELPERS\icons.dm"
#include "code\__HELPERS\jatum.dm"
#include "code\__HELPERS\level_traits.dm"
+#include "code\__HELPERS\levels.dm"
#include "code\__HELPERS\lighting.dm"
#include "code\__HELPERS\maths.dm"
#include "code\__HELPERS\matrices.dm"
@@ -565,7 +571,6 @@
#include "code\controllers\subsystem\processing\supermatter_cascade.dm"
#include "code\controllers\subsystem\processing\tramprocess.dm"
#include "code\controllers\subsystem\processing\wet_floors.dm"
-#include "code\datums\action.dm"
#include "code\datums\alarm.dm"
#include "code\datums\armor.dm"
#include "code\datums\beam.dm"
@@ -620,15 +625,32 @@
#include "code\datums\achievements\misc_achievements.dm"
#include "code\datums\achievements\misc_scores.dm"
#include "code\datums\achievements\skill_achievements.dm"
-#include "code\datums\actions\beam_rifle.dm"
+#include "code\datums\actions\action.dm"
+#include "code\datums\actions\cooldown_action.dm"
+#include "code\datums\actions\innate_action.dm"
+#include "code\datums\actions\item_action.dm"
+#include "code\datums\actions\items\adjust.dm"
+#include "code\datums\actions\items\beam_rifle.dm"
+#include "code\datums\actions\items\beserk.dm"
+#include "code\datums\actions\items\boot_dash.dm"
+#include "code\datums\actions\items\cult_dagger.dm"
+#include "code\datums\actions\items\hands_free.dm"
+#include "code\datums\actions\items\organ_action.dm"
+#include "code\datums\actions\items\set_internals.dm"
+#include "code\datums\actions\items\stealth_box.dm"
+#include "code\datums\actions\items\summon_stickmen.dm"
+#include "code\datums\actions\items\toggles.dm"
+#include "code\datums\actions\items\vortex_recall.dm"
#include "code\datums\actions\mobs\blood_warp.dm"
#include "code\datums\actions\mobs\charge.dm"
#include "code\datums\actions\mobs\dash.dm"
#include "code\datums\actions\mobs\fire_breath.dm"
+#include "code\datums\actions\mobs\language_menu.dm"
#include "code\datums\actions\mobs\lava_swoop.dm"
#include "code\datums\actions\mobs\meteors.dm"
#include "code\datums\actions\mobs\mobcooldown.dm"
#include "code\datums\actions\mobs\projectileattack.dm"
+#include "code\datums\actions\mobs\small_sprite.dm"
#include "code\datums\actions\mobs\transform_weapon.dm"
#include "code\datums\ai\_ai_behavior.dm"
#include "code\datums\ai\_ai_controller.dm"
@@ -714,6 +736,7 @@
#include "code\datums\components\aura_healing.dm"
#include "code\datums\components\bakeable.dm"
#include "code\datums\components\beetlejuice.dm"
+#include "code\datums\components\blood_walk.dm"
#include "code\datums\components\bloodysoles.dm"
#include "code\datums\components\boomerang.dm"
#include "code\datums\components\butchering.dm"
@@ -738,7 +761,6 @@
#include "code\datums\components\deadchat_control.dm"
#include "code\datums\components\dejavu.dm"
#include "code\datums\components\deployable.dm"
-#include "code\datums\components\diggable.dm"
#include "code\datums\components\drift.dm"
#include "code\datums\components\earprotection.dm"
#include "code\datums\components\edit_complainer.dm"
@@ -749,6 +771,7 @@
#include "code\datums\components\engraved.dm"
#include "code\datums\components\explodable.dm"
#include "code\datums\components\faction_granter.dm"
+#include "code\datums\components\fishing_spot.dm"
#include "code\datums\components\force_move.dm"
#include "code\datums\components\fov_handler.dm"
#include "code\datums\components\fullauto.dm"
@@ -799,6 +822,7 @@
#include "code\datums\components\rot.dm"
#include "code\datums\components\rotation.dm"
#include "code\datums\components\scope.dm"
+#include "code\datums\components\seclight_attachable.dm"
#include "code\datums\components\shell.dm"
#include "code\datums\components\shielded.dm"
#include "code\datums\components\shrink.dm"
@@ -950,7 +974,6 @@
#include "code\datums\elements\basic_body_temp_sensitive.dm"
#include "code\datums\elements\beauty.dm"
#include "code\datums\elements\bed_tucking.dm"
-#include "code\datums\elements\blood_walk.dm"
#include "code\datums\elements\bsa_blocker.dm"
#include "code\datums\elements\bump_click.dm"
#include "code\datums\elements\chemical_transfer.dm"
@@ -966,6 +989,7 @@
#include "code\datums\elements\death_drops.dm"
#include "code\datums\elements\delete_on_drop.dm"
#include "code\datums\elements\deliver_first.dm"
+#include "code\datums\elements\diggable.dm"
#include "code\datums\elements\digitalcamo.dm"
#include "code\datums\elements\drag_pickup.dm"
#include "code\datums\elements\dryable.dm"
@@ -977,6 +1001,7 @@
#include "code\datums\elements\firestacker.dm"
#include "code\datums\elements\footstep.dm"
#include "code\datums\elements\forced_gravity.dm"
+#include "code\datums\elements\frozen.dm"
#include "code\datums\elements\haunted.dm"
#include "code\datums\elements\honkspam.dm"
#include "code\datums\elements\item_fov.dm"
@@ -984,6 +1009,7 @@
#include "code\datums\elements\kneecapping.dm"
#include "code\datums\elements\kneejerk.dm"
#include "code\datums\elements\knockback.dm"
+#include "code\datums\elements\lazy_fishing_spot.dm"
#include "code\datums\elements\lifesteal.dm"
#include "code\datums\elements\light_blocking.dm"
#include "code\datums\elements\light_eaten.dm"
@@ -1105,19 +1131,25 @@
#include "code\datums\mood_events\needs_events.dm"
#include "code\datums\mutations\_combined.dm"
#include "code\datums\mutations\_mutations.dm"
-#include "code\datums\mutations\actions.dm"
#include "code\datums\mutations\adaptation.dm"
#include "code\datums\mutations\antenna.dm"
+#include "code\datums\mutations\autotomy.dm"
#include "code\datums\mutations\body.dm"
#include "code\datums\mutations\chameleon.dm"
#include "code\datums\mutations\cold.dm"
+#include "code\datums\mutations\fire_breath.dm"
#include "code\datums\mutations\hulk.dm"
+#include "code\datums\mutations\olfaction.dm"
#include "code\datums\mutations\passive.dm"
#include "code\datums\mutations\radioactive.dm"
#include "code\datums\mutations\sight.dm"
#include "code\datums\mutations\speech.dm"
#include "code\datums\mutations\telekinesis.dm"
+#include "code\datums\mutations\telepathy.dm"
+#include "code\datums\mutations\tongue_spike.dm"
#include "code\datums\mutations\touch.dm"
+#include "code\datums\mutations\void_magnet.dm"
+#include "code\datums\mutations\webbing.dm"
#include "code\datums\mutations\holy_mutation\burdened.dm"
#include "code\datums\mutations\holy_mutation\honorbound.dm"
#include "code\datums\proximity_monitor\field.dm"
@@ -1242,6 +1274,7 @@
#include "code\game\gamemodes\dynamic\dynamic_rulesets_midround.dm"
#include "code\game\gamemodes\dynamic\dynamic_rulesets_roundstart.dm"
#include "code\game\gamemodes\dynamic\dynamic_simulations.dm"
+#include "code\game\gamemodes\dynamic\dynamic_unfavorable_situation.dm"
#include "code\game\gamemodes\dynamic\ruleset_picking.dm"
#include "code\game\machinery\_machinery.dm"
#include "code\game\machinery\ai_slipper.dm"
@@ -1254,7 +1287,6 @@
#include "code\game\machinery\cell_charger.dm"
#include "code\game\machinery\civilian_bounties.dm"
#include "code\game\machinery\constructable_frame.dm"
-#include "code\game\machinery\crossing_signal.dm"
#include "code\game\machinery\dance_machine.dm"
#include "code\game\machinery\defibrillator_mount.dm"
#include "code\game\machinery\deployable.dm"
@@ -1334,7 +1366,6 @@
#include "code\game\machinery\computer\station_alert.dm"
#include "code\game\machinery\computer\teleporter.dm"
#include "code\game\machinery\computer\terminal.dm"
-#include "code\game\machinery\computer\tram_controls.dm"
#include "code\game\machinery\computer\warrant.dm"
#include "code\game\machinery\computer\arcade\arcade.dm"
#include "code\game\machinery\computer\arcade\orion.dm"
@@ -1469,7 +1500,6 @@
#include "code\game\objects\effects\temporary_visuals\projectiles\muzzle.dm"
#include "code\game\objects\effects\temporary_visuals\projectiles\projectile_effects.dm"
#include "code\game\objects\effects\temporary_visuals\projectiles\tracer.dm"
-#include "code\game\objects\items\AI_modules.dm"
#include "code\game\objects\items\airlock_painter.dm"
#include "code\game\objects\items\apc_frame.dm"
#include "code\game\objects\items\binoculars.dm"
@@ -1510,7 +1540,6 @@
#include "code\game\objects\items\fireaxe.dm"
#include "code\game\objects\items\flamethrower.dm"
#include "code\game\objects\items\gift.dm"
-#include "code\game\objects\items\granters.dm"
#include "code\game\objects\items\gun_maintenance.dm"
#include "code\game\objects\items\hand_items.dm"
#include "code\game\objects\items\handcuffs.dm"
@@ -1565,6 +1594,14 @@
#include "code\game\objects\items\virgin_mary.dm"
#include "code\game\objects\items\wall_mounted.dm"
#include "code\game\objects\items\weaponry.dm"
+#include "code\game\objects\items\AI_modules\_AI_modules.dm"
+#include "code\game\objects\items\AI_modules\freeform.dm"
+#include "code\game\objects\items\AI_modules\full_lawsets.dm"
+#include "code\game\objects\items\AI_modules\hacked.dm"
+#include "code\game\objects\items\AI_modules\ion.dm"
+#include "code\game\objects\items\AI_modules\repair.dm"
+#include "code\game\objects\items\AI_modules\supplied.dm"
+#include "code\game\objects\items\AI_modules\zeroth.dm"
#include "code\game\objects\items\circuitboards\circuitboard.dm"
#include "code\game\objects\items\circuitboards\computer_circuitboards.dm"
#include "code\game\objects\items\circuitboards\machine_circuitboards.dm"
@@ -1611,9 +1648,11 @@
#include "code\game\objects\items\devices\scanners\slime_scanner.dm"
#include "code\game\objects\items\devices\scanners\t_scanner.dm"
#include "code\game\objects\items\food\_food.dm"
+#include "code\game\objects\items\food\bait.dm"
#include "code\game\objects\items\food\bread.dm"
#include "code\game\objects\items\food\burgers.dm"
#include "code\game\objects\items\food\cake.dm"
+#include "code\game\objects\items\food\cheese.dm"
#include "code\game\objects\items\food\deepfried.dm"
#include "code\game\objects\items\food\donkpocket.dm"
#include "code\game\objects\items\food\donuts.dm"
@@ -1634,6 +1673,29 @@
#include "code\game\objects\items\food\snacks.dm"
#include "code\game\objects\items\food\soup.dm"
#include "code\game\objects\items\food\spaghetti.dm"
+#include "code\game\objects\items\granters\_granters.dm"
+#include "code\game\objects\items\granters\oragami.dm"
+#include "code\game\objects\items\granters\crafting\_crafting_granter.dm"
+#include "code\game\objects\items\granters\crafting\bone_notes.dm"
+#include "code\game\objects\items\granters\crafting\cannon.dm"
+#include "code\game\objects\items\granters\crafting\desserts.dm"
+#include "code\game\objects\items\granters\crafting\pipegun.dm"
+#include "code\game\objects\items\granters\magic\_spell_granter.dm"
+#include "code\game\objects\items\granters\magic\barnyard.dm"
+#include "code\game\objects\items\granters\magic\blind.dm"
+#include "code\game\objects\items\granters\magic\charge.dm"
+#include "code\game\objects\items\granters\magic\fireball.dm"
+#include "code\game\objects\items\granters\magic\forcewall.dm"
+#include "code\game\objects\items\granters\magic\knock.dm"
+#include "code\game\objects\items\granters\magic\mime.dm"
+#include "code\game\objects\items\granters\magic\mindswap.dm"
+#include "code\game\objects\items\granters\magic\sacred_flame.dm"
+#include "code\game\objects\items\granters\magic\smoke.dm"
+#include "code\game\objects\items\granters\magic\summon_item.dm"
+#include "code\game\objects\items\granters\martial_arts\_martial_arts.dm"
+#include "code\game\objects\items\granters\martial_arts\cqc.dm"
+#include "code\game\objects\items\granters\martial_arts\plasma_fist.dm"
+#include "code\game\objects\items\granters\martial_arts\sleeping_carp.dm"
#include "code\game\objects\items\grenades\_grenade.dm"
#include "code\game\objects\items\grenades\antigravity.dm"
#include "code\game\objects\items\grenades\atmos_grenades.dm"
@@ -1677,6 +1739,7 @@
#include "code\game\objects\items\robot\items\food.dm"
#include "code\game\objects\items\robot\items\generic.dm"
#include "code\game\objects\items\robot\items\hud.dm"
+#include "code\game\objects\items\robot\items\hypo.dm"
#include "code\game\objects\items\robot\items\storage.dm"
#include "code\game\objects\items\robot\items\tools.dm"
#include "code\game\objects\items\stacks\ammonia_crystals.dm"
@@ -1763,7 +1826,6 @@
#include "code\game\objects\structures\headpike.dm"
#include "code\game\objects\structures\hivebot.dm"
#include "code\game\objects\structures\holosign.dm"
-#include "code\game\objects\structures\industrial_lift.dm"
#include "code\game\objects\structures\janicart.dm"
#include "code\game\objects\structures\kitchen_spike.dm"
#include "code\game\objects\structures\ladders.dm"
@@ -1793,7 +1855,6 @@
#include "code\game\objects\structures\tank_dispenser.dm"
#include "code\game\objects\structures\tank_holder.dm"
#include "code\game\objects\structures\training_machine.dm"
-#include "code\game\objects\structures\tram_walls.dm"
#include "code\game\objects\structures\traps.dm"
#include "code\game\objects\structures\votingbox.dm"
#include "code\game\objects\structures\watercloset.dm"
@@ -2008,10 +2069,6 @@
#include "code\modules\admin\verbs\SDQL2\SDQL_2.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2_parser.dm"
#include "code\modules\admin\verbs\SDQL2\SDQL_2_wrappers.dm"
-#include "code\modules\admin\verbs\SDQL2\SDQL_spells\executor_component.dm"
-#include "code\modules\admin\verbs\SDQL2\SDQL_spells\spell_admin_panel.dm"
-#include "code\modules\admin\verbs\SDQL2\SDQL_spells\spell_edit_menu.dm"
-#include "code\modules\admin\verbs\SDQL2\SDQL_spells\spells.dm"
#include "code\modules\admin\view_variables\admin_delete.dm"
#include "code\modules\admin\view_variables\color_matrix_editor.dm"
#include "code\modules\admin\view_variables\debug_variables.dm"
@@ -2196,7 +2253,7 @@
#include "code\modules\antagonists\heretic\magic\manse_link.dm"
#include "code\modules\antagonists\heretic\magic\mansus_grasp.dm"
#include "code\modules\antagonists\heretic\magic\mirror_walk.dm"
-#include "code\modules\antagonists\heretic\magic\nightwater_rebirth.dm"
+#include "code\modules\antagonists\heretic\magic\nightwatcher_rebirth.dm"
#include "code\modules\antagonists\heretic\magic\rust_wave.dm"
#include "code\modules\antagonists\heretic\magic\void_phase.dm"
#include "code\modules\antagonists\heretic\magic\void_pull.dm"
@@ -2261,6 +2318,7 @@
#include "code\modules\antagonists\traitor\objectives\destroy_heirloom.dm"
#include "code\modules\antagonists\traitor\objectives\destroy_item.dm"
#include "code\modules\antagonists\traitor\objectives\destroy_machinery.dm"
+#include "code\modules\antagonists\traitor\objectives\eyesnatching.dm"
#include "code\modules\antagonists\traitor\objectives\hack_comm_console.dm"
#include "code\modules\antagonists\traitor\objectives\kidnapping.dm"
#include "code\modules\antagonists\traitor\objectives\kill_pet.dm"
@@ -2278,12 +2336,15 @@
#include "code\modules\antagonists\wizard\wizard.dm"
#include "code\modules\antagonists\wizard\equipment\artefact.dm"
#include "code\modules\antagonists\wizard\equipment\soulstone.dm"
-#include "code\modules\antagonists\wizard\equipment\spellbook.dm"
+#include "code\modules\antagonists\wizard\equipment\wizard_spellbook.dm"
+#include "code\modules\antagonists\wizard\equipment\spellbook_entries\_entry.dm"
+#include "code\modules\antagonists\wizard\equipment\spellbook_entries\assistance.dm"
+#include "code\modules\antagonists\wizard\equipment\spellbook_entries\challenges.dm"
+#include "code\modules\antagonists\wizard\equipment\spellbook_entries\defensive.dm"
+#include "code\modules\antagonists\wizard\equipment\spellbook_entries\mobility.dm"
+#include "code\modules\antagonists\wizard\equipment\spellbook_entries\offensive.dm"
+#include "code\modules\antagonists\wizard\equipment\spellbook_entries\summons.dm"
#include "code\modules\antagonists\xeno\xeno.dm"
-#include "code\modules\aquarium\aquarium.dm"
-#include "code\modules\aquarium\aquarium_behaviour.dm"
-#include "code\modules\aquarium\aquarium_kit.dm"
-#include "code\modules\aquarium\fish.dm"
#include "code\modules\art\paintings.dm"
#include "code\modules\art\statues.dm"
#include "code\modules\assembly\assembly.dm"
@@ -2628,11 +2689,13 @@
#include "code\modules\clothing\head\crown.dm"
#include "code\modules\clothing\head\fedora.dm"
#include "code\modules\clothing\head\frenchberet.dm"
+#include "code\modules\clothing\head\garlands.dm"
#include "code\modules\clothing\head\hardhat.dm"
#include "code\modules\clothing\head\hat.dm"
#include "code\modules\clothing\head\helmet.dm"
#include "code\modules\clothing\head\jobs.dm"
#include "code\modules\clothing\head\justice.dm"
+#include "code\modules\clothing\head\mind_monkey_helmet.dm"
#include "code\modules\clothing\head\moth.dm"
#include "code\modules\clothing\head\papersack.dm"
#include "code\modules\clothing\head\pirate.dm"
@@ -2753,10 +2816,11 @@
#include "code\modules\events\abductor.dm"
#include "code\modules\events\alien_infestation.dm"
#include "code\modules\events\anomaly.dm"
+#include "code\modules\events\anomaly_bioscrambler.dm"
#include "code\modules\events\anomaly_bluespace.dm"
-#include "code\modules\events\anomaly_delimber.dm"
#include "code\modules\events\anomaly_flux.dm"
#include "code\modules\events\anomaly_grav.dm"
+#include "code\modules\events\anomaly_hallucination.dm"
#include "code\modules\events\anomaly_pyro.dm"
#include "code\modules\events\anomaly_vortex.dm"
#include "code\modules\events\aurora_caelus.dm"
@@ -2775,6 +2839,7 @@
#include "code\modules\events\false_alarm.dm"
#include "code\modules\events\fugitive_spawning.dm"
#include "code\modules\events\ghost_role.dm"
+#include "code\modules\events\gravity_generator_blackout.dm"
#include "code\modules\events\grid_check.dm"
#include "code\modules\events\heart_attack.dm"
#include "code\modules\events\immovable_rod.dm"
@@ -2857,8 +2922,21 @@
#include "code\modules\explorer_drone\exploration_events\fluff.dm"
#include "code\modules\explorer_drone\exploration_events\resource.dm"
#include "code\modules\explorer_drone\exploration_events\trader.dm"
+#include "code\modules\fishing\admin.dm"
+#include "code\modules\fishing\bait.dm"
+#include "code\modules\fishing\fish_catalog.dm"
+#include "code\modules\fishing\fishing_equipment.dm"
+#include "code\modules\fishing\fishing_minigame.dm"
+#include "code\modules\fishing\fishing_portal_machine.dm"
+#include "code\modules\fishing\fishing_rod.dm"
+#include "code\modules\fishing\fishing_traits.dm"
+#include "code\modules\fishing\aquarium\aquarium.dm"
+#include "code\modules\fishing\aquarium\aquarium_kit.dm"
+#include "code\modules\fishing\fish\_fish.dm"
+#include "code\modules\fishing\fish\fish_types.dm"
+#include "code\modules\fishing\sources\_fish_source.dm"
+#include "code\modules\fishing\sources\source_types.dm"
#include "code\modules\flufftext\Dreaming.dm"
-#include "code\modules\flufftext\Hallucination.dm"
#include "code\modules\food_and_drinks\food.dm"
#include "code\modules\food_and_drinks\pizzabox.dm"
#include "code\modules\food_and_drinks\plate.dm"
@@ -2905,6 +2983,21 @@
#include "code\modules\food_and_drinks\restaurant\customers\_customer.dm"
#include "code\modules\forensics\_forensics.dm"
#include "code\modules\forensics\forensics_helpers.dm"
+#include "code\modules\hallucination\_hallucination.dm"
+#include "code\modules\hallucination\airlock.dm"
+#include "code\modules\hallucination\chat.dm"
+#include "code\modules\hallucination\death.dm"
+#include "code\modules\hallucination\fire.dm"
+#include "code\modules\hallucination\hazard.dm"
+#include "code\modules\hallucination\hostile_mob.dm"
+#include "code\modules\hallucination\HUD.dm"
+#include "code\modules\hallucination\husk.dm"
+#include "code\modules\hallucination\item.dm"
+#include "code\modules\hallucination\plasma_flood.dm"
+#include "code\modules\hallucination\polymorph.dm"
+#include "code\modules\hallucination\shock.dm"
+#include "code\modules\hallucination\sound.dm"
+#include "code\modules\hallucination\stray_bullet.dm"
#include "code\modules\holiday\easter.dm"
#include "code\modules\holiday\foreign_calendar.dm"
#include "code\modules\holiday\holidays.dm"
@@ -2958,6 +3051,7 @@
#include "code\modules\hydroponics\grown\kronkus.dm"
#include "code\modules\hydroponics\grown\melon.dm"
#include "code\modules\hydroponics\grown\mushrooms.dm"
+#include "code\modules\hydroponics\grown\olive.dm"
#include "code\modules\hydroponics\grown\onion.dm"
#include "code\modules\hydroponics\grown\peas.dm"
#include "code\modules\hydroponics\grown\pineapple.dm"
@@ -2975,6 +3069,14 @@
#include "code\modules\hydroponics\grown\weeds\kudzu.dm"
#include "code\modules\hydroponics\grown\weeds\nettle.dm"
#include "code\modules\hydroponics\grown\weeds\starthistle.dm"
+#include "code\modules\industrial_lift\crossing_signal.dm"
+#include "code\modules\industrial_lift\industrial_lift.dm"
+#include "code\modules\industrial_lift\lift_master.dm"
+#include "code\modules\industrial_lift\tram_controls.dm"
+#include "code\modules\industrial_lift\tram_landmark.dm"
+#include "code\modules\industrial_lift\tram_lift_master.dm"
+#include "code\modules\industrial_lift\tram_override_objects.dm"
+#include "code\modules\industrial_lift\tram_walls.dm"
#include "code\modules\instruments\items.dm"
#include "code\modules\instruments\piano_synth.dm"
#include "code\modules\instruments\stationary.dm"
@@ -3254,7 +3356,6 @@
#include "code\modules\mob\dead\observer\observer_say.dm"
#include "code\modules\mob\dead\observer\orbit.dm"
#include "code\modules\mob\living\blood.dm"
-#include "code\modules\mob\living\bloodcrawl.dm"
#include "code\modules\mob\living\damage_procs.dm"
#include "code\modules\mob\living\death.dm"
#include "code\modules\mob\living\emote.dm"
@@ -3966,7 +4067,6 @@
#include "code\modules\reagents\chemistry\recipes\special.dm"
#include "code\modules\reagents\chemistry\recipes\toxins.dm"
#include "code\modules\reagents\reagent_containers\blood_pack.dm"
-#include "code\modules\reagents\reagent_containers\borghydro.dm"
#include "code\modules\reagents\reagent_containers\bottle.dm"
#include "code\modules\reagents\reagent_containers\chem_pack.dm"
#include "code\modules\reagents\reagent_containers\dropper.dm"
@@ -3978,6 +4078,7 @@
#include "code\modules\reagents\reagent_containers\pill.dm"
#include "code\modules\reagents\reagent_containers\spray.dm"
#include "code\modules\reagents\reagent_containers\syringes.dm"
+#include "code\modules\reagents\reagent_containers\watering_can.dm"
#include "code\modules\reagents\withdrawal\_addiction.dm"
#include "code\modules\reagents\withdrawal\generic_addictions.dm"
#include "code\modules\recycling\conveyor.dm"
@@ -4080,7 +4181,7 @@
#include "code\modules\research\xenobiology\vatgrowing\samples\cell_lines\common.dm"
#include "code\modules\research\xenobiology\vatgrowing\samples\viruses\_virus.dm"
#include "code\modules\security_levels\keycard_authentication.dm"
-#include "code\modules\security_levels\security_levels.dm"
+#include "code\modules\security_levels\security_level_datums.dm"
#include "code\modules\shuttle\arrivals.dm"
#include "code\modules\shuttle\assault_pod.dm"
#include "code\modules\shuttle\battlecruiser_starfury.dm"
@@ -4105,48 +4206,79 @@
#include "code\modules\shuttle\white_ship.dm"
#include "code\modules\spatial_grid\cell_tracker.dm"
#include "code\modules\spells\spell.dm"
-#include "code\modules\spells\spell_types\aimed.dm"
-#include "code\modules\spells\spell_types\area_teleport.dm"
-#include "code\modules\spells\spell_types\bloodcrawl.dm"
-#include "code\modules\spells\spell_types\charge.dm"
-#include "code\modules\spells\spell_types\cone_spells.dm"
-#include "code\modules\spells\spell_types\conjure.dm"
-#include "code\modules\spells\spell_types\construct_spells.dm"
-#include "code\modules\spells\spell_types\curse.dm"
-#include "code\modules\spells\spell_types\emplosion.dm"
-#include "code\modules\spells\spell_types\ethereal_jaunt.dm"
-#include "code\modules\spells\spell_types\explosion.dm"
-#include "code\modules\spells\spell_types\forcewall.dm"
-#include "code\modules\spells\spell_types\genetic.dm"
-#include "code\modules\spells\spell_types\godhand.dm"
-#include "code\modules\spells\spell_types\infinite_guns.dm"
-#include "code\modules\spells\spell_types\inflict_handler.dm"
-#include "code\modules\spells\spell_types\knock.dm"
-#include "code\modules\spells\spell_types\lichdom.dm"
-#include "code\modules\spells\spell_types\lightning.dm"
-#include "code\modules\spells\spell_types\mime.dm"
-#include "code\modules\spells\spell_types\personality_commune.dm"
-#include "code\modules\spells\spell_types\projectile.dm"
-#include "code\modules\spells\spell_types\rightandwrong.dm"
-#include "code\modules\spells\spell_types\rod_form.dm"
-#include "code\modules\spells\spell_types\santa.dm"
-#include "code\modules\spells\spell_types\shadow_walk.dm"
-#include "code\modules\spells\spell_types\shapeshift.dm"
-#include "code\modules\spells\spell_types\soultap.dm"
-#include "code\modules\spells\spell_types\spacetime_distortion.dm"
-#include "code\modules\spells\spell_types\summonitem.dm"
-#include "code\modules\spells\spell_types\telepathy.dm"
-#include "code\modules\spells\spell_types\the_traps.dm"
-#include "code\modules\spells\spell_types\touch_attacks.dm"
-#include "code\modules\spells\spell_types\trigger.dm"
-#include "code\modules\spells\spell_types\turf_teleport.dm"
-#include "code\modules\spells\spell_types\voice_of_god.dm"
-#include "code\modules\spells\spell_types\wizard.dm"
-#include "code\modules\spells\spell_types\xeno.dm"
+#include "code\modules\spells\spell_types\madness_curse.dm"
+#include "code\modules\spells\spell_types\right_and_wrong.dm"
+#include "code\modules\spells\spell_types\aoe_spell\_aoe_spell.dm"
+#include "code\modules\spells\spell_types\aoe_spell\area_conversion.dm"
+#include "code\modules\spells\spell_types\aoe_spell\knock.dm"
+#include "code\modules\spells\spell_types\aoe_spell\magic_missile.dm"
+#include "code\modules\spells\spell_types\aoe_spell\repulse.dm"
+#include "code\modules\spells\spell_types\aoe_spell\sacred_flame.dm"
+#include "code\modules\spells\spell_types\cone\_cone.dm"
+#include "code\modules\spells\spell_types\conjure\_conjure.dm"
+#include "code\modules\spells\spell_types\conjure\bees.dm"
+#include "code\modules\spells\spell_types\conjure\carp.dm"
+#include "code\modules\spells\spell_types\conjure\constructs.dm"
+#include "code\modules\spells\spell_types\conjure\creatures.dm"
+#include "code\modules\spells\spell_types\conjure\cult_turfs.dm"
+#include "code\modules\spells\spell_types\conjure\ed_swarm.dm"
+#include "code\modules\spells\spell_types\conjure\invisible_chair.dm"
+#include "code\modules\spells\spell_types\conjure\invisible_wall.dm"
+#include "code\modules\spells\spell_types\conjure\link_worlds.dm"
+#include "code\modules\spells\spell_types\conjure\presents.dm"
+#include "code\modules\spells\spell_types\conjure\soulstone.dm"
+#include "code\modules\spells\spell_types\conjure\the_traps.dm"
+#include "code\modules\spells\spell_types\conjure_item\_conjure_item.dm"
+#include "code\modules\spells\spell_types\conjure_item\infinite_guns.dm"
+#include "code\modules\spells\spell_types\conjure_item\invisible_box.dm"
+#include "code\modules\spells\spell_types\conjure_item\lighting_packet.dm"
+#include "code\modules\spells\spell_types\conjure_item\snowball.dm"
+#include "code\modules\spells\spell_types\jaunt\_jaunt.dm"
+#include "code\modules\spells\spell_types\jaunt\bloodcrawl.dm"
+#include "code\modules\spells\spell_types\jaunt\ethereal_jaunt.dm"
+#include "code\modules\spells\spell_types\jaunt\shadow_walk.dm"
+#include "code\modules\spells\spell_types\list_target\_list_target.dm"
+#include "code\modules\spells\spell_types\list_target\telepathy.dm"
+#include "code\modules\spells\spell_types\pointed\_pointed.dm"
+#include "code\modules\spells\spell_types\pointed\abyssal_gaze.dm"
#include "code\modules\spells\spell_types\pointed\barnyard.dm"
#include "code\modules\spells\spell_types\pointed\blind.dm"
+#include "code\modules\spells\spell_types\pointed\dominate.dm"
+#include "code\modules\spells\spell_types\pointed\finger_guns.dm"
+#include "code\modules\spells\spell_types\pointed\fireball.dm"
+#include "code\modules\spells\spell_types\pointed\lightning_bolt.dm"
#include "code\modules\spells\spell_types\pointed\mind_transfer.dm"
-#include "code\modules\spells\spell_types\pointed\pointed.dm"
+#include "code\modules\spells\spell_types\pointed\spell_cards.dm"
+#include "code\modules\spells\spell_types\projectile\_basic_projectile.dm"
+#include "code\modules\spells\spell_types\projectile\juggernaut.dm"
+#include "code\modules\spells\spell_types\self\basic_heal.dm"
+#include "code\modules\spells\spell_types\self\charge.dm"
+#include "code\modules\spells\spell_types\self\disable_tech.dm"
+#include "code\modules\spells\spell_types\self\forcewall.dm"
+#include "code\modules\spells\spell_types\self\lichdom.dm"
+#include "code\modules\spells\spell_types\self\lightning.dm"
+#include "code\modules\spells\spell_types\self\mime_vow.dm"
+#include "code\modules\spells\spell_types\self\mutate.dm"
+#include "code\modules\spells\spell_types\self\night_vision.dm"
+#include "code\modules\spells\spell_types\self\personality_commune.dm"
+#include "code\modules\spells\spell_types\self\rod_form.dm"
+#include "code\modules\spells\spell_types\self\smoke.dm"
+#include "code\modules\spells\spell_types\self\soultap.dm"
+#include "code\modules\spells\spell_types\self\spacetime_distortion.dm"
+#include "code\modules\spells\spell_types\self\stop_time.dm"
+#include "code\modules\spells\spell_types\self\summonitem.dm"
+#include "code\modules\spells\spell_types\self\voice_of_god.dm"
+#include "code\modules\spells\spell_types\shapeshift\_shapeshift.dm"
+#include "code\modules\spells\spell_types\shapeshift\dragon.dm"
+#include "code\modules\spells\spell_types\shapeshift\polar_bear.dm"
+#include "code\modules\spells\spell_types\shapeshift\shapechange.dm"
+#include "code\modules\spells\spell_types\teleport\_teleport.dm"
+#include "code\modules\spells\spell_types\teleport\blink.dm"
+#include "code\modules\spells\spell_types\teleport\teleport.dm"
+#include "code\modules\spells\spell_types\touch\_touch.dm"
+#include "code\modules\spells\spell_types\touch\duffelbag_curse.dm"
+#include "code\modules\spells\spell_types\touch\flesh_to_stone.dm"
+#include "code\modules\spells\spell_types\touch\smite.dm"
#include "code\modules\station_goals\bsa.dm"
#include "code\modules\station_goals\dna_vault.dm"
#include "code\modules\station_goals\shield.dm"
@@ -4238,10 +4370,6 @@
#include "code\modules\tgui\states.dm"
#include "code\modules\tgui\status_composers.dm"
#include "code\modules\tgui\tgui.dm"
-#include "code\modules\tgui\tgui_alert.dm"
-#include "code\modules\tgui\tgui_input_list.dm"
-#include "code\modules\tgui\tgui_input_number.dm"
-#include "code\modules\tgui\tgui_input_text.dm"
#include "code\modules\tgui\tgui_window.dm"
#include "code\modules\tgui\states\admin.dm"
#include "code\modules\tgui\states\always.dm"
@@ -4264,6 +4392,13 @@
#include "code\modules\tgui\states\reverse_contained.dm"
#include "code\modules\tgui\states\self.dm"
#include "code\modules\tgui\states\zlevel.dm"
+#include "code\modules\tgui_input\alert.dm"
+#include "code\modules\tgui_input\list.dm"
+#include "code\modules\tgui_input\number.dm"
+#include "code\modules\tgui_input\text.dm"
+#include "code\modules\tgui_input\say_modal\modal.dm"
+#include "code\modules\tgui_input\say_modal\speech.dm"
+#include "code\modules\tgui_input\say_modal\typing.dm"
#include "code\modules\tgui_panel\audio.dm"
#include "code\modules\tgui_panel\external.dm"
#include "code\modules\tgui_panel\telemetry.dm"
diff --git a/tgui/.eslintrc.yml b/tgui/.eslintrc.yml
index daf5f9936e930..fc7385b68b33d 100644
--- a/tgui/.eslintrc.yml
+++ b/tgui/.eslintrc.yml
@@ -1,4 +1,5 @@
root: true
+extends: prettier
parser: '@typescript-eslint/parser'
parserOptions:
ecmaVersion: 2020
@@ -17,7 +18,6 @@ settings:
react:
version: '16.10'
rules:
-
## Possible Errors
## ----------------------------------------
## Enforce “for” loop update clause moving the counter in the right
@@ -349,15 +349,15 @@ rules:
## Enforce the location of arrow function bodies
# implicit-arrow-linebreak: error
## Enforce consistent indentation
- indent: [error, 2, { SwitchCase: 1 }]
+ # indent: [error, 2, { SwitchCase: 1 }]
## Enforce the consistent use of either double or single quotes in JSX
## attributes
- jsx-quotes: [error, prefer-double]
+ # jsx-quotes: [error, prefer-double]
## Enforce consistent spacing between keys and values in object literal
## properties
- key-spacing: [error, { beforeColon: false, afterColon: true }]
+ # key-spacing: [error, { beforeColon: false, afterColon: true }]
## Enforce consistent spacing before and after keywords
- keyword-spacing: [error, { before: true, after: true }]
+ # keyword-spacing: [error, { before: true, after: true }]
## Enforce position of line comments
# line-comment-position: error
## Enforce consistent linebreak style
@@ -369,15 +369,15 @@ rules:
## Enforce a maximum depth that blocks can be nested
# max-depth: error
## Enforce a maximum line length
- max-len: [error, {
- code: 80,
- ## Ignore imports
- ignorePattern: '^(import\s.+\sfrom\s|.*require\()',
- ignoreUrls: true,
- ignoreRegExpLiterals: true,
- ignoreStrings: true,
- ignoreTemplateLiterals: true,
- }]
+ # max-len: [error, {
+ # code: 80,
+ # ## Ignore imports
+ # ignorePattern: '^(import\s.+\sfrom\s|.*require\()',
+ # ignoreUrls: true,
+ # ignoreRegExpLiterals: true,
+ # ignoreStrings: true,
+ # ignoreTemplateLiterals: true,
+ # }]
## Enforce a maximum number of lines per file
# max-lines: error
## Enforce a maximum number of line of code in a function
@@ -414,7 +414,7 @@ rules:
## Disallow mixed binary operators
# no-mixed-operators: error
## Disallow mixed spaces and tabs for indentation
- no-mixed-spaces-and-tabs: error
+ # no-mixed-spaces-and-tabs: error
## Disallow use of chained assignment expressions
# no-multi-assign: error
## Disallow multiple empty lines
@@ -440,7 +440,7 @@ rules:
## Disallow ternary operators when simpler alternatives exist
# no-unneeded-ternary: error
## Disallow whitespace before properties
- no-whitespace-before-property: error
+ # no-whitespace-before-property: error
## Enforce the location of single-line statements
# nonblock-statement-body-position: error
## Enforce consistent line breaks inside braces
@@ -457,7 +457,7 @@ rules:
## Require or disallow assignment operator shorthand where possible
# operator-assignment: error
## Enforce consistent linebreak style for operators
- operator-linebreak: [error, before]
+ # operator-linebreak: [error, before]
## Require or disallow padding within blocks
# padded-blocks: error
## Require or disallow padding lines between statements
@@ -482,11 +482,11 @@ rules:
## Enforce consistent spacing before blocks
space-before-blocks: [error, always]
## Enforce consistent spacing before function definition opening parenthesis
- space-before-function-paren: [error, {
- anonymous: always,
- named: never,
- asyncArrow: always,
- }]
+ # space-before-function-paren: [error, {
+ # anonymous: always,
+ # named: never,
+ # asyncArrow: always,
+ # }]
## Enforce consistent spacing inside parentheses
space-in-parens: [error, never]
## Require spacing around infix operators
@@ -695,7 +695,7 @@ rules:
react/jsx-closing-tag-location: error
## Enforce or disallow newlines inside of curly braces in JSX attributes and
## expressions (fixable)
- react/jsx-curly-newline: error
+ # react/jsx-curly-newline: error
## Enforce or disallow spaces inside of curly braces in JSX attributes and
## expressions (fixable)
react/jsx-curly-spacing: error
@@ -708,11 +708,11 @@ rules:
## Enforce event handler naming conventions in JSX
react/jsx-handler-names: error
## Validate JSX indentation (fixable)
- react/jsx-indent: [error, 2, {
- checkAttributes: true,
- }]
+ # react/jsx-indent: [error, 2, {
+ # checkAttributes: true,
+ # }]
## Validate props indentation in JSX (fixable)
- react/jsx-indent-props: [error, 2]
+ # react/jsx-indent-props: [error, 2]
## Validate JSX has key prop when in array or iterator
react/jsx-key: error
## Validate JSX maximum depth
diff --git a/tgui/.prettierignore b/tgui/.prettierignore
new file mode 100644
index 0000000000000..79e703c954408
--- /dev/null
+++ b/tgui/.prettierignore
@@ -0,0 +1,15 @@
+## NPM
+/**/node_modules
+
+## Yarn
+/.yarn
+/yarn.lock
+/.pnp.*
+
+/docs
+/public
+/packages/tgui-polyfill
+/packages/tgfont/static
+**/*.json
+**/*.yml
+**/*.md
diff --git a/tgui/.prettierrc.yml b/tgui/.prettierrc.yml
index fe51f01cc4dba..1eebe6098b11d 100644
--- a/tgui/.prettierrc.yml
+++ b/tgui/.prettierrc.yml
@@ -1,8 +1,10 @@
arrowParens: always
-bracketSpacing: true
+breakLongMethodChains: true
endOfLine: lf
+importFormatting: oneline
jsxBracketSameLine: true
jsxSingleQuote: false
+offsetTernaryExpressions: true
printWidth: 80
proseWrap: preserve
quoteProps: preserve
@@ -10,3 +12,4 @@ semi: true
singleQuote: true
tabWidth: 2
trailingComma: es5
+useTabs: false
diff --git a/tgui/.yarn/sdks/eslint/lib/api.js b/tgui/.yarn/sdks/eslint/lib/api.js
index 97a052442a866..fc728d952bb8b 100644
--- a/tgui/.yarn/sdks/eslint/lib/api.js
+++ b/tgui/.yarn/sdks/eslint/lib/api.js
@@ -11,10 +11,10 @@ const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
if (existsSync(absPnpApiPath)) {
if (!process.versions.pnp) {
- // Setup the environment to be able to require eslint/lib/api.js
+ // Setup the environment to be able to require eslint
require(absPnpApiPath).setup();
}
}
-// Defer to the real eslint/lib/api.js your application uses
-module.exports = absRequire(`eslint/lib/api.js`);
+// Defer to the real eslint your application uses
+module.exports = absRequire(`eslint`);
diff --git a/tgui/.yarn/sdks/prettier/index.js b/tgui/.yarn/sdks/prettier/index.js
new file mode 100644
index 0000000000000..f6882d809725b
--- /dev/null
+++ b/tgui/.yarn/sdks/prettier/index.js
@@ -0,0 +1,20 @@
+#!/usr/bin/env node
+
+const {existsSync} = require(`fs`);
+const {createRequire, createRequireFromPath} = require(`module`);
+const {resolve} = require(`path`);
+
+const relPnpApiPath = "../../../.pnp.cjs";
+
+const absPnpApiPath = resolve(__dirname, relPnpApiPath);
+const absRequire = (createRequire || createRequireFromPath)(absPnpApiPath);
+
+if (existsSync(absPnpApiPath)) {
+ if (!process.versions.pnp) {
+ // Setup the environment to be able to require prettier/index.js
+ require(absPnpApiPath).setup();
+ }
+}
+
+// Defer to the real prettier/index.js your application uses
+module.exports = absRequire(`prettier/index.js`);
diff --git a/tgui/.yarn/sdks/prettier/package.json b/tgui/.yarn/sdks/prettier/package.json
new file mode 100644
index 0000000000000..0cbd71ff32d5a
--- /dev/null
+++ b/tgui/.yarn/sdks/prettier/package.json
@@ -0,0 +1,6 @@
+{
+ "name": "prettier",
+ "version": "0.19.0-sdk",
+ "main": "./index.js",
+ "type": "commonjs"
+}
diff --git a/tgui/.yarn/sdks/typescript/lib/tsserver.js b/tgui/.yarn/sdks/typescript/lib/tsserver.js
index 4d90f3879d03a..9f9f4d6f4696c 100644
--- a/tgui/.yarn/sdks/typescript/lib/tsserver.js
+++ b/tgui/.yarn/sdks/typescript/lib/tsserver.js
@@ -18,6 +18,7 @@ const moduleWrapper = tsserver => {
const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
+ const isPortal = str => str.startsWith("portal:/");
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
@@ -30,7 +31,7 @@ const moduleWrapper = tsserver => {
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
- if (isAbsolute(str) && !str.match(/^\^zip:/) && (str.match(/\.zip\//) || isVirtual(str))) {
+ if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
@@ -44,7 +45,7 @@ const moduleWrapper = tsserver => {
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
- if (locator && dependencyTreeRoots.has(`${locator.name}@${locator.reference}`)) {
+ if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
str = resolved;
}
}
@@ -60,10 +61,34 @@ const moduleWrapper = tsserver => {
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
- case `vscode`: {
+ // 2021-10-08: VSCode changed the format in 1.61.
+ // Before | ^zip:/c:/foo/bar.zip/package.json
+ // After | ^/zip//c:/foo/bar.zip/package.json
+ //
+ // 2022-04-06: VSCode changed the format in 1.66.
+ // Before | ^/zip//c:/foo/bar.zip/package.json
+ // After | ^/zip/c:/foo/bar.zip/package.json
+ //
+ // 2022-05-06: VSCode changed the format in 1.68
+ // Before | ^/zip/c:/foo/bar.zip/package.json
+ // After | ^/zip//c:/foo/bar.zip/package.json
+ //
+ case `vscode <1.61`: {
str = `^zip:${str}`;
} break;
+ case `vscode <1.66`: {
+ str = `^/zip/${str}`;
+ } break;
+
+ case `vscode <1.68`: {
+ str = `^/zip${str}`;
+ } break;
+
+ case `vscode`: {
+ str = `^/zip/${str}`;
+ } break;
+
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
@@ -77,7 +102,7 @@ const moduleWrapper = tsserver => {
// everything else is up to neovim
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
- str = `zipfile:${str}`;
+ str = `zipfile://${str}`;
} break;
default: {
@@ -91,9 +116,28 @@ const moduleWrapper = tsserver => {
}
function fromEditorPath(str) {
- return process.platform === `win32`
- ? str.replace(/^\^?zip:\//, ``)
- : str.replace(/^\^?zip:/, ``);
+ switch (hostInfo) {
+ case `coc-nvim`: {
+ str = str.replace(/\.zip::/, `.zip/`);
+ // The path for coc-nvim is in format of //zipfile://.yarn/...
+ // So in order to convert it back, we use .* to match all the thing
+ // before `zipfile:`
+ return process.platform === `win32`
+ ? str.replace(/^.*zipfile:\//, ``)
+ : str.replace(/^.*zipfile:/, ``);
+ } break;
+
+ case `neovim`: {
+ str = str.replace(/\.zip::/, `.zip/`);
+ // The path for neovim is in format of zipfile:////.yarn/...
+ return str.replace(/^zipfile:\/\//, ``);
+ } break;
+
+ case `vscode`:
+ default: {
+ return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
+ } break;
+ }
}
// Force enable 'allowLocalPluginLoads'
@@ -119,8 +163,9 @@ const moduleWrapper = tsserver => {
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
- onMessage(/** @type {string} */ message) {
- const parsedMessage = JSON.parse(message)
+ onMessage(/** @type {string | object} */ message) {
+ const isStringMessage = typeof message === 'string';
+ const parsedMessage = isStringMessage ? JSON.parse(message) : message;
if (
parsedMessage != null &&
@@ -129,11 +174,32 @@ const moduleWrapper = tsserver => {
typeof parsedMessage.arguments.hostInfo === `string`
) {
hostInfo = parsedMessage.arguments.hostInfo;
+ if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
+ const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
+ // The RegExp from https://semver.org/ but without the caret at the start
+ /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
+ ) ?? []).map(Number)
+
+ if (major === 1) {
+ if (minor < 61) {
+ hostInfo += ` <1.61`;
+ } else if (minor < 66) {
+ hostInfo += ` <1.66`;
+ } else if (minor < 68) {
+ hostInfo += ` <1.68`;
+ }
+ }
+ }
}
- return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => {
- return typeof value === `string` ? fromEditorPath(value) : value;
- }));
+ const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
+ return typeof value === 'string' ? fromEditorPath(value) : value;
+ });
+
+ return originalOnMessage.call(
+ this,
+ isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
+ );
},
send(/** @type {any} */ msg) {
diff --git a/tgui/.yarn/sdks/typescript/lib/tsserverlibrary.js b/tgui/.yarn/sdks/typescript/lib/tsserverlibrary.js
index c3de4ff5d7057..878b11946a4cc 100644
--- a/tgui/.yarn/sdks/typescript/lib/tsserverlibrary.js
+++ b/tgui/.yarn/sdks/typescript/lib/tsserverlibrary.js
@@ -18,6 +18,7 @@ const moduleWrapper = tsserver => {
const pnpApi = require(`pnpapi`);
const isVirtual = str => str.match(/\/(\$\$virtual|__virtual__)\//);
+ const isPortal = str => str.startsWith("portal:/");
const normalize = str => str.replace(/\\/g, `/`).replace(/^\/?/, `/`);
const dependencyTreeRoots = new Set(pnpApi.getDependencyTreeRoots().map(locator => {
@@ -30,7 +31,7 @@ const moduleWrapper = tsserver => {
function toEditorPath(str) {
// We add the `zip:` prefix to both `.zip/` paths and virtual paths
- if (isAbsolute(str) && !str.match(/^\^zip:/) && (str.match(/\.zip\//) || isVirtual(str))) {
+ if (isAbsolute(str) && !str.match(/^\^?(zip:|\/zip\/)/) && (str.match(/\.zip\//) || isVirtual(str))) {
// We also take the opportunity to turn virtual paths into physical ones;
// this makes it much easier to work with workspaces that list peer
// dependencies, since otherwise Ctrl+Click would bring us to the virtual
@@ -44,7 +45,7 @@ const moduleWrapper = tsserver => {
const resolved = isVirtual(str) ? pnpApi.resolveVirtual(str) : str;
if (resolved) {
const locator = pnpApi.findPackageLocator(resolved);
- if (locator && dependencyTreeRoots.has(`${locator.name}@${locator.reference}`)) {
+ if (locator && (dependencyTreeRoots.has(`${locator.name}@${locator.reference}`) || isPortal(locator.reference))) {
str = resolved;
}
}
@@ -60,10 +61,34 @@ const moduleWrapper = tsserver => {
//
// Ref: https://github.com/microsoft/vscode/issues/105014#issuecomment-686760910
//
- case `vscode`: {
+ // 2021-10-08: VSCode changed the format in 1.61.
+ // Before | ^zip:/c:/foo/bar.zip/package.json
+ // After | ^/zip//c:/foo/bar.zip/package.json
+ //
+ // 2022-04-06: VSCode changed the format in 1.66.
+ // Before | ^/zip//c:/foo/bar.zip/package.json
+ // After | ^/zip/c:/foo/bar.zip/package.json
+ //
+ // 2022-05-06: VSCode changed the format in 1.68
+ // Before | ^/zip/c:/foo/bar.zip/package.json
+ // After | ^/zip//c:/foo/bar.zip/package.json
+ //
+ case `vscode <1.61`: {
str = `^zip:${str}`;
} break;
+ case `vscode <1.66`: {
+ str = `^/zip/${str}`;
+ } break;
+
+ case `vscode <1.68`: {
+ str = `^/zip${str}`;
+ } break;
+
+ case `vscode`: {
+ str = `^/zip/${str}`;
+ } break;
+
// To make "go to definition" work,
// We have to resolve the actual file system path from virtual path
// and convert scheme to supported by [vim-rzip](https://github.com/lbrayner/vim-rzip)
@@ -77,7 +102,7 @@ const moduleWrapper = tsserver => {
// everything else is up to neovim
case `neovim`: {
str = normalize(resolved).replace(/\.zip\//, `.zip::`);
- str = `zipfile:${str}`;
+ str = `zipfile://${str}`;
} break;
default: {
@@ -91,9 +116,28 @@ const moduleWrapper = tsserver => {
}
function fromEditorPath(str) {
- return process.platform === `win32`
- ? str.replace(/^\^?zip:\//, ``)
- : str.replace(/^\^?zip:/, ``);
+ switch (hostInfo) {
+ case `coc-nvim`: {
+ str = str.replace(/\.zip::/, `.zip/`);
+ // The path for coc-nvim is in format of //zipfile://.yarn/...
+ // So in order to convert it back, we use .* to match all the thing
+ // before `zipfile:`
+ return process.platform === `win32`
+ ? str.replace(/^.*zipfile:\//, ``)
+ : str.replace(/^.*zipfile:/, ``);
+ } break;
+
+ case `neovim`: {
+ str = str.replace(/\.zip::/, `.zip/`);
+ // The path for neovim is in format of zipfile:////.yarn/...
+ return str.replace(/^zipfile:\/\//, ``);
+ } break;
+
+ case `vscode`:
+ default: {
+ return str.replace(/^\^?(zip:|\/zip(\/ts-nul-authority)?)\/+/, process.platform === `win32` ? `` : `/`)
+ } break;
+ }
}
// Force enable 'allowLocalPluginLoads'
@@ -119,8 +163,9 @@ const moduleWrapper = tsserver => {
let hostInfo = `unknown`;
Object.assign(Session.prototype, {
- onMessage(/** @type {string} */ message) {
- const parsedMessage = JSON.parse(message)
+ onMessage(/** @type {string | object} */ message) {
+ const isStringMessage = typeof message === 'string';
+ const parsedMessage = isStringMessage ? JSON.parse(message) : message;
if (
parsedMessage != null &&
@@ -129,11 +174,32 @@ const moduleWrapper = tsserver => {
typeof parsedMessage.arguments.hostInfo === `string`
) {
hostInfo = parsedMessage.arguments.hostInfo;
+ if (hostInfo === `vscode` && process.env.VSCODE_IPC_HOOK) {
+ const [, major, minor] = (process.env.VSCODE_IPC_HOOK.match(
+ // The RegExp from https://semver.org/ but without the caret at the start
+ /(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
+ ) ?? []).map(Number)
+
+ if (major === 1) {
+ if (minor < 61) {
+ hostInfo += ` <1.61`;
+ } else if (minor < 66) {
+ hostInfo += ` <1.66`;
+ } else if (minor < 68) {
+ hostInfo += ` <1.68`;
+ }
+ }
+ }
}
- return originalOnMessage.call(this, JSON.stringify(parsedMessage, (key, value) => {
- return typeof value === `string` ? fromEditorPath(value) : value;
- }));
+ const processedMessageJSON = JSON.stringify(parsedMessage, (key, value) => {
+ return typeof value === 'string' ? fromEditorPath(value) : value;
+ });
+
+ return originalOnMessage.call(
+ this,
+ isStringMessage ? processedMessageJSON : JSON.parse(processedMessageJSON)
+ );
},
send(/** @type {any} */ msg) {
diff --git a/tgui/babel.config.js b/tgui/babel.config.js
index d8ddb75721db3..e702c9a7119d6 100644
--- a/tgui/babel.config.js
+++ b/tgui/babel.config.js
@@ -4,8 +4,9 @@
* @license MIT
*/
-const createBabelConfig = options => {
+const createBabelConfig = (options) => {
const { presets = [], plugins = [], removeConsole } = options;
+ // prettier-ignore
return {
presets: [
[require.resolve('@babel/preset-typescript'), {
@@ -34,7 +35,7 @@ const createBabelConfig = options => {
};
};
-module.exports = api => {
+module.exports = (api) => {
api.cache(true);
const mode = process.env.NODE_ENV;
return createBabelConfig({ mode });
diff --git a/tgui/jest.config.js b/tgui/jest.config.js
index e654f0089b840..8b78818004be4 100644
--- a/tgui/jest.config.js
+++ b/tgui/jest.config.js
@@ -4,9 +4,7 @@ module.exports = {
'/packages/**/__tests__/*.{js,ts,tsx}',
'/packages/**/*.{spec,test}.{js,ts,tsx}',
],
- testPathIgnorePatterns: [
- '/packages/tgui-bench',
- ],
+ testPathIgnorePatterns: ['/packages/tgui-bench'],
testEnvironment: 'jsdom',
testRunner: require.resolve('jest-circus/runner'),
transform: {
diff --git a/tgui/package.json b/tgui/package.json
index 6c2035a5ec483..16a0489973ee9 100644
--- a/tgui/package.json
+++ b/tgui/package.json
@@ -7,16 +7,17 @@
"packages/*"
],
"scripts": {
- "tgui:build": "webpack",
"tgui:analyze": "webpack --analyze",
+ "tgui:bench": "webpack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js",
+ "tgui:build": "webpack",
"tgui:dev": "node --experimental-modules packages/tgui-dev-server/index.js",
"tgui:lint": "eslint packages --ext .js,.cjs,.ts,.tsx",
+ "tgui:prettier": "prettierx --check .",
"tgui:sonar": "eslint packages --ext .js,.cjs,.ts,.tsx -c .eslintrc-sonar.yml",
- "tgui:tsc": "tsc",
"tgui:test": "jest --watch",
"tgui:test-simple": "CI=true jest --color",
"tgui:test-ci": "CI=true jest --color --collect-coverage",
- "tgui:bench": "webpack --env TGUI_BENCH=1 && node packages/tgui-bench/index.js"
+ "tgui:tsc": "tsc"
},
"dependencies": {
"@babel/core": "^7.15.0",
@@ -38,6 +39,7 @@
"common": "workspace:*",
"css-loader": "^5.2.7",
"eslint": "^7.32.0",
+ "eslint-config-prettier": "^8.5.0",
"eslint-plugin-radar": "^0.2.1",
"eslint-plugin-react": "^7.24.0",
"eslint-plugin-unused-imports": "^1.1.4",
@@ -47,6 +49,7 @@
"jest-circus": "^27.0.6",
"jsdom": "^16.7.0",
"mini-css-extract-plugin": "^1.6.2",
+ "prettier": "npm:prettierx@0.19.0",
"sass": "^1.37.5",
"sass-loader": "^11.1.1",
"style-loader": "^2.0.0",
diff --git a/tgui/packages/common/collections.spec.ts b/tgui/packages/common/collections.spec.ts
index 58eff7f354c99..ef35a95cb3ea9 100644
--- a/tgui/packages/common/collections.spec.ts
+++ b/tgui/packages/common/collections.spec.ts
@@ -1,20 +1,20 @@
-import { range, zip } from "./collections";
+import { range, zip } from './collections';
// Type assertions, these will lint if the types are wrong.
-const _zip1: [string, number] = zip(["a"], [1])[0];
+const _zip1: [string, number] = zip(['a'], [1])[0];
-describe("range", () => {
- test("range(0, 5)", () => {
+describe('range', () => {
+ test('range(0, 5)', () => {
expect(range(0, 5)).toEqual([0, 1, 2, 3, 4]);
});
});
-describe("zip", () => {
+describe('zip', () => {
test("zip(['a', 'b', 'c'], [1, 2, 3, 4])", () => {
- expect(zip(["a", "b", "c"], [1, 2, 3, 4])).toEqual([
- ["a", 1],
- ["b", 2],
- ["c", 3],
+ expect(zip(['a', 'b', 'c'], [1, 2, 3, 4])).toEqual([
+ ['a', 1],
+ ['b', 2],
+ ['c', 3],
]);
});
});
diff --git a/tgui/packages/common/collections.ts b/tgui/packages/common/collections.ts
index 5ab378070f0bb..49f500ebd29a6 100644
--- a/tgui/packages/common/collections.ts
+++ b/tgui/packages/common/collections.ts
@@ -11,43 +11,34 @@
*
* If collection is 'null' or 'undefined', it will be returned "as is"
* without emitting any errors (which can be useful in some cases).
- *
- * @returns {any[]}
*/
-export const filter = (iterateeFn: (
- input: T,
- index: number,
- collection: T[],
-) => boolean) =>
- (collection: T[]): T[] => {
- if (collection === null || collection === undefined) {
- return collection;
- }
- if (Array.isArray(collection)) {
- const result: T[] = [];
- for (let i = 0; i < collection.length; i++) {
- const item = collection[i];
- if (iterateeFn(item, i, collection)) {
- result.push(item);
- }
+export const filter =
+ (iterateeFn: (input: T, index: number, collection: T[]) => boolean) =>
+ (collection: T[]): T[] => {
+ if (collection === null || collection === undefined) {
+ return collection;
+ }
+ if (Array.isArray(collection)) {
+ const result: T[] = [];
+ for (let i = 0; i < collection.length; i++) {
+ const item = collection[i];
+ if (iterateeFn(item, i, collection)) {
+ result.push(item);
}
- return result;
}
- throw new Error(`filter() can't iterate on type ${typeof collection}`);
- };
+ return result;
+ }
+ throw new Error(`filter() can't iterate on type ${typeof collection}`);
+ };
type MapFunction = {
- (iterateeFn: (
- value: T,
- index: number,
- collection: T[],
- ) => U): (collection: T[]) => U[];
-
- (iterateeFn: (
- value: T,
- index: K,
- collection: Record,
- ) => U): (collection: Record) => U[];
+ (iterateeFn: (value: T, index: number, collection: T[]) => U): (
+ collection: T[]
+ ) => U[];
+
+ (
+ iterateeFn: (value: T, index: K, collection: Record) => U
+ ): (collection: Record) => U[];
};
/**
@@ -58,7 +49,8 @@ type MapFunction = {
* If collection is 'null' or 'undefined', it will be returned "as is"
* without emitting any errors (which can be useful in some cases).
*/
-export const map: MapFunction = (iterateeFn) =>
+export const map: MapFunction =
+ (iterateeFn) =>
(collection: T[]): U[] => {
if (collection === null || collection === undefined) {
return collection;
@@ -81,9 +73,10 @@ export const map: MapFunction = (iterateeFn) =>
* Given a collection, will run each element through an iteratee function.
* Will then filter out undefined values.
*/
-export const filterMap = (collection: T[], iterateeFn: (
- value: T
-) => U | undefined): U[] => {
+export const filterMap = (
+ collection: T[],
+ iterateeFn: (value: T) => U | undefined
+): U[] => {
const finalCollection: U[] = [];
for (const value of collection) {
@@ -119,22 +112,22 @@ const COMPARATOR = (objA, objB) => {
*
* Iteratees are called with one argument (value).
*/
-export const sortBy = (
- ...iterateeFns: ((input: T) => unknown)[]
-) => (array: T[]): T[] => {
+export const sortBy =
+ (...iterateeFns: ((input: T) => unknown)[]) =>
+ (array: T[]): T[] => {
if (!Array.isArray(array)) {
return array;
}
let length = array.length;
// Iterate over the array to collect criteria to sort it by
let mappedArray: {
- criteria: unknown[],
- value: T,
+ criteria: unknown[];
+ value: T;
}[] = [];
for (let i = 0; i < length; i++) {
const value = array[i];
mappedArray.push({
- criteria: iterateeFns.map(fn => fn(value)),
+ criteria: iterateeFns.map((fn) => fn(value)),
value,
});
}
@@ -163,15 +156,14 @@ export const range = (start: number, end: number): number[] =>
/**
* A fast implementation of reduce.
*/
-export const reduce = (reducerFn, initialValue) => array => {
+export const reduce = (reducerFn, initialValue) => (array) => {
const length = array.length;
let i;
let result;
if (initialValue === undefined) {
i = 1;
result = array[0];
- }
- else {
+ } else {
i = 0;
result = initialValue;
}
@@ -192,13 +184,14 @@ export const reduce = (reducerFn, initialValue) => array => {
* is determined by the order they occur in the array. The iteratee is
* invoked with one argument: value.
*/
-export const uniqBy = (
- iterateeFn?: (value: T) => unknown
-) => (array: T[]): T[] => {
+export const uniqBy =
+ (iterateeFn?: (value: T) => unknown) =>
+ (array: T[]): T[] => {
const { length } = array;
const result: T[] = [];
const seen: unknown[] = iterateeFn ? [] : result;
let index = -1;
+ // prettier-ignore
outer:
while (++index < length) {
let value: T | 0 = array[index];
@@ -214,8 +207,7 @@ export const uniqBy = (
seen.push(computed);
}
result.push(value);
- }
- else if (!seen.includes(computed)) {
+ } else if (!seen.includes(computed)) {
if (seen !== result) {
seen.push(computed);
}
@@ -224,7 +216,6 @@ export const uniqBy = (
}
return result;
};
-/* eslint-enable indent */
export const uniq = uniqBy();
@@ -261,7 +252,8 @@ export const zip = (...arrays: T): Zip => {
* specify how grouped values should be combined. The iteratee is
* invoked with the elements of each group.
*/
-export const zipWith = (iterateeFn: (...values: T[]) => U) =>
+export const zipWith =
+ (iterateeFn: (...values: T[]) => U) =>
(...arrays: T[][]): U[] => {
return map((values: T[]) => iterateeFn(...values))(zip(...arrays));
};
@@ -269,7 +261,7 @@ export const zipWith = (iterateeFn: (...values: T[]) => U) =>
const binarySearch = (
getKey: (value: T) => U,
collection: readonly T[],
- inserting: T,
+ inserting: T
): number => {
if (collection.length === 0) {
return 0;
@@ -301,12 +293,10 @@ const binarySearch = (
return compare > insertingKey ? middle : middle + 1;
};
-export const binaryInsertWith = (getKey: (value: T) => U):
- ((collection: readonly T[], value: T) => T[]) =>
-{
- return (collection, value) => {
+export const binaryInsertWith =
+ (getKey: (value: T) => U) =>
+ (collection: readonly T[], value: T) => {
const copy = [...collection];
copy.splice(binarySearch(getKey, collection, value), 0, value);
return copy;
};
-};
diff --git a/tgui/packages/common/color.js b/tgui/packages/common/color.js
index 2aadae8d6bdf3..672fce529b615 100644
--- a/tgui/packages/common/color.js
+++ b/tgui/packages/common/color.js
@@ -27,23 +27,23 @@ export class Color {
/**
* Creates a color from the CSS hex color notation.
*/
-Color.fromHex = hex => (
+Color.fromHex = (hex) =>
new Color(
parseInt(hex.substr(1, 2), 16),
parseInt(hex.substr(3, 2), 16),
- parseInt(hex.substr(5, 2), 16))
-);
+ parseInt(hex.substr(5, 2), 16)
+ );
/**
* Linear interpolation of two colors.
*/
-Color.lerp = (c1, c2, n) => (
+Color.lerp = (c1, c2, n) =>
new Color(
(c2.r - c1.r) * n + c1.r,
(c2.g - c1.g) * n + c1.g,
(c2.b - c1.b) * n + c1.b,
- (c2.a - c1.a) * n + c1.a)
-);
+ (c2.a - c1.a) * n + c1.a
+ );
/**
* Loops up the color in the provided list of colors
diff --git a/tgui/packages/common/events.js b/tgui/packages/common/events.js
index 6d590a34453be..7eeff511aa566 100644
--- a/tgui/packages/common/events.js
+++ b/tgui/packages/common/events.js
@@ -19,10 +19,9 @@ export class EventEmitter {
if (!listeners) {
throw new Error(`There is no listeners for "${name}"`);
}
- this.listeners[name] = listeners
- .filter(existingListener => {
- return existingListener !== listener;
- });
+ this.listeners[name] = listeners.filter((existingListener) => {
+ return existingListener !== listener;
+ });
}
emit(name, ...params) {
diff --git a/tgui/packages/common/fp.js b/tgui/packages/common/fp.js
index 7aa00a00f3ee2..ba7df09d40701 100644
--- a/tgui/packages/common/fp.js
+++ b/tgui/packages/common/fp.js
@@ -9,6 +9,7 @@
* functions, where each successive invocation is supplied the return
* value of the previous.
*/
+// prettier-ignore
export const flow = (...funcs) => (input, ...rest) => {
let output = input;
for (let func of funcs) {
@@ -37,11 +38,12 @@ export const flow = (...funcs) => (input, ...rest) => {
*/
export const compose = (...funcs) => {
if (funcs.length === 0) {
- return arg => arg;
+ return (arg) => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
+ // prettier-ignore
return funcs.reduce((a, b) => (value, ...rest) =>
a(b(value, ...rest), ...rest));
};
diff --git a/tgui/packages/common/math.ts b/tgui/packages/common/math.ts
index 97e6b60b2ed4a..9dc1d65569362 100644
--- a/tgui/packages/common/math.ts
+++ b/tgui/packages/common/math.ts
@@ -14,7 +14,7 @@ export const clamp = (value, min, max) => {
/**
* Limits a number between 0 and 1.
*/
-export const clamp01 = value => {
+export const clamp01 = (value) => {
return value < 0 ? 0 : value > 1 ? 1 : value;
};
@@ -69,9 +69,7 @@ export const toFixed = (value, fractionDigits = 0) => {
* Range is an array of two numbers, for example: [0, 15].
*/
export const inRange = (value, range) => {
- return range
- && value >= range[0]
- && value <= range[1];
+ return range && value >= range[0] && value <= range[1];
};
/**
@@ -92,7 +90,7 @@ export const keyOfMatchingRange = (value, ranges) => {
/**
* Get number of digits following the decimal point in a number
*/
-export const numberOfDecimalDigits = value => {
+export const numberOfDecimalDigits = (value) => {
if (Math.floor(value) !== value) {
return value.toString().split('.')[1].length || 0;
}
diff --git a/tgui/packages/common/perf.js b/tgui/packages/common/perf.js
index 8414971f93b03..591aa3537dee6 100644
--- a/tgui/packages/common/perf.js
+++ b/tgui/packages/common/perf.js
@@ -48,8 +48,9 @@ const measure = (markerNameA, markerNameB) => {
}
};
-const formatDuration = duration => {
+const formatDuration = (duration) => {
const durationInFrames = duration / FRAME_DURATION;
+ // prettier-ignore
return duration.toFixed(duration < 10 ? 1 : 0) + 'ms '
+ '(' + durationInFrames.toFixed(2) + ' frames)';
};
diff --git a/tgui/packages/common/random.ts b/tgui/packages/common/random.ts
new file mode 100644
index 0000000000000..fbf9030b1bafb
--- /dev/null
+++ b/tgui/packages/common/random.ts
@@ -0,0 +1,32 @@
+import { clamp } from './math';
+
+/**
+ * Returns random number between lowerBound exclusive and upperBound inclusive
+ */
+export const randomNumber = (lowerBound: number, upperBound: number) => {
+ return Math.random() * (upperBound - lowerBound) + lowerBound;
+};
+
+/**
+ * Returns random integer between lowerBound exclusive and upperBound inclusive
+ */
+export const randomInteger = (lowerBound: number, upperBound: number) => {
+ lowerBound = Math.ceil(lowerBound);
+ upperBound = Math.floor(upperBound);
+ return Math.floor(Math.random() * (upperBound - lowerBound) + lowerBound);
+};
+
+/**
+ * Returns random array element
+ */
+export const randomPick = (array: T[]) => {
+ return array[Math.floor(Math.random() * array.length)];
+};
+
+/**
+ * Return 1 with probability P percent; otherwise 0
+ */
+export const randomProb = (probability: number) => {
+ const normalized = clamp(probability, 0, 100) / 100;
+ return Math.random() <= normalized;
+};
diff --git a/tgui/packages/common/react.ts b/tgui/packages/common/react.ts
index c8a08f04934b4..8e42d0971ab41 100644
--- a/tgui/packages/common/react.ts
+++ b/tgui/packages/common/react.ts
@@ -24,7 +24,7 @@ export const classes = (classNames: (string | BooleanLike)[]) => {
*/
export const normalizeChildren = (children: T | T[]) => {
if (Array.isArray(children)) {
- return children.flat().filter(value => value) as T[];
+ return children.flat().filter((value) => value) as T[];
}
if (typeof children === 'object') {
return [children];
@@ -64,6 +64,7 @@ export const pureComponentHooks = {
* A helper to determine whether the object is renderable by React.
*/
export const canRender = (value: unknown) => {
+ // prettier-ignore
return value !== undefined
&& value !== null
&& typeof value !== 'boolean';
diff --git a/tgui/packages/common/redux.js b/tgui/packages/common/redux.js
index ebed11f166b2e..3997134cd7421 100644
--- a/tgui/packages/common/redux.js
+++ b/tgui/packages/common/redux.js
@@ -20,11 +20,11 @@ export const createStore = (reducer, enhancer) => {
const getState = () => currentState;
- const subscribe = listener => {
+ const subscribe = (listener) => {
listeners.push(listener);
};
- const dispatch = action => {
+ const dispatch = (action) => {
currentState = reducer(currentState, action);
for (let i = 0; i < listeners.length; i++) {
listeners[i]();
@@ -49,6 +49,7 @@ export const createStore = (reducer, enhancer) => {
* actions.
*/
export const applyMiddleware = (...middlewares) => {
+ // prettier-ignore
return createStore => (reducer, ...args) => {
const store = createStore(reducer, ...args);
@@ -80,7 +81,7 @@ export const applyMiddleware = (...middlewares) => {
* in the state that are not present in the reducers object. This function
* is also more flexible than the redux counterpart.
*/
-export const combineReducers = reducersObj => {
+export const combineReducers = (reducersObj) => {
const keys = Object.keys(reducersObj);
let hasChanged = false;
return (prevState = {}, action) => {
@@ -94,9 +95,7 @@ export const combineReducers = reducersObj => {
nextState[key] = nextDomainState;
}
}
- return hasChanged
- ? nextState
- : prevState;
+ return hasChanged ? nextState : prevState;
};
};
@@ -136,15 +135,14 @@ export const createAction = (type, prepare = null) => {
};
actionCreator.toString = () => '' + type;
actionCreator.type = type;
- actionCreator.match = action => action.type === type;
+ actionCreator.match = (action) => action.type === type;
return actionCreator;
};
-
// Implementation specific
// --------------------------------------------------------
-export const useDispatch = context => {
+export const useDispatch = (context) => {
return context.store.dispatch;
};
diff --git a/tgui/packages/common/storage.js b/tgui/packages/common/storage.js
index 83dc6d99c1cca..acf842f64083b 100644
--- a/tgui/packages/common/storage.js
+++ b/tgui/packages/common/storage.js
@@ -17,11 +17,10 @@ const INDEXED_DB_STORE_NAME = 'storage-v1';
const READ_ONLY = 'readonly';
const READ_WRITE = 'readwrite';
-const testGeneric = testFn => () => {
+const testGeneric = (testFn) => () => {
try {
return Boolean(testFn());
- }
- catch {
+ } catch {
return false;
}
};
@@ -29,10 +28,12 @@ const testGeneric = testFn => () => {
// Localstorage can sometimes throw an error, even if DOM storage is not
// disabled in IE11 settings.
// See: https://superuser.com/questions/1080011
+// prettier-ignore
const testLocalStorage = testGeneric(() => (
window.localStorage && window.localStorage.getItem
));
+// prettier-ignore
const testIndexedDb = testGeneric(() => (
(window.indexedDB || window.msIndexedDB)
&& (window.IDBTransaction || window.msIDBTransaction)
@@ -96,8 +97,7 @@ class IndexedDbBackend {
req.onupgradeneeded = () => {
try {
req.result.createObjectStore(INDEXED_DB_STORE_NAME);
- }
- catch (err) {
+ } catch (err) {
reject(new Error('Failed to upgrade IDB: ' + req.error));
}
};
@@ -109,7 +109,8 @@ class IndexedDbBackend {
}
getStore(mode) {
- return this.dbPromise.then(db => db
+ // prettier-ignore
+ return this.dbPromise.then((db) => db
.transaction(INDEXED_DB_STORE_NAME, mode)
.objectStore(INDEXED_DB_STORE_NAME));
}
@@ -161,8 +162,7 @@ class StorageProxy {
const backend = new IndexedDbBackend();
await backend.dbPromise;
return backend;
- }
- catch {}
+ } catch {}
}
if (testLocalStorage()) {
return new LocalStorageBackend();
diff --git a/tgui/packages/common/string.babel-plugin.cjs b/tgui/packages/common/string.babel-plugin.cjs
index 68295aefcf843..97ca67c6ea4ca 100644
--- a/tgui/packages/common/string.babel-plugin.cjs
+++ b/tgui/packages/common/string.babel-plugin.cjs
@@ -19,7 +19,7 @@
/**
* Removes excess whitespace and indentation from the string.
*/
-const multiline = str => {
+const multiline = (str) => {
const lines = str.split('\n');
// Determine base indentation
let minIndent;
@@ -40,15 +40,15 @@ const multiline = str => {
// Remove this base indentation and trim the resulting string
// from both ends.
return lines
- .map(line => line.substr(minIndent).trimRight())
+ .map((line) => line.substr(minIndent).trimRight())
.join('\n')
.trim();
};
-const StringPlugin = ref => {
+const StringPlugin = (ref) => {
return {
visitor: {
- TaggedTemplateExpression: path => {
+ TaggedTemplateExpression: (path) => {
if (path.node.tag.name === 'multiline') {
const { quasi } = path.node;
if (quasi.expressions.length > 0) {
diff --git a/tgui/packages/common/string.js b/tgui/packages/common/string.js
index 16a0921a2559c..0d8eef431ed3f 100644
--- a/tgui/packages/common/string.js
+++ b/tgui/packages/common/string.js
@@ -7,7 +7,7 @@
/**
* Removes excess whitespace and indentation from the string.
*/
-export const multiline = str => {
+export const multiline = (str) => {
if (Array.isArray(str)) {
// Small stub to allow usage as a template tag
return multiline(str.join(''));
@@ -32,7 +32,7 @@ export const multiline = str => {
// Remove this base indentation and trim the resulting string
// from both ends.
return lines
- .map(line => line.substr(minIndent).trimRight())
+ .map((line) => line.substr(minIndent).trimRight())
.join('\n')
.trim();
};
@@ -44,12 +44,13 @@ export const multiline = str => {
*
* Example: createGlobPattern('*@domain')('user@domain') === true
*/
-export const createGlobPattern = pattern => {
- const escapeString = str => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
+export const createGlobPattern = (pattern) => {
+ const escapeString = (str) => str.replace(/[|\\{}()[\]^$+*?.]/g, '\\$&');
+ // prettier-ignore
const regex = new RegExp('^'
+ pattern.split(/\*+/).map(escapeString).join('.*')
+ '$');
- return str => regex.test(str);
+ return (str) => regex.test(str);
};
/**
@@ -64,7 +65,7 @@ export const createGlobPattern = pattern => {
*/
export const createSearch = (searchText, stringifier) => {
const preparedSearchText = searchText.toLowerCase().trim();
- return obj => {
+ return (obj) => {
if (!preparedSearchText) {
return true;
}
@@ -72,13 +73,11 @@ export const createSearch = (searchText, stringifier) => {
if (!str) {
return false;
}
- return str
- .toLowerCase()
- .includes(preparedSearchText);
+ return str.toLowerCase().includes(preparedSearchText);
};
};
-export const capitalize = str => {
+export const capitalize = (str) => {
// Handle array
if (Array.isArray(str)) {
return str.map(capitalize);
@@ -87,7 +86,7 @@ export const capitalize = str => {
return str.charAt(0).toUpperCase() + str.slice(1).toLowerCase();
};
-export const toTitleCase = str => {
+export const toTitleCase = (str) => {
// Handle array
if (Array.isArray(str)) {
return str.map(toTitleCase);
@@ -98,20 +97,21 @@ export const toTitleCase = str => {
}
// Handle string
const WORDS_UPPER = ['Id', 'Tv'];
+ // prettier-ignore
const WORDS_LOWER = [
'A', 'An', 'And', 'As', 'At', 'But', 'By', 'For', 'For', 'From', 'In',
'Into', 'Near', 'Nor', 'Of', 'On', 'Onto', 'Or', 'The', 'To', 'With',
];
- let currentStr = str.replace(/([^\W_]+[^\s-]*) */g, str => {
+ let currentStr = str.replace(/([^\W_]+[^\s-]*) */g, (str) => {
return str.charAt(0).toUpperCase() + str.substr(1).toLowerCase();
});
for (let word of WORDS_LOWER) {
const regex = new RegExp('\\s' + word + '\\s', 'g');
- currentStr = currentStr.replace(regex, str => str.toLowerCase());
+ currentStr = currentStr.replace(regex, (str) => str.toLowerCase());
}
for (let word of WORDS_UPPER) {
const regex = new RegExp('\\b' + word + '\\b', 'g');
- currentStr = currentStr.replace(regex, str => str.toLowerCase());
+ currentStr = currentStr.replace(regex, (str) => str.toLowerCase());
}
return currentStr;
};
@@ -122,7 +122,7 @@ export const toTitleCase = str => {
* @param {String} str Encoded HTML string
* @return {String} Decoded HTML string
*/
-export const decodeHtmlEntities = str => {
+export const decodeHtmlEntities = (str) => {
if (!str) {
return str;
}
@@ -133,8 +133,9 @@ export const decodeHtmlEntities = str => {
quot: '"',
lt: '<',
gt: '>',
- apos: '\'',
+ apos: "'",
};
+ // prettier-ignore
return str
// Newline tags
.replace(/ /gi, '\n')
@@ -156,6 +157,7 @@ export const decodeHtmlEntities = str => {
/**
* Converts an object into a query string,
*/
+// prettier-ignore
export const buildQueryString = obj => Object.keys(obj)
.map(key => encodeURIComponent(key)
+ '=' + encodeURIComponent(obj[key]))
diff --git a/tgui/packages/common/timer.js b/tgui/packages/common/timer.js
index 1177071b9c627..7d89e935b9b57 100644
--- a/tgui/packages/common/timer.js
+++ b/tgui/packages/common/timer.js
@@ -28,11 +28,31 @@ export const debounce = (fn, time, immediate = false) => {
};
};
+/**
+ * Returns a function, that, when invoked, will only be triggered at most once
+ * during a given window of time.
+ */
+export const throttle = (fn, time) => {
+ let previouslyRun, queuedToRun;
+ return function invokeFn(...args) {
+ const now = Date.now();
+ queuedToRun = clearTimeout(queuedToRun);
+ if (!previouslyRun || now - previouslyRun >= time) {
+ fn.apply(null, args);
+ previouslyRun = now;
+ } else {
+ queuedToRun = setTimeout(
+ invokeFn.bind(null, ...args),
+ time - (now - previouslyRun)
+ );
+ }
+ };
+};
+
/**
* Suspends an asynchronous function for N milliseconds.
*
* @param {number} time
*/
-export const sleep = time => (
- new Promise(resolve => setTimeout(resolve, time))
-);
+export const sleep = (time) =>
+ new Promise((resolve) => setTimeout(resolve, time));
diff --git a/tgui/packages/common/types.ts b/tgui/packages/common/types.ts
index a92ac122d9fec..e219bd3b7e12e 100644
--- a/tgui/packages/common/types.ts
+++ b/tgui/packages/common/types.ts
@@ -1,5 +1,6 @@
/**
* Returns the arguments of a function F as an array.
*/
+// prettier-ignore
export type ArgumentsOf
= F extends (...args: infer A) => unknown ? A : never;
diff --git a/tgui/packages/common/uuid.js b/tgui/packages/common/uuid.js
index 7721af64949bc..6e156d8649bc7 100644
--- a/tgui/packages/common/uuid.js
+++ b/tgui/packages/common/uuid.js
@@ -11,9 +11,10 @@
*/
export const createUuid = () => {
let d = new Date().getTime();
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
+ // prettier-ignore
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
};
diff --git a/tgui/packages/common/vector.js b/tgui/packages/common/vector.js
index c3ac350a4e8e0..b1f85f7429db8 100644
--- a/tgui/packages/common/vector.js
+++ b/tgui/packages/common/vector.js
@@ -32,17 +32,17 @@ export const vecDivide = (...vecs) => {
};
export const vecScale = (vec, n) => {
- return map(x => x * n)(vec);
+ return map((x) => x * n)(vec);
};
-export const vecInverse = vec => {
- return map(x => -x)(vec);
+export const vecInverse = (vec) => {
+ return map((x) => -x)(vec);
};
-export const vecLength = vec => {
+export const vecLength = (vec) => {
return Math.sqrt(reduce(ADD)(zipWith(MUL)(vec, vec)));
};
-export const vecNormalize = vec => {
+export const vecNormalize = (vec) => {
return vecDivide(vec, vecLength(vec));
};
diff --git a/tgui/packages/tgfont/mkdist.cjs b/tgui/packages/tgfont/mkdist.cjs
index 5c628becf9929..85634bd265d9c 100644
--- a/tgui/packages/tgfont/mkdist.cjs
+++ b/tgui/packages/tgfont/mkdist.cjs
@@ -10,5 +10,4 @@ process.chdir(__dirname);
// Silently make a dist folder
try {
require('fs').mkdirSync('dist');
-}
-catch (err) {}
+} catch (err) {}
diff --git a/tgui/packages/tgui-bench/entrypoint.tsx b/tgui/packages/tgui-bench/entrypoint.tsx
index d72aa60a667ec..377848fe3ae09 100644
--- a/tgui/packages/tgui-bench/entrypoint.tsx
+++ b/tgui/packages/tgui-bench/entrypoint.tsx
@@ -62,8 +62,7 @@ const setupApp = async () => {
}
suite.run();
});
- }
- catch (error) {
+ } catch (error) {
sendMessage({ type: 'error', error });
}
}
diff --git a/tgui/packages/tgui-bench/index.js b/tgui/packages/tgui-bench/index.js
index ac3d8c9cce09a..9f6aee20996d0 100644
--- a/tgui/packages/tgui-bench/index.js
+++ b/tgui/packages/tgui-bench/index.js
@@ -27,7 +27,8 @@ const setup = async () => {
assets += `\n`;
const publicDir = path.resolve(__dirname, '../../public');
- const page = fs.readFileSync(path.join(publicDir, 'tgui.html'), 'utf-8')
+ const page = fs
+ .readFileSync(path.join(publicDir, 'tgui.html'), 'utf-8')
.replace('\n', assets);
server.register(require('fastify-static'), {
@@ -67,8 +68,7 @@ const setup = async () => {
try {
await server.listen(3002, '0.0.0.0');
- }
- catch (err) {
+ } catch (err) {
console.error(err);
process.exit(1);
}
diff --git a/tgui/packages/tgui-bench/lib/benchmark.js b/tgui/packages/tgui-bench/lib/benchmark.js
index 1e76cec70870b..0837678decf9c 100644
--- a/tgui/packages/tgui-bench/lib/benchmark.js
+++ b/tgui/packages/tgui-bench/lib/benchmark.js
@@ -7,6 +7,7 @@
* Manually stripped from useless junk by /tg/station13 maintainers.
* Available under MIT license
*/
+// prettier-ignore
module.exports = (function() {
'use strict';
diff --git a/tgui/packages/tgui-bench/tests/Button.test.tsx b/tgui/packages/tgui-bench/tests/Button.test.tsx
index e3472cbbbfaed..6b806d720ab83 100644
--- a/tgui/packages/tgui-bench/tests/Button.test.tsx
+++ b/tgui/packages/tgui-bench/tests/Button.test.tsx
@@ -7,28 +7,18 @@ const render = createRenderer();
const handleClick = () => undefined;
export const SingleButton = () => {
- const node = (
-
- );
+ const node = ;
render(node);
};
export const SingleButtonWithCallback = () => {
- const node = (
-
- );
+ const node = ;
render(node);
};
export const SingleButtonWithLinkEvent = () => {
const node = (
-
+
);
render(node);
};
@@ -36,11 +26,7 @@ export const SingleButtonWithLinkEvent = () => {
export const ListOfButtons = () => {
const nodes: JSX.Element[] = [];
for (let i = 0; i < 100; i++) {
- const node = (
-
- );
+ const node = ;
nodes.push(node);
}
render(