diff --git a/code/__DEFINES/DNA.dm b/code/__DEFINES/DNA.dm
index cc51f9d3e87352..88efd2c3e6c7eb 100644
--- a/code/__DEFINES/DNA.dm
+++ b/code/__DEFINES/DNA.dm
@@ -166,6 +166,10 @@
#define ORGAN_SLOT_VOICE "vocal_cords"
#define ORGAN_SLOT_ZOMBIE "zombie_infection"
+// DOPPLER ADDITION START - Power-based organs
+#define ORGAN_SLOT_PSYKER "psyker_organ"
+// DOPPLER ADDITION END
+
/// Organ slot external
#define ORGAN_SLOT_EXTERNAL_TAIL "tail"
#define ORGAN_SLOT_EXTERNAL_SPINES "spines"
@@ -238,6 +242,7 @@ GLOBAL_LIST_INIT(organ_process_order, list(
ORGAN_SLOT_XENO_ACIDGLAND,
ORGAN_SLOT_XENO_NEUROTOXINGLAND,
ORGAN_SLOT_XENO_EGGSAC,
+ ORGAN_SLOT_PSYKER, // DOPPLER ADDITION: Adds the psyker organ from powers to organ lists.
))
// Defines for used in creating "perks" for the species preference pages.
diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm
index accfdf2ab22468..aff23a961375e0 100644
--- a/code/__DEFINES/vv.dm
+++ b/code/__DEFINES/vv.dm
@@ -162,6 +162,7 @@
#define VV_HK_PURRBATION "purrbation"
#define VV_HK_APPLY_DNA_INFUSION "apply_dna_infusion"
#define VV_HK_TURN_INTO_MMI "turn_into_mmi"
+#define VV_HK_MOD_POWERS "powermod" // DOPPLER EDIT ADDITION - Power System
// misc
#define VV_HK_SPACEVINE_PURGE "spacevine_purge"
diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm
index c24462b94937b2..8438d1b8ac840a 100644
--- a/code/__DEFINES/~doppler_defines/powers.dm
+++ b/code/__DEFINES/~doppler_defines/powers.dm
@@ -3,16 +3,290 @@
* All defines related to the powers system
*/
-// Maximum amount of points a player can spend on their powers
+/// Maximum amount of points a player can spend on their powers
+#define MAXIMUM_POWER_POINTS 20
+#define POWER_PRIORITY_ROOT "Root"
+#define POWER_PRIORITY_BASIC "Basic"
+#define POWER_PRIORITY_ADVANCED "Advanced"
-#define MAXIMUM_POWER_POINTS 20
+#define POWER_ARCHETYPE_SORCEROUS "Sorcerous"
+#define POWER_ARCHETYPE_RESONANT "Resonant"
+#define POWER_ARCHETYPE_MORTAL "Mortal"
+
+#define POWER_PATH_THAUMATURGE "Thaumaturge"
+#define POWER_PATH_ENIGMATIST "Enigmatist"
+#define POWER_PATH_THEOLOGIST "Theologist"
+#define POWER_PATH_PSYKER "Psyker"
+#define POWER_PATH_CULTIVATOR "Cultivator"
+#define POWER_PATH_ABERRANT "Aberrant"
+#define POWER_PATH_WARFIGHTER "Warfighter"
+#define POWER_PATH_EXPERT "Expert"
+#define POWER_PATH_AUGMENTED "Augmented"
+
+/// Any traits granted by powers.
+#define POWER_TRAIT "power_trait"
+
+/// This power can only be applied to humans.
+#define POWER_HUMAN_ONLY (1<<0)
+/// This power processes on SSpowers (and should implement power process)
+#define POWER_PROCESSES (1<<1)
+/// This power is has a visual aspect in that it changes how the player looks. Used in generating dummies.
+#define POWER_CHANGES_APPEARANCE (1<<2)
+
+/// Security record categories for powers.
+#define CAT_POWER_ALL 0
+#define CAT_POWER_MINOR_THREAT 1
+#define CAT_POWER_MAJOR_THREAT 2
+
+/// Threat level tags used by /datum/power.security_threat
+#define POWER_THREAT_MINOR "minor"
+#define POWER_THREAT_MAJOR "major"
+
+// Trait for when you are unable to use resonant powers
+#define TRAIT_RESONANCE_SILENCED "RESONANCE_SILENCED"
+
+// Trait for when you are immune to resonant powers
+#define TRAIT_ANTIRESONANCE "TRAIT_ANTIRESONANCE"
+
+// Trait for when you are immune to resonant powers that reveal any information about you.
+#define TRAIT_ANTIRESONANCE_SCRYING "TRAIT_ANTIRESONANCE_SCRYING"
+
+// How much anti resonant stuff should cost by default
+#define ANTIRESONANCE_BASE_CHARGE_COST 1
+
+// Listener for dispelling
+#define COMSIG_ATOM_DISPEL "atom_dispel"
+
+/// Fired after a successful unarmed hit (i.e. not missed/blocked), right before damage is applied.
+/// Args: (mob/living/carbon/attacker, mob/living/carbon/target, obj/item/bodypart/affecting, damage, armor_block, limb_sharpness)
+#define COMSIG_HUMAN_UNARMED_HIT "living_unarmed_hit"
+
+/// Fired after a succesful block in /mob/living/proc/check_block().
+/// Args: (atom/hit_by, damage, attack_text, attack_type, armour_penetration, damage_type)
+#define COMSIG_LIVING_SUCCESSFUL_BLOCK "living_succesful_block"
+
+// Bitflag return value(s) from handlers:
+#define DISPEL_RESULT_DISPELLED (1<<0)
+
+// Bitflags for how dispel should behave
+#define DISPEL_CASCADE_CARRIED (1<<0)
+
+// Trait defining that someone can interact remotely with objects. Used by Manipulate and is overall used to bypass range checks on can_interact
+#define TRAIT_REMOTE_INTERACT "remote_interact"
+
+// Trait that allows a mob to keep UIs open beyond their normal range.
+#define TRAIT_NO_UI_DISTANCE "no_ui_distance"
+
+/**
+ * SORCEROUS
+ * All defines related to the sorcerous archetype.
+ */
+
+/**
+ * SORCEROUS: THAUMATURGE
+ * All defines related to the Thaumaturge powers.
+ */
+
+// How much mana you practically can cap out at.
+#define THAUMATURGE_MAX_MANA (MAXIMUM_POWER_POINTS * THAUMATURGE_MANA_MULT )
+
+// The factor with which we multiply our power points to get our mana.
+#define THAUMATURGE_MANA_MULT 2
-GLOBAL_LIST_INIT(path_core_powers, list(
- "path_sorcerous" = /datum/power/prestidigitation,
- "path_resonant" = /datum/power/meditate,
- "path_mortal" = /datum/power/tenacious
-))
+// How many spells of a type can you prepare max?
+#define THAUMATURGE_MAX_CHARGES_BASE 6
+
+// For refund abilities, how much refund chance does each level/degree add.
+#define THAUMATURGE_REFUND_MULT_BASE 35
+#define THAUMATURGE_REFUND_MULT_AFFINITY 5
+
+// hard cap on refund powers.
+#define THAUMATURGE_REFUND_MAX 75
+
+// How long a thaumaturge has to sleep to get their charges. Please make sure that this is BELOW the normal sleep verb's time.
+#define THAUMATURGE_SLEEP_TIME 30 SECONDS
+
+// Thaumaturge action resource display modes.
+#define THAUMATURGE_RESOURCE_DISPLAY_CHARGES "charges"
+#define THAUMATURGE_RESOURCE_DISPLAY_PREP_COST "prep_cost"
+
+// Thresholds for hemomancy whenever you are either low blood or ready to overcast, relative to blood volume normal.
+#define THAUMATURGE_HEMOMANCY_LOW_BLOOD_THRESHOLD 0.85
+#define THAUMATURGE_HEMOMANCY_OVERCAST_THRESHOLD 1.10
+
+// The minimum affinity you have with your blood hand.
+#define THAUMATURGE_HEMOMANCY_MIN_AFFINITY 3
+// The maximum affinity you can get with overcasting.
+#define THAUMATURGE_HEMOMANCY_MAX_AFFINITY 6
+// How much blood cost scales from prep_cost (and UI display) for hemomancy.
+#define THAUMATURGE_HEMOMANCY_BLOOD_COST_MULTIPLIER 4
+
+/**
+ * SORCEROUS: ENIGMATIST
+ * All defines related to the enigmatist powers.
+ */
+
+/// Standard value for how much damage enigmatist chalk can take.
+#define ENIGMATIST_CHALK_STANDARD_INTEGRITY 100
+
+// Standard damages an enigmatist spell can do.
+#define ENIGMATIST_CHALK_TRIVIAL_DAMAGE (ENIGMATIST_CHALK_STANDARD_INTEGRITY / 100)
+#define ENIGMATIST_CHALK_MINOR_DAMAGE (ENIGMATIST_CHALK_STANDARD_INTEGRITY / 10)
+#define ENIGMATIST_CHALK_MODERATE_DAMAGE (ENIGMATIST_CHALK_STANDARD_INTEGRITY / 5)
+#define ENIGMATIST_CHALK_MAJOR_DAMAGE (ENIGMATIST_CHALK_STANDARD_INTEGRITY / 2)
+#define ENIGMATIST_CHALK_CRUSHING_DAMAGE (ENIGMATIST_CHALK_STANDARD_INTEGRITY)
+
+/// From /obj/item/enigmatist_chalk/click_alt(...): (enigmatist_flags, list/spell_options)
+#define COMSIG_ENIGMATIST_CHALK_SELECTION "enigmatist_chalk_selection"
+
+// Bitflags for what type of chalk/power a given chalk/power is.
+/// Basic resonant chalks/powers.
+#define ENIGMATIST_RESONANT (1<<0)
+/// Chalks/powers relating to unsealed lore.
+#define ENIGMATIST_UNSEALED (1<<1)
+/// Chalks/powers relating to illuminated lore.
+#define ENIGMATIST_ILLUMINATED (1<<2)
+/// Chalks/powers relating to divided lore.
+#define ENIGMATIST_DIVIDED (1<<3)
+
+/// Any Enigmatist lore whatsoever.
+#define ENIGMATIST_ANY_ALL (ENIGMATIST_RESONANT|ENIGMATIST_UNSEALED|ENIGMATIST_ILLUMINATED|ENIGMATIST_DIVIDED)
+
+/**
+ * SORCEROUS: THEOLOGIST
+ * All defines related to the enigmatist powers.
+ */
+
+// How much root abilities should heal (max), if they heal.
+#define THEOLOGIST_ROOT_HEALING 30
+
+// Healing equates to this much piety.
+#define THEOLOGIST_PIETY_HEALING_COEFFICIENT 0.2
+
+// Maximum amount of Piety (chaplain gets double this amount)
+#define THEOLOGIST_PIETY_MAX 50
+
+// UI location of the Piety element
+#define THEOLOGIST_UI_SCREEN_LOC "WEST,CENTER-2:15"
+
+// In case the space is taken up by cultivator
+#define THEOLOGIST_ALT_UI_SCREEN_LOC "WEST+1,CENTER-2:15"
+
+// Trait made as to prevent duplicate smites.
+#define TRAIT_HAS_SMITING_STRIKE "has_smiting_strike"
+
+// Standard Theologian costs
+#define THEOLOGIST_PIETY_TRIVIAL (THEOLOGIST_PIETY_MAX / 100)
+#define THEOLOGIST_PIETY_MINOR (THEOLOGIST_PIETY_MAX / 10)
+#define THEOLOGIST_PIETY_MODERATE (THEOLOGIST_PIETY_MAX / 5)
+#define THEOLOGIST_PIETY_MAJOR (THEOLOGIST_PIETY_MAX / 2)
+#define THEOLOGIST_PIETY_CRUSHING (THEOLOGIST_PIETY_MAX)
+
+/**
+ * RESONANT
+ * All defines related to the resonant archetype.
+ */
+
+/**
+ * RESONANT: CULTIVATOR
+ * All defines related to the cultivator powers.
+ */
+
+// Maximum amount of Energy we can have.
+#define CULTIVATOR_ENERGY_MAX 1000
+
+// How much energy we get from meditation every 2.5 seconds
+#define CULTIVATOR_ENERGY_MEDITATION_POWER 5
+
+// UI location of the Cultivator element
+#define CULTIVATOR_UI_SCREEN_LOC "WEST,CENTER-2:15"
+
+// Bonus damage on strikes done while in alignment. Balancing notes: punches have a base 20% miss chance, and this does not stack with martial arts.
+#define CULTIVATOR_ALIGNMENT_DAMAGE_BONUS 15
+
+// The max amount of Energy we give from aura farming per second
+#define CULTIVATOR_MAX_CULTIVATION_BONUS 3
+// The min amount of Energy we give from aura farming per second
+#define CULTIVATOR_MIN_CULTIVATION_BONUS 0
+
+// How much does activating the alignment cost
+#define CULTIVATOR_ALIGNMENT_ACTIVATION_COST 200
+
+// How much does sustaining the alignment cost
+#define CULTIVATOR_ALIGNMENT_UPKEEP_COST 3
+
+// Standard Energy cost defines for Cultivators.
+#define CULTIVATOR_ENERGY_TRIVIAL (CULTIVATOR_ENERGY_MAX / 100)
+#define CULTIVATOR_ENERGY_MINOR (CULTIVATOR_ENERGY_MAX / 10)
+#define CULTIVATOR_ENERGY_MODERATE (CULTIVATOR_ENERGY_MAX / 5)
+#define CULTIVATOR_ENERGY_MAJOR (CULTIVATOR_ENERGY_MAX / 2)
+#define CULTIVATOR_ENERGY_CRUSHING (CULTIVATOR_ENERGY_MAX)
+
+// Defines SPECIFICALLY for auro farming amounts
+#define CULTIVATOR_AURA_FARM_TRIVIAL (CULTIVATOR_MAX_CULTIVATION_BONUS / 100)
+#define CULTIVATOR_AURA_FARM_MINOR (CULTIVATOR_MAX_CULTIVATION_BONUS / 10)
+#define CULTIVATOR_AURA_FARM_MODERATE (CULTIVATOR_MAX_CULTIVATION_BONUS / 5)
+#define CULTIVATOR_AURA_FARM_MAJOR (CULTIVATOR_MAX_CULTIVATION_BONUS / 2)
+#define CULTIVATOR_AURA_FARM_CRUSHING (CULTIVATOR_MAX_CULTIVATION_BONUS)
+
+// Cultivator alignment activion/deactivation signals
+#define COMSIG_CULTIVATOR_ALIGNMENT_ENABLED "cultivator_alignment_enabled"
+#define COMSIG_CULTIVATOR_ALIGNMENT_DISABLED "cultivator_alignment_disabled"
+
+// The trait for Astral Touched's flight upgrades (using AddElementTrait)
+#define TRAIT_ASTRAL_TOUCHED_FLIGHT "astral_touched_flight"
+
+/**
+ * RESONANT: PSYKER
+ * All defines related to the psyker powers.
+ */
+
+// Standard stress threshold value for the Psyker's organ.
+#define PSYKER_STRESS_STANDARD_THRESHOLD 100
+
+// Standard stress recovery per second before modifiers.
+#define PSYKER_STRESS_RECOVERY 1
+
+// How much meditate recovers.
+#define PSYKER_STRESS_MEDITATION_POWER 10
+// How much chemotropic gland recovers with substances.
+#define PSYKER_STRESS_CHEMOTROPIC_POWER 10
+
+// Standard stress for Psykers. This all goes off of the base organ being 100.
+#define PSYKER_STRESS_TRIVIAL (PSYKER_STRESS_STANDARD_THRESHOLD / 100)
+#define PSYKER_STRESS_MINOR (PSYKER_STRESS_STANDARD_THRESHOLD / 10)
+#define PSYKER_STRESS_MODERATE (PSYKER_STRESS_STANDARD_THRESHOLD / 5)
+#define PSYKER_STRESS_MAJOR (PSYKER_STRESS_STANDARD_THRESHOLD / 2)
+#define PSYKER_STRESS_CRUSHING (PSYKER_STRESS_STANDARD_THRESHOLD)
+
+// Psyker event tiers.
+#define PSYKER_EVENT_TIER_MILD 1
+#define PSYKER_EVENT_TIER_SEVERE 2
+#define PSYKER_EVENT_TIER_CATASTROPHIC 3
+
+// Psyker event rarities
+#define PSYKER_EVENT_RARITY_COMMON 100
+#define PSYKER_EVENT_RARITY_UNCOMMON 50
+#define PSYKER_EVENT_RARITY_RARE 25
+#define PSYKER_EVENT_RARITY_VERYRARE 10
+
+// Standard messages for Psyker Events
+#define PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE "As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity; as well as a feeling that something is very wrong."
+
+// The trait for Psyker's Levitate power.
+#define TRAIT_PSYKER_LEVITATE_FLIGHT "psyker_levitate_flight"
+
+// Efficiency multiplier when using another root's organ
+#define PSYKER_MISMATCHED_ORGAN_EFFICIENCY 0.33
+
+/**
+ * RESONANT: ABERRANT
+ * All defines related to the aberrant powers.
+ */
+
+// Trait that lets you use the riftwalker mechanic.
+#define TRAIT_ABERRANT_RIFTWALKER "riftwalker"
/**MORTAL DEFINES
* I'm literally just using this to define Breacher Knuckle right now
@@ -22,3 +296,73 @@ GLOBAL_LIST_INIT(path_core_powers, list(
#define MARTIALART_BREACHERKNUCKLE "breacher knuckle"
#define MARTIALART_MAD_DOG "the mag dog style"
+
+/**
+ * MORTAL: WARFIGHTER
+ * All defines related to the augmented powers.
+ */
+
+// The amount to multiple the effects of all commander powers by.
+#define WARFIGHTER_COMMANDER_BASE_MULT 1
+
+// The multiplier bonus for sharing a department with the target as a commander
+#define WARFIGHTER_COMMANDER_DEPARTMENT_BONUS 0.3
+
+// The multiplier bonus for being a head of staff as a commander
+#define WARFIGHTER_COMMANDER_HEAD_BONUS 0.3
+
+// The global GCD for Warfigher powers
+#define WARFIGHTER_COMMANDER_SHARED_COOLDOWN 2 SECONDS
+
+// Trait for the Explosives Specialist power
+#define TRAIT_POWER_EXPLOSIVES_SPECIALIST "power_explosives_specialist"
+
+/**
+ * MORTAL: Augmented
+ * All defines related to the augmented powers.
+ */
+
+// Max quality on augments
+#define AUGMENTED_PREMIUM_QUALITY_MAX 100
+
+// The quality you start with at roundstart
+#define AUGMENTED_PREMIUM_QUALITY_START 75
+
+// How often augments will normally lose quality, and how much.
+#define AUGMENTED_DECAY_INTERVAL 4 MINUTES
+#define AUGMENTED_DECAY_AMOUNT 1
+
+// Thresholds for Premium Quality tiers. As long as it is above the number = all is good
+#define AUGMENTED_PREMIUM_THRESHOLD_OPTIMAL (AUGMENTED_PREMIUM_QUALITY_MAX * 0.75)
+#define AUGMENTED_PREMIUM_THRESHOLD_HIGH (AUGMENTED_PREMIUM_QUALITY_MAX * 0.50)
+#define AUGMENTED_PREMIUM_THRESHOLD_MEDIUM (AUGMENTED_PREMIUM_QUALITY_MAX * 0.25)
+#define AUGMENTED_PREMIUM_THRESHOLD_LOW (AUGMENTED_PREMIUM_QUALITY_MAX * 0)
+
+// Percentage mods for quality.
+#define AUGMENTED_PREMIUM_QUALITY_TRIVIAL (AUGMENTED_PREMIUM_QUALITY_MAX / 100)
+#define AUGMENTED_PREMIUM_QUALITY_MINOR (AUGMENTED_PREMIUM_QUALITY_MAX / 10)
+#define AUGMENTED_PREMIUM_QUALITY_MODERATE (AUGMENTED_PREMIUM_QUALITY_MAX / 5)
+#define AUGMENTED_PREMIUM_QUALITY_MAJOR (AUGMENTED_PREMIUM_QUALITY_MAX / 2)
+#define AUGMENTED_PREMIUM_QUALITY_CRUSHING (AUGMENTED_PREMIUM_QUALITY_MAX)
+
+// The amount of performance from each. We expect high to be the norm, so that is our 1, instead of optimal.
+#define AUGMENTED_PREMIUM_EFFICIENCY_OPTIMAL 1.2
+#define AUGMENTED_PREMIUM_EFFICIENCY_HIGH 1
+#define AUGMENTED_PREMIUM_EFFICIENCY_MEDIUM 0.85
+#define AUGMENTED_PREMIUM_EFFICIENCY_LOW 0.6
+#define AUGMENTED_PREMIUM_EFFICIENCY_BROKEN 0
+
+// Refurbish steps
+#define AUGMENTED_REFURBISH_OPEN "open"
+#define AUGMENTED_REFURBISH_PARTS "parts"
+#define AUGMENTED_REFURBISH_CALIBRATE "calibrate"
+#define AUGMENTED_REFURBISH_CLOSE "close"
+
+// Used for the prefs to shorthand tell there's nothing in the right or left arm augment slot.
+#define AUGMENTED_NO_AUGMENT "None"
+
+// Arm selection overrides for augmented powers.
+#define AUGMENTED_ARM_USE_PREFS 0
+#define AUGMENTED_ARM_LEFT 1
+#define AUGMENTED_ARM_RIGHT 2
+#define AUGMENTED_ARM_BOTH 3
diff --git a/code/__DEFINES/~doppler_defines/signals.dm b/code/__DEFINES/~doppler_defines/signals.dm
index eb564d0987de5b..f8e36d17a90bd9 100644
--- a/code/__DEFINES/~doppler_defines/signals.dm
+++ b/code/__DEFINES/~doppler_defines/signals.dm
@@ -8,6 +8,12 @@
#define COMPONENT_POWER_SUCCESS (1<<0)
#define COMPONENT_NO_CELL (1<<1)
#define COMPONENT_NO_CHARGE (1<<2)
+/// Sent right before a power action resolves through use_action: (mob/living/user, atom/target)
+#define COMSIG_POWER_ACTION_USED "power_action_used"
+/// Sent when a power action successfully resolves (use_action returned TRUE): (mob/living/user, atom/target)
+#define COMSIG_POWER_ACTION_SUCCESS "power_action_success"
+/// Sent by thaumaturge get_affinity for external affinity riders: (datum/action/cooldown/power/thaumaturge/action)
+#define COMSIG_THAUMATURGE_AFFINITY_QUERY "thaumaturge_affinity_query"
/// For when a Hemophage's pulsating tumor gets added to their body.
#define COMSIG_PULSATING_TUMOR_ADDED "pulsating_tumor_added"
diff --git a/code/_globalvars/~doppler_globalvars/powers.dm b/code/_globalvars/~doppler_globalvars/powers.dm
new file mode 100644
index 00000000000000..f71ff00f51cc7b
--- /dev/null
+++ b/code/_globalvars/~doppler_globalvars/powers.dm
@@ -0,0 +1,44 @@
+/// Unholy mobs for various Theologist powers. Includes subtypes.
+GLOBAL_LIST_INIT(unholy_mobs, typecacheof(list(
+ /mob/living/basic/mining, // mining mobs
+ /mob/living/simple_animal/hostile/asteroid, // mining mobs
+ /mob/living/simple_animal/hostile/megafauna, // megafauna
+ /mob/living/basic/boss, //megafauna
+ /mob/living/basic/skeleton, // undead
+ /mob/living/basic/zombie, // undead
+ /mob/living/basic/revenant, // undead
+ /mob/living/basic/construct, // cult constructs
+ /mob/living/basic/heretic_summon, // heretic
+)))
+
+/// Shapechanger power.
+GLOBAL_LIST_INIT(shapechange_form_types, list(
+ "Parrot" = /mob/living/basic/parrot,
+ "Penguin" = /mob/living/basic/pet/penguin/emperor,
+ "Stoat" = /mob/living/basic/stoat,
+ "Fox" = /mob/living/basic/pet/fox,
+ "Cat" = /mob/living/basic/pet/cat,
+ "Corgi" = /mob/living/basic/pet/dog/corgi,
+ "Mouse" = /mob/living/basic/mouse,
+ "Lizard" = /mob/living/basic/lizard,
+ "Snake" = /mob/living/basic/snake,
+ "Cockroach" = /mob/living/basic/cockroach,
+ "Duct Spider" = /mob/living/basic/spider/maintenance,
+ "Bat" = /mob/living/basic/bat,
+ "Butterfly" = /mob/living/basic/butterfly,
+ "Mothroach" = /mob/living/basic/mothroach,
+))
+
+/// Shapechanger: Spider power.
+GLOBAL_LIST_INIT(shapechange_spider_form_types, list(
+ "Hunter" = /mob/living/basic/spider/giant/hunter,
+ "Guard" = /mob/living/basic/spider/giant/guard,
+ "Ambush" = /mob/living/basic/spider/giant/ambush,
+))
+
+/// Light sizes for bioluminescene
+GLOBAL_LIST_INIT(bioluminescence_sizes, list(
+ "Small" = 2,
+ "Medium" = 3,
+ "Large" = 4,
+))
diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm
index a073d67de4f03f..bf64dbeea7a88c 100644
--- a/code/controllers/subsystem/ticker.dm
+++ b/code/controllers/subsystem/ticker.dm
@@ -556,9 +556,12 @@ SUBSYSTEM_DEF(ticker)
if(new_player_mob.client?.prefs?.should_be_random_hardcore(player_assigned_role, new_player_living.mind))
new_player_mob.client.prefs.hardcore_random_setup(new_player_living)
SSquirks.AssignQuirks(new_player_living, new_player_mob.client)
+ // DOPPLER EDIT ADDITION BEGIN - Archetype Powers
+ SSpowers.assign_powers(new_player_living, new_player_mob.client)
+ // DOPPLER EDIT ADDITION END
else // clear any personalities the prefs added since our job clearly does not want them
new_player_living.clear_personalities()
- // DOPPLER EDIT ADDITION START - Restricted loadout items
+ // DOPPLER EDIT ADDITION START - Restricted loadout items
var/list/loadout = loadout_list_to_datums(new_player_mob.client?.prefs?.read_preference(/datum/preference/loadout))
for(var/datum/loadout_item/item as anything in loadout)
if(item.restricted_roles && length(item.restricted_roles) && !(player_assigned_role.title in item.restricted_roles))
diff --git a/code/datums/martial/_martial.dm b/code/datums/martial/_martial.dm
index 4a0c9c84b3089f..6ff6f9dc14b12e 100644
--- a/code/datums/martial/_martial.dm
+++ b/code/datums/martial/_martial.dm
@@ -102,7 +102,12 @@
if(HAS_TRAIT(source, TRAIT_PACIFISM) && !pacifist_style)
return NONE
- return harm_act(source, attack_target)
+ // DOPPLER EDIT BEGIN - Sends unarmed hit signaler with martial arts
+ var/harm_result = harm_act(source, attack_target)
+ if(harm_result & COMPONENT_CANCEL_ATTACK_CHAIN)
+ send_unarmed_hit_signal(source, attack_target)
+ return harm_result
+ // DOPPLER EDIT END
return help_act(source, attack_target)
diff --git a/code/datums/records/manifest.dm b/code/datums/records/manifest.dm
index 1abcaf82147e11..1f416f40cc2e00 100644
--- a/code/datums/records/manifest.dm
+++ b/code/datums/records/manifest.dm
@@ -168,6 +168,9 @@ GLOBAL_DATUM_INIT(manifest, /datum/manifest, new)
minor_disabilities_desc = person.get_quirk_string(TRUE, CAT_QUIRK_MINOR_DISABILITY),
quirk_notes = person.get_quirk_string(TRUE, CAT_QUIRK_NOTES),
// DOPPLER EDIT BEGIN - records & flavor text
+ power_notes = person.get_sec_power_string(CAT_POWER_ALL),
+ power_notes_minor = person.get_sec_power_string(CAT_POWER_MINOR_THREAT, include_empty_text = FALSE),
+ power_notes_major = person.get_sec_power_string(CAT_POWER_MAJOR_THREAT, include_empty_text = FALSE),
past_general_records = person_client?.prefs.read_preference(/datum/preference/text/past_general_records),
past_medical_records = person_client?.prefs.read_preference(/datum/preference/text/past_medical_records),
past_security_records = person_client?.prefs.read_preference(/datum/preference/text/past_security_records),
diff --git a/code/datums/records/record.dm b/code/datums/records/record.dm
index 155403d7dea6df..3d7448069ef4b4 100644
--- a/code/datums/records/record.dm
+++ b/code/datums/records/record.dm
@@ -109,6 +109,9 @@
mental_status = MENTAL_STABLE,
quirk_notes,
// DOPPLER EDIT START - records & flavor text
+ power_notes = "No powers declared.",
+ power_notes_minor = "",
+ power_notes_major = "",
past_general_records = "",
past_medical_records = "",
past_security_records = "",
@@ -126,6 +129,9 @@
src.mental_status = mental_status
src.quirk_notes = quirk_notes
// DOPPLER EDIT START
+ src.power_notes = power_notes
+ src.power_notes_minor = power_notes_minor
+ src.power_notes_major = power_notes_major
src.past_general_records = past_general_records
src.past_medical_records = past_medical_records
src.past_security_records = past_security_records
@@ -274,6 +280,8 @@
if(past_general_records != "")
final_paper_text += " General Records:"
final_paper_text += " [past_general_records] "
+ final_paper_text += " Powers:"
+ final_paper_text += " [power_notes || "No powers declared."] "
// DOPPLER EDIT END
final_paper_text += "
Security Data
"
diff --git a/code/game/machinery/computer/records/security.dm b/code/game/machinery/computer/records/security.dm
index abf3f1b36643d8..280e45f56c86e0 100644
--- a/code/game/machinery/computer/records/security.dm
+++ b/code/game/machinery/computer/records/security.dm
@@ -131,6 +131,9 @@
trim = target.trim,
wanted_status = target.wanted_status,
// DOPPLER EDIT BEGIN - records & flavor text
+ power_notes = target.power_notes,
+ power_notes_minor = target.power_notes_minor,
+ power_notes_major = target.power_notes_major,
past_general_records = target.past_general_records,
past_security_records = target.past_security_records,
age_chronological = target.age_chronological,
diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm
index 87313ba7bdfdfb..a24313eb103380 100644
--- a/code/game/objects/items.dm
+++ b/code/game/objects/items.dm
@@ -495,6 +495,13 @@
if(LAZYLEN(unique_reskin) && !current_skin)
.["reskinnable"] = "This item is able to be reskinned! Alt-Click to do so!"
+ // DOPPLER EDIT ADDITION START: Thaumaturges can examine items for affinity stat
+ if(affinity)
+ var/mob/living/living_user = isliving(user) ? user : null
+ if(isobserver(user) || (living_user && living_user.has_power_in_path(POWER_PATH_THAUMATURGE)))
+ .["affinity [affinity]"] = "Provides affinity [affinity] for thaumaturgic powers."
+ // DOPPLER EDIT ADDITION END
+
/obj/item/examine_descriptor(mob/user)
return "item"
diff --git a/code/game/objects/items/devices/scanners/health_analyzer.dm b/code/game/objects/items/devices/scanners/health_analyzer.dm
index 9050174585c4ed..d0eee1f1edb696 100644
--- a/code/game/objects/items/devices/scanners/health_analyzer.dm
+++ b/code/game/objects/items/devices/scanners/health_analyzer.dm
@@ -342,7 +342,20 @@
var/list/cyberimps
for(var/obj/item/organ/target_organ as anything in humantarget.organs)
if(IS_ROBOTIC_ORGAN(target_organ) && !(target_organ.organ_flags & ORGAN_HIDDEN))
- LAZYADD(cyberimps, target_organ.examine_title(user))
+ // DOPPLER EDIT ADDITION BEGIN - Adds Premium augment support to organs.
+ var/line = target_organ.examine_title(user)
+ if(target_organ.premium_component) // DOPPLER ADDITION
+ var/quality = round(target_organ.premium_component.quality)
+ var/quality_text = "quality: [quality]%"
+ quality_text = conditional_tooltip(quality_text, "Premium augment quality affects performance. At 0% it must be refurbished. \
+ Using premium augment maintenance surgery on the appropriate bodypart ([parse_zone(target_organ.zone)]) will restore up to 75% so long as it is not broken. \
+ Removing the augment with organ manipulation and refurbishing it in-hand will restore up to 100% (examine the augment for instructions).", tochat)
+ if(quality <= 0)
+ line = "[line] ([quality_text] refurbish required)"
+ else
+ line = "[line] ([quality_text])"
+ LAZYADD(cyberimps, line) // was: LAZYADD(cyberimps, target_organ.examine_title(user))
+ // DOPPLER EDIT ADDITION END
if(target_organ.organ_flags & ORGAN_MUTANT)
mutant = TRUE
if(LAZYLEN(cyberimps))
@@ -350,7 +363,6 @@
render_list += ""
render_list += "Detected cybernetic modifications: "
render_list += "[english_list(cyberimps, and_text = ", and ")] "
-
render_list += ""
//Genetic stability
diff --git a/code/game/objects/items/grenades/_grenade.dm b/code/game/objects/items/grenades/_grenade.dm
index 11e7f3683e281f..a138577c93b2b2 100644
--- a/code/game/objects/items/grenades/_grenade.dm
+++ b/code/game/objects/items/grenades/_grenade.dm
@@ -62,6 +62,10 @@
/obj/item/grenade/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_ODD_CUSTOMIZABLE_FOOD_INGREDIENT, type)
+ // DOPPLER EDIT ADDITION BEGIN - Display timers in hand for Explosive Specialist & timers on ground for Observers & Explosives Specialist
+ AddComponent(/datum/component/grenade_timer_hud)
+ AddComponent(/datum/component/grenade_timer_ground)
+ // DOPPLER EDIT ADDITION END
RegisterSignal(src, COMSIG_ITEM_USED_AS_INGREDIENT, PROC_REF(on_used_as_ingredient))
/obj/item/grenade/suicide_act(mob/living/carbon/user)
diff --git a/code/game/objects/items/grenades/chem_grenade.dm b/code/game/objects/items/grenades/chem_grenade.dm
index 98b7edf19ae2f6..1dfb6dee9a0826 100644
--- a/code/game/objects/items/grenades/chem_grenade.dm
+++ b/code/game/objects/items/grenades/chem_grenade.dm
@@ -257,6 +257,9 @@
to_chat(user, span_warning("You prime [src]! [DisplayTimeText(det_time)]!"))
active = TRUE
+ // DOPPLER EDIT ADDITION BEGIN Why do we not send this signal?! Used by explosives specialist and a dozen other things.
+ SEND_SIGNAL(src, COMSIG_GRENADE_ARMED, det_time, delayoverride)
+ // DOPPLER EDIT ADDITION END
update_icon_state()
playsound(src, grenade_arm_sound, volume, grenade_sound_vary)
if (landminemode)
diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm
index 90eaecb21249b9..a9dbee16542893 100644
--- a/code/modules/admin/topic.dm
+++ b/code/modules/admin/topic.dm
@@ -421,6 +421,9 @@
if(!target.client)
to_chat(usr, "[target] has no client!", confidential = TRUE)
return
+ // DOPPLER EDIT ADDITION BEGIN - Rolls adding powers into game panel.
+ SSpowers.assign_powers(target, target.client)
+ // DOPPLER EDIT ADDITION END
SSquirks.AssignQuirks(target, target.client)
log_admin("[key_name(usr)] applied client quirks to [key_name(target)].")
message_admins(span_adminnotice("[key_name_admin(usr)] applied client quirks to [key_name_admin(target)]."))
diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm
index 780142b85b7a24..a7adf558a55cb8 100644
--- a/code/modules/admin/verbs/admingame.dm
+++ b/code/modules/admin/verbs/admingame.dm
@@ -141,7 +141,9 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_ADMIN, "Show Player Panel", mo
body += " "
if(!isnewplayer(player))
body += "Forcesay | "
- body += "Apply Client Quirks | "
+ // DOPPLER EDIT BEGIN: Rolls powers into apply client quirks. - ORIGINAL : body += "Apply Client Quirks | "
+ body += "Apply Client Quirks & Powers | "
+ // DOPPLER EDIT END
body += "Thunderdome 1 | "
body += "Thunderdome 2 | "
body += "Thunderdome Admin | "
diff --git a/code/modules/bitrunning/server/obj_generation.dm b/code/modules/bitrunning/server/obj_generation.dm
index a73fc8e86a1fc3..5074b1806ec32a 100644
--- a/code/modules/bitrunning/server/obj_generation.dm
+++ b/code/modules/bitrunning/server/obj_generation.dm
@@ -170,6 +170,7 @@
if(!(domain_forbids_flags & DOMAIN_FORBIDS_ABILITIES))
avatar_preference.safe_transfer_prefs_to(avatar)
SSquirks.AssignQuirks(avatar, prefs_disk.mock_client)
+ SSpowers.assign_powers(avatar, prefs_disk.mock_client) // DOPPLER EDIT ADDITION - Assign powers to bitrunning mobs if allowed
if(!(domain_forbids_flags & DOMAIN_FORBIDS_ITEMS) && prefs_disk.include_loadout)
avatar.equip_outfit_and_loadout(/datum/outfit, avatar_preference)
diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm
index 8eef35b84219b0..93ebe8e76afe78 100644
--- a/code/modules/client/preferences_savefile.dm
+++ b/code/modules/client/preferences_savefile.dm
@@ -381,24 +381,16 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
languages = save_languages
alt_job_titles = save_data?["alt_job_titles"]
- var/list/save_powers = SANITIZE_LIST(save_data?["powers"])
-
- for(var/power in save_powers)
- var/value = save_powers[power]
- save_powers -= power
-
- if(istext(value))
- value = _text2path(value)
-
- save_powers[power] = value
-
- powers = save_powers
+ all_powers = save_data?["all_powers"]
// DOPPLER SHIFT ADDITION END
//try to fix any outdated data if necessary
//preference updating will handle saving the updated data for us.
if(SHOULD_UPDATE_DATA(data_validity_integer))
update_character(data_validity_integer, save_data)
+ // DOPPLER EDIT ADDITION BEGIN - Modular Character Savefile Migration
+ check_doppler_character_savefile(save_data)
+ // DOPPLER EDIT ADDITION END
//Sanitize
randomise = SANITIZE_LIST(randomise)
@@ -406,8 +398,8 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
all_quirks = SANITIZE_LIST(all_quirks)
// DOPPLER SHIFT ADDITION BEGIN
languages = SANITIZE_LIST(languages)
+ all_powers = SANITIZE_LIST(all_powers)
alt_job_titles = SANITIZE_LIST(alt_job_titles)
- powers = SANITIZE_LIST(powers)
// DOPPLER SHIFT ADDITION END
//Validate job prefs
@@ -462,9 +454,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car
//Quirks
save_data["all_quirks"] = all_quirks
- save_data["languages"] = languages // DOPPLER SHIFT ADDITION - we might want to migrate this
- save_data["alt_job_titles"] = alt_job_titles // DOPPLER SHIFT ADDITION: alt job titles
- save_data["powers"] = powers // dopplor powerz :3c
+ save_character_doppler(save_data) // DOPPLER ADDITION - Modular Savefile
return TRUE
diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm
index 2e501933dc5d6d..e17eb99ec562b0 100644
--- a/code/modules/mob/dead/new_player/new_player.dm
+++ b/code/modules/mob/dead/new_player/new_player.dm
@@ -245,6 +245,9 @@
if(job.job_flags & JOB_ASSIGN_QUIRKS)
if(CONFIG_GET(flag/roundstart_traits))
SSquirks.AssignQuirks(humanc, humanc.client)
+ // DOPPLER EDIT ADDITION BEGIN - Archetype Powers
+ SSpowers.assign_powers(humanc, humanc.client)
+ // DOPPLER EDIT ADDITION END
else // clear any personalities the prefs added since our job clearly does not want them
humanc.clear_personalities()
diff --git a/code/modules/mob/living/basic/basic_defense.dm b/code/modules/mob/living/basic/basic_defense.dm
index b4100a73cc1601..97d90fa57eb2c4 100644
--- a/code/modules/mob/living/basic/basic_defense.dm
+++ b/code/modules/mob/living/basic/basic_defense.dm
@@ -34,6 +34,10 @@
)
to_chat(user, span_danger("You [response_harm_simple] [src]!"))
playsound(loc, attacked_sound, 25, TRUE, -1)
+ /// DOPPLER EDIT ADDITION START - Sends unarmed hit signalers for unarmed powers such as martial artist, cultivator, etc.
+ var/limb_sharpness = active_arm?.unarmed_sharpness
+ SEND_SIGNAL(user, COMSIG_HUMAN_UNARMED_HIT, src, null, damage, 0, limb_sharpness)
+ /// DOPPLER EDIT ADDITION END
apply_damage(damage)
log_combat(user, src, "attacked")
updatehealth()
diff --git a/code/modules/mob/living/carbon/alien/adult/adult_defense.dm b/code/modules/mob/living/carbon/alien/adult/adult_defense.dm
index 3b65549d6d9c04..31c5bc1e7d993b 100644
--- a/code/modules/mob/living/carbon/alien/adult/adult_defense.dm
+++ b/code/modules/mob/living/carbon/alien/adult/adult_defense.dm
@@ -32,6 +32,11 @@
var/obj/item/bodypart/affecting = get_bodypart(get_random_valid_zone(user.zone_selected))
apply_damage(damage, BRUTE, affecting)
log_combat(user, src, "attacked")
+ /// DOPPLER EDIT ADDITION START - Sends unarmed hit signalers for unarmed powers such as martial artist, cultivator, etc.
+ var/obj/item/bodypart/arm/active_arm = user.get_active_hand()
+ var/limb_sharpness = active_arm?.unarmed_sharpness
+ SEND_SIGNAL(user, COMSIG_HUMAN_UNARMED_HIT, src, affecting, damage, 0, limb_sharpness)
+ /// DOPPLER EDIT ADDITION END
else
playsound(loc, 'sound/items/weapons/punchmiss.ogg', 25, TRUE, -1)
visible_message(span_danger("[user]'s punch misses [src]!"), \
diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm
index 9044899bf7787c..e2517b4e72fe00 100644
--- a/code/modules/mob/living/carbon/examine.dm
+++ b/code/modules/mob/living/carbon/examine.dm
@@ -605,6 +605,9 @@
. += "Criminal status: [wanted_status]"
. += "Important Notes: [security_note]"
. += "Security record: \[View\]"
+ // DOPPLER EDIT ADDITION BEGIN - Adds the ability for sechuds to see powers.
+ . += "\[Show powers\]"
+ // DOPPLER EDIT ADDITION END
if(ishuman(user))
. += "\[Add citation\]\
\[Add crime\]\
diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm
index 52eaf3066cbbc1..6ba40fd359adc4 100644
--- a/code/modules/mob/living/carbon/human/_species.dm
+++ b/code/modules/mob/living/carbon/human/_species.dm
@@ -912,6 +912,11 @@ GLOBAL_LIST_EMPTY(features_by_species)
var/attack_type = attacking_bodypart.attack_type
var/kicking = (atk_effect == ATTACK_EFFECT_KICK)
var/final_armor_block = armor_block
+
+ // DOPPLER EDIT ADDITION BEGIN - Adds a signaler for the power system so that we can track if we land punches.
+ SEND_SIGNAL(user, COMSIG_HUMAN_UNARMED_HIT, target, affecting, damage, armor_block, limb_sharpness)
+ // DOPPLER EDIT ADDITION END
+
if(kicking || grappled) //kicks and punches when grappling bypass armor slightly.
if(damage >= 9)
target.force_say()
diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm
index a127d1397fbff2..c2389d15b12b01 100644
--- a/code/modules/mob/living/carbon/human/human.dm
+++ b/code/modules/mob/living/carbon/human/human.dm
@@ -334,6 +334,43 @@
sec_record_message += "\nAdded by [crime.author] at [crime.time]"
to_chat(human_or_ghost_user, boxed_message(sec_record_message))
return
+ // DOPPLER EDIT ADDITION START - Lets SecHuds see powers from sec records.
+ if(href_list["powers"])
+ if(ishuman(human_or_ghost_user))
+ var/mob/living/carbon/human/human_user = human_or_ghost_user
+ if(!human_user.canUseHUD())
+ return
+ if(!HAS_TRAIT(human_or_ghost_user, TRAIT_SECURITY_HUD))
+ return
+ var/list/powers_major = splittext(target_record.power_notes_major || "", " ")
+ var/list/powers_minor = splittext(target_record.power_notes_minor || "", " ")
+ var/has_major = FALSE
+ var/has_minor = FALSE
+ for(var/entry in powers_major)
+ if(entry != "")
+ has_major = TRUE
+ break
+ for(var/entry in powers_minor)
+ if(entry != "")
+ has_minor = TRUE
+ break
+ if(has_major || has_minor)
+ if(has_major)
+ to_chat(human_or_ghost_user, "Detected high-threat powers:")
+ for(var/power_entry in powers_major)
+ if(power_entry == "")
+ continue
+ to_chat(human_or_ghost_user, "[power_entry]")
+ if(has_minor)
+ to_chat(human_or_ghost_user, "Detected powers:")
+ for(var/power_entry in powers_minor)
+ if(power_entry == "")
+ continue
+ to_chat(human_or_ghost_user, "[power_entry]")
+ else
+ to_chat(human_or_ghost_user, "No registered powers found.")
+ return
+ // DOPPLER EDIT ADDITION END.
if(ishuman(human_or_ghost_user))
var/mob/living/carbon/human/human_user = human_or_ghost_user
if(href_list["add_citation"])
diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm
index 46b96b7c42f001..e0598f17a43cf2 100644
--- a/code/modules/mob/living/living.dm
+++ b/code/modules/mob/living/living.dm
@@ -1391,9 +1391,12 @@
var/datum/dna/mob_DNA = has_dna()
if(!mob_DNA || !mob_DNA.check_mutation(/datum/mutation/telekinesis) || !tkMaxRangeCheck(src, target))
- if(!(action_bitflags & SILENT_ADJACENCY))
- to_chat(src, span_warning("You are too far away!"))
- return FALSE
+ // DOPPLER EDIT START - Adds a trait for Powers to bypass range checks. ORIGINAL: if(!(action_bitflags & SILENT_ADJACENCY)) - to_chat(src, span_warning("You are too far away!")) - return FALSE
+ if(!HAS_TRAIT(src, TRAIT_REMOTE_INTERACT)) // remove this line if reverting this modular edit + restore indentation
+ if(!(action_bitflags & SILENT_ADJACENCY))
+ to_chat(src, span_warning("You are too far away!"))
+ return FALSE
+ // DOPPLER EDIT END
if((action_bitflags & NEED_VENTCRAWL) && !HAS_TRAIT(src, TRAIT_VENTCRAWLER_NUDE) && !HAS_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS))
to_chat(src, span_warning("You wouldn't fit!"))
diff --git a/code/modules/mob/living/living_defense.dm b/code/modules/mob/living/living_defense.dm
index 6b228b5e456bc0..64ac169582c266 100644
--- a/code/modules/mob/living/living_defense.dm
+++ b/code/modules/mob/living/living_defense.dm
@@ -825,6 +825,9 @@
/mob/living/proc/check_block(atom/hit_by, damage, attack_text = "the attack", attack_type = MELEE_ATTACK, armour_penetration = 0, damage_type = BRUTE)
if(SEND_SIGNAL(src, COMSIG_LIVING_CHECK_BLOCK, hit_by, damage, attack_text, attack_type, armour_penetration, damage_type) & SUCCESSFUL_BLOCK)
+ // DOPPLER EDIT ADDITION BEGIN - Sends a signal if we succesfuly block.
+ SEND_SIGNAL(src, COMSIG_LIVING_SUCCESSFUL_BLOCK, hit_by, damage, attack_text, attack_type, armour_penetration, damage_type)
+ // DOPPLER EDIT ADDITION BEGIN
return SUCCESSFUL_BLOCK
return FAILED_BLOCK
diff --git a/code/modules/mob/living/simple_animal/animal_defense.dm b/code/modules/mob/living/simple_animal/animal_defense.dm
index 2c7bc4583e2cfe..54db4254c6e581 100644
--- a/code/modules/mob/living/simple_animal/animal_defense.dm
+++ b/code/modules/mob/living/simple_animal/animal_defense.dm
@@ -27,6 +27,11 @@
playsound(loc, attacked_sound, 25, TRUE, -1)
apply_damage(harm_intent_damage)
log_combat(user, src, "attacked")
+ /// DOPPLER EDIT ADDITION START - Sends unarmed hit signalers for unarmed powers such as martial artist, cultivator, etc.
+ var/obj/item/bodypart/arm/active_arm = user.get_active_hand()
+ var/limb_sharpness = active_arm?.unarmed_sharpness
+ SEND_SIGNAL(user, COMSIG_HUMAN_UNARMED_HIT, src, null, harm_intent_damage, 0, limb_sharpness)
+ // DOPPLER EDIT ADDITION END
return TRUE
/mob/living/simple_animal/get_shoving_message(mob/living/shover, obj/item/weapon, shove_flags)
diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm
index 9cb2545b69f2af..dd0f0fe354e91c 100644
--- a/code/modules/mob/mob.dm
+++ b/code/modules/mob/mob.dm
@@ -1135,6 +1135,10 @@
return TRUE
if (Adjacent(A))
return TRUE
+ // DOPPLER ADDITION BEGIN - Adds a trait for Powers to bypass range checks.
+ if(HAS_TRAIT(src, TRAIT_NO_UI_DISTANCE) && (A in view(src)))
+ return TRUE
+ // DOPPLER ADDITION END - Adds a trait for Powers to bypass range checks.
var/datum/dna/mob_dna = has_dna()
if(mob_dna?.check_mutation(/datum/mutation/telekinesis) && tkMaxRangeCheck(src, A))
return TRUE
diff --git a/code/modules/mob_spawn/mob_spawn.dm b/code/modules/mob_spawn/mob_spawn.dm
index 1c58f55f90624c..722c3bcfb9e141 100644
--- a/code/modules/mob_spawn/mob_spawn.dm
+++ b/code/modules/mob_spawn/mob_spawn.dm
@@ -112,6 +112,7 @@
else
spawned_human.equipOutfit(outfit)
SSquirks.AssignQuirks(spawned_human, spawned_human.client)
+ SSpowers.assign_powers(spawned_human, spawned_human.client) // DOPPLER EDIT ADDITION - Assign mobs their powers
else
spawned_human.equipOutfit(outfit)
else if(allow_prefs && spawned_mob.client)
@@ -120,6 +121,7 @@
if(allow_loadout)
spawned_human.equip_outfit_and_loadout(new /datum/outfit(), spawned_human.client?.prefs)
SSquirks.AssignQuirks(spawned_human, spawned_human.client)
+ SSpowers.assign_powers(spawned_human, spawned_human.client) // DOPPLER EDIT ADDITION - Assign mobs their powers
/// DOPPLER SHIFT ADDITION END
///these mob spawn subtypes do not trigger until attacked by a ghost.
diff --git a/code/modules/surgery/organs/_organ.dm b/code/modules/surgery/organs/_organ.dm
index 3b2edfea15d1f6..09b2a8cc621e1c 100644
--- a/code/modules/surgery/organs/_organ.dm
+++ b/code/modules/surgery/organs/_organ.dm
@@ -101,6 +101,11 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
if(cell_line && (organ_flags & ORGAN_ORGANIC))
AddElement(/datum/element/swabable, cell_line, cell_line_amount = rand(cells_minimum, cells_maximum))
+ // DOPPLER ADDITION START - Adds Premium augment support to organs.
+ if(premium && !premium_component)
+ premium_component = AddComponent(/datum/component/premium_augment)
+ // DOPPLER ADDITION END
+
START_PROCESSING(SSobj, src)
/obj/item/organ/Destroy()
@@ -196,6 +201,10 @@ INITIALIZE_IMMEDIATE(/obj/item/organ)
. = ..()
. += zones_tip()
+ // DOPPLER ADDITION START - Adds Premium augment support to organs.
+ if(premium_component)
+ . += premium_component.get_refurb_examine_lines(src)
+ // DOPPLER ADDITION END
if(HAS_MIND_TRAIT(user, TRAIT_ENTRAILS_READER) || isobserver(user))
if(HAS_TRAIT(src, TRAIT_CLIENT_STARTING_ORGAN))
diff --git a/code/modules/tgui/states.dm b/code/modules/tgui/states.dm
index a8fe0a46b4f600..b2e24e911d240e 100644
--- a/code/modules/tgui/states.dm
+++ b/code/modules/tgui/states.dm
@@ -125,4 +125,8 @@
/mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE, allow_tk = TRUE)
if(allow_tk && dna.check_mutation(/datum/mutation/telekinesis) && tkMaxRangeCheck(src, src_object))
return UI_INTERACTIVE
+ // DOPPLER ADDITION BEGIN - Support for powers to interact with objects from range with LoS
+ if(HAS_TRAIT(src, TRAIT_NO_UI_DISTANCE) && (src_object in view(src)))
+ return UI_INTERACTIVE
+ // DOPPLER ADDITION END - Support for powers to interact with objects from range with LoS
return ..()
diff --git a/modular_doppler/_HELPERS/preferences.dm b/modular_doppler/_HELPERS/preferences.dm
index 4cc10b17833892..82e0bf5e5ba242 100644
--- a/modular_doppler/_HELPERS/preferences.dm
+++ b/modular_doppler/_HELPERS/preferences.dm
@@ -1,37 +1,3 @@
-/// List of power prototypes to reference, assoc [type] = prototype
-GLOBAL_LIST_INIT_TYPED(power_datum_instances, /datum/power, init_power_prototypes())
-
-// list of power datums
-GLOBAL_LIST_INIT(all_powers, init_all_powers())
-
-/proc/init_power_prototypes()
-
- var/list/power_list = list()
-
- for(var/datum/power/power_type as anything in typesof(/datum/power))
- if(!initial(power_type.name))
- continue
- if(!power_type.is_accessible)
- continue
-
- power_list[power_type] = new power_type()
-
- return power_list
-
-/// List f all powers
-/proc/init_all_powers()
-
- var/list/powers_list = list()
-
- for(var/datum/power/power_type as anything in typesof(/datum/power))
- if(!initial(power_type.name))
- continue
- if(!power_type.is_accessible)
- continue
-
- powers_list += power_type
-
- return powers_list
/// List of all Brain Traumas for the Quirk. NOT every Trauma in the game
/// Many are left out because they're covered by other quirks or are strictly beneficial
diff --git a/modular_doppler/_savefile_migration/code/_preferences_savefile.dm b/modular_doppler/_savefile_migration/code/_preferences_savefile.dm
new file mode 100644
index 00000000000000..a302af65e5d591
--- /dev/null
+++ b/modular_doppler/_savefile_migration/code/_preferences_savefile.dm
@@ -0,0 +1,37 @@
+
+#define DOPPLER_SAVEFILE_VERSION_MAX 1
+
+#define VERSION_NEW_POWERS 1
+
+#define SHOULD_UPDATE_DOPPLER_DATA(version) (version < DOPPLER_SAVEFILE_VERSION_MAX)
+
+// Grabs the savefile
+/datum/preferences/proc/get_savefile_version(list/save_data)
+ var/savefile_version = save_data["doppler_version"]
+ return savefile_version
+
+//Checks if the savefile is older.
+/datum/preferences/proc/check_doppler_character_savefile(list/save_data)
+ if(isnull(save_data))
+ save_data = list()
+ var/current_version = get_savefile_version(save_data)
+ if(!SHOULD_UPDATE_DOPPLER_DATA(current_version))
+ return
+ update_character_doppler(current_version, save_data)
+
+/// Updates our character save data.
+/datum/preferences/proc/update_character_doppler(current_version, list/save_data)
+ // Version for old powers system
+ if(current_version < VERSION_NEW_POWERS)
+ nuke_old_powers(save_data)
+
+/datum/preferences/proc/save_character_doppler(list/save_data)
+ save_data["languages"] = languages
+ save_data["alt_job_titles"] = alt_job_titles
+ save_data["all_powers"] = all_powers
+ // Track Doppler-specific savefile version separately from core prefs.
+ save_data["doppler_version"] = DOPPLER_SAVEFILE_VERSION_MAX
+
+#undef DOPPLER_SAVEFILE_VERSION_MAX
+#undef VERSION_NEW_POWERS
+#undef SHOULD_UPDATE_DOPPLER_DATA
diff --git a/modular_doppler/_savefile_migration/code/powers_migration.dm b/modular_doppler/_savefile_migration/code/powers_migration.dm
new file mode 100644
index 00000000000000..5860a1bd2ccb64
--- /dev/null
+++ b/modular_doppler/_savefile_migration/code/powers_migration.dm
@@ -0,0 +1,9 @@
+
+/**
+ * Removes the old powers from people's savefiles
+ */
+/datum/preferences/proc/nuke_old_powers(list/save_data)
+ if(save_data && ("powers" in save_data))
+ save_data -= "powers"
+ var/ckey_to_log = parent?.ckey || "unknown"
+ log_game("[ckey_to_log]'s powers were migrated over from the old powers system.")
diff --git a/modular_doppler/flavortext_and_records/code/records.dm b/modular_doppler/flavortext_and_records/code/records.dm
index 91b28d30a2be5a..7dfacf6f5ac844 100644
--- a/modular_doppler/flavortext_and_records/code/records.dm
+++ b/modular_doppler/flavortext_and_records/code/records.dm
@@ -12,6 +12,13 @@
/// Self-written exploitables/vulnerable information intended for antagonists to make use of.
var/exploitable_records
+ ///Crew facing power notes
+ var/power_notes
+ /// Power notes that don't impact security's ability to do their job e.g being able to speak 15 languages
+ var/power_notes_minor
+ /// Power notes that do impact security's ability to do their job e.g being able to shoot laserbeams
+ var/power_notes_major
+
/// PREFERENCES
/datum/preference/numeric/age
diff --git a/modular_doppler/genemod_quirk/code/genemod_quirk.dm b/modular_doppler/genemod_quirk/code/genemod_quirk.dm
index 7ce81473fcb711..1c399a928e1a82 100644
--- a/modular_doppler/genemod_quirk/code/genemod_quirk.dm
+++ b/modular_doppler/genemod_quirk/code/genemod_quirk.dm
@@ -33,7 +33,7 @@
can_randomize = FALSE
/proc/generate_genemod_quirk_list()
- var/list/stuff_we_dont_want = list(/datum/mutation/self_amputation, /datum/mutation/hulk, /datum/mutation/clever, /datum/mutation/blind, /datum/mutation/thermal, /datum/mutation/telepathy, /datum/mutation/void, /datum/mutation/badblink, /datum/mutation/acidflesh)
+ var/list/stuff_we_dont_want = list(/datum/mutation/self_amputation, /datum/mutation/hulk, /datum/mutation/clever, /datum/mutation/blind, /datum/mutation/thermal, /datum/mutation/telepathy, /datum/mutation/telekinesis, /datum/mutation/void, /datum/mutation/badblink, /datum/mutation/acidflesh)
var/list/genemods = list()
for (var/datum/mutation/mut as anything in subtypesof(/datum/mutation))
diff --git a/modular_doppler/icspawn/observer_spawn.dm b/modular_doppler/icspawn/observer_spawn.dm
index 4f0948ec05b1d0..a93ff04adac48a 100644
--- a/modular_doppler/icspawn/observer_spawn.dm
+++ b/modular_doppler/icspawn/observer_spawn.dm
@@ -59,6 +59,7 @@
spawned_player.equipOutfit(dresscode)
if(addquirks == "Quirks & Loadout" || addquirks == "Quirks Only")
SSquirks.AssignQuirks(player_as_human, user.client)
+ SSpowers.assign_powers(player_as_human, user.client)
player_as_human.dna.update_dna_identity()
else if(dresscode != "Naked")
spawned_player.equipOutfit(dresscode)
diff --git a/modular_doppler/modular_customization/preferences/preferences.dm b/modular_doppler/modular_customization/preferences/preferences.dm
index 535da2c8640766..2a2584b35c2c01 100644
--- a/modular_doppler/modular_customization/preferences/preferences.dm
+++ b/modular_doppler/modular_customization/preferences/preferences.dm
@@ -6,9 +6,6 @@
/// Associative list, keyed by language typepath, pointing to list(percent_understood, (LANGUAGE_UNDERSTOOD, or LANGUAGE_SPOKEN, for whether we understand or speak the language))
var/list/languages = list()
- /// Associative list containing all powers, pointing to their respective cost
- var/list/powers = list()
-
// Updates the mob's chat color in the global cache
/datum/preferences/safe_transfer_prefs_to(mob/living/carbon/human/character, icon_updates = TRUE, is_antag = FALSE)
. = ..()
diff --git a/modular_doppler/modular_customization/preferences/preferences_setup.dm b/modular_doppler/modular_customization/preferences/preferences_setup.dm
index d54c36cd35836f..738978fc88d042 100644
--- a/modular_doppler/modular_customization/preferences/preferences_setup.dm
+++ b/modular_doppler/modular_customization/preferences/preferences_setup.dm
@@ -44,5 +44,14 @@
continue
mannequin.add_quirk(quirk_type, parent)
+ // Apply visual powers too. Same logic applies.
+ if(SSpowers?.initialized)
+ mannequin.cleanse_power_datums()
+ for(var/power_name as anything in all_powers)
+ var/datum/power/power_type = SSpowers.powers[power_name]
+ if(!(initial(power_type.power_flags) & POWER_CHANGES_APPEARANCE))
+ continue
+ mannequin.add_archetype_power(power_type, parent)
+
mannequin.update_body()
return mannequin.appearance
diff --git a/modular_doppler/modular_powers/HowToAddPowers.md b/modular_doppler/modular_powers/HowToAddPowers.md
new file mode 100644
index 00000000000000..fda52986bb04c7
--- /dev/null
+++ b/modular_doppler/modular_powers/HowToAddPowers.md
@@ -0,0 +1,563 @@
+Powers Design Document
+
+Explaining the system architecture of the Powers System and how to contribute to it
+
+Written by Creeper Joe / @creeperjoe on Discord
+
+Last updated: 26/06/26
+
+Version: V1.0
+
+# Quick Notes (for the lazy)
+
+Disclaimer: If this is your first time working with the powers system, read the full document first. This section exists as a quick-reference sheet for returning contributors.
+Disclaimer 2: Any written document may with time become outdated. Check the last-updated tag at the top: if its been a few years, take it with a grain of salt.
+
+- Vars in this document are written in _italics_. Functions are referred to by ending in ().
+
+- Defines can be found [here](../../code/__DEFINES/~doppler_defines/powers.dm) and the global lists/vars for Powers can be found [here](../../code/_globalvars/~doppler_globalvars/powers.dm).
+
+- Just look at how other powers do things; there are so many examples to draw from.
+
+- For power datums, the main functions you want to usually use are add(), post_add() and remove().
+ - Always make sure to set _name_, _desc_, _value_, _security_record_text_ and _required_powers_ (if any).
+ - Make sure with _security_record_threat_ to set POWER_THREAT_MAJOR if it gets in the way of security doing their job.
+
+- For action datums, the main functions you want to use are use_action() for using the action and can_use() for validation.
+ - Use _active_ when there’s an active state to the power, to help communicate to other powers.
+ - Use _click_to_activate_ = TRUE to use click-based targeting.
+ - If you use projectiles, fire_projectile() is set up to handle almost all projectile firing mechanics. Make sure to set \_anti_magic_on_target\* to FALSE so that it will only check on impact, and that \_click_to_activate\* is TRUE so that you actually give targets.
+ - If you want to have cast/use times, use the _use_time_ var, and optionally _use_time_flags_ to set the conditions for interruption. You can further tweak the properties of the cast time with do_use_time().
+ - There are a lot of other niche vars in /datum/action/cooldown/power; it is recommended to look at them to see if you can use them.
+
+- Use status effects for any and all lingering effects on mobs.
+
+- **Read the appropriate path notes for **[**Path-Specific Notes on Adding Powers**](#anchor-10), some things may radically differ per path.
+
+# Technical Notes on Adding Powers
+
+First of all, you’ll have to possess some rudimentary understanding of code. The systems are set-up in a way that they are easily understandable, but to make things ‘work’ you’ll need to both be able to understand what you want to make and how to accomplish it.
+
+Understanding _how_ you want to accomplish it is usually the first difficult step in the process. To do this you usually need to interact with at least 1 of these 3 datums, and sometimes multiples.
+
+- The power datum. This is what defines the cost, name, description and what archetype it belongs to. Make sure it inherits the appropriate parent type. All passive powers will usually make the most use of this datum.
+- The action datum. If your power has any actively used component that does not integrate into existing systems, you’ll want one of this. This adds an action-button that people can manipulate, and comes with a lot of support-tools for various powers to wield.
+- Status effect datums. Any lingering effect on a mob should use these: it is a diegetic way to handle lingering effects, with a lot of useful helper functions such as durations, visible timers and UI alerts.
+
+Make sure you keep your code compartmental. Not every power needs an action datum, but if it has one, you should make sure everything to do with the action stays inside the action, rather than trying to criss-cross communicate between the power and the action. This applies to status effects too; minimize the need to communicate.
+
+Below are several sections going in-depth on the important information per datum.
+
+## Power Datum
+
+The Power Datum is what registers the power in the system. The name and description is what shows up in prefs and the value is how much it costs. These are largely universal and don’t differ a great deal, but make sure you have the correct parent so that you inherit all the relevant traits.
+
+When it comes to adding functionality to powers, most of it will make use of the add(), post_add(), add_unique() and remove() functions to call their own procs, signallers, etc. It does not have many helpers; most of these are in the action datum, so you usually want to keep passive-only powers delegated to this.
+
+```dm
+/datum/power/aberrant/miasmic_conversion
+ name = "Miasmic Conversion"
+ desc = "Your body mends itself disturbingly well, but creates toxic backlash in your system. You passively convert 1 brute or burn damage per second to toxins damage, at a 90% ratio. You also passively heal a tiny amount of toxins damage per second."
+ security_record_text = "Subject extremely rapidly regenerates, but experiences toxic backlash when they do."
+ value = 4
+ power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES
+ required_powers = list(/datum/power/aberrant_root/monstrous)
+ /// how much we passively heal tox
+ var/passive_tox_healing = 0.05
+ /// how much we heal/convert per second
+ var/healing = 1
+ /// the ratio at which we convert.
+ var/conversion_rate = 0.90
+```
+
+Example of a Power Datum, set up to passively heal damage (`Miasmic Conversion`).
+
+The difference between the various add functions is important. add() and post_add() differ in when it resolves. Some powers may require the mob to be fully initialized first before we can safely set values, for which post_add() is useful, as it guarantees we have a fully functional mob. Otherwise, you should default to add(). add_unique() works similar to post_add() but only functions when \_power_transfer\* (indicating that the power is being transferred over from another source) is not true, and is meant for things that should occur only once, such as spawning items.
+
+The description of powers supports newlines using \n; it is recommended you use these for formatting to prevent word-slop.
+
+### Variables & Functions
+
+Check /datum/power for important variables. The most common ones you’ll use are _name_ and _desc_ (self-explanatory), _value_ (the cost), _required_powers_ (prerequisites), _action_path_ (the reference to the action datum, if any), and _security_record_text_ (the text that appears in security records).
+
+action_path particularly deserves a unique mention. If you have an activate-able ability that warrants its own button, you want to make an action datum and define the full type-path of the action inside here. Usually, you have the power- and action datums in the same file for ease of access. More on action datums is elaborated in the [Action Datum section](#anchor-4).
+
+_power_flags_ is another useful tool, specifically allowing you to pass POWER_PROCESSES as an argument to make the power start processing, performing the process() function every tick.
+
+There are several sub-types of variables to do with power prerequisites.
+
+- _required_powers_ as mentioned determines which powers are required to select it. This a list, so you can have multiple power requirements.
+- _required_allow_subtypes_, allowing any with that typepath to count rather than an exact match. This is most commonly used for having the prerequisite of having a path’s roots but not a specific root.
+- required_allow_any, which changes it so that you only need to fullfill the prerequisites of one power to qualify. This stacks with required_allow_subtypes.
+
+For security records the following variables and functions are much more relevant:
+
+- _security_record_text,_ which is the message that gets appended to security records for powers. This **should** always start with “Subject (...)”, and should refer to the person as subject and with gender-neutral terms (for the clinical writing style).
+- _security_record_threat_, which is either POWER_THREAT_MINOR or POWER_THREAT_MAJOR. This determines if it is in bold on the record and red on SecHUDs. If it can get in the way of Security in any way of performing their duties, it should be major. Any teleportation, combat abilities (offensive or defensive), hidden storage spaces and any other ability capable of thwarting them. Always lean towards major if in doubt, as it is used to help security assess risk in a pinch.
+- include_in_security_records, which determines if it shows up in the records at all. This should **normally** always be true and should not be modified.
+- get_security_record_text(). This is the function that determines what gets contributed to the security records. If you take the omnilingual power, you can see it overrides it to give a custom result: this is usually how you want to pass custom powers.
+
+There is also species blacklists, for which the following variables are relevant.
+
+- species_blacklist. This is a list that blocks any and all listed species from selecting it, including subtypes. Preferably you want to only use this if this causes bugs and gameplay issues that cannot be remedied, such as holosynths being unable to use the Shapechange power because of their nature of being tied to a holopen. Avoid using species restrictions for purely balance reasons, as this leads to major player dissatisfaction.
+- species_blacklist_is_whitelist. This inverses the above blacklist to become a whitelist, meaning only the listed species are allowed to choose the power.
+
+Note that with any and all validation (blacklists and required species), it is only checked on the preferences and character creation. Adding incompatible powers through admin-powers is still possible, at the admin’s own risk.
+
+## Action Datum
+
+The Action Datum is an optional but very commonly used part of powers. This adds an action-icon on the power owner’s UI, allowing them to manipulate the power at their leisure and call it using hotkeys or the press of the button. The system is largely stylized after Changeling’s action system, so if you are familiar with that, you should feel at home.
+
+```dm
+/datum/action/cooldown/power/thaumaturge/gale_blast
+ name = "Gale Blast"
+ desc = "Shoots forth a blast of wind. The blast keeps traveling until it hits a solid structure, extinguishing any fires and dragging along any items with it. If it hits a creature, it knocks them back 3 spaces and extinguishes them."
+ button_icon = 'icons/effects/effects.dmi'
+ button_icon_state = "smoke"
+ required_affinity = 3
+ prep_cost = 3
+ click_to_activate = TRUE
+ anti_magic_on_target = FALSE
+
+/datum/action/cooldown/power/thaumaturge/gale_blast/use_action(mob/living/user, atom/target)
+ if(fire_projectile(user, target, /obj/projectile/resonant/gale_blast))
+ playsound(user, 'sound/effects/podwoosh.ogg', 60, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+ return FALSE
+```
+
+Example of an Action Datum that shoots a projectile (`Gale Blast`).
+
+### The Pipeline
+
+Actions traverse a pipeline from the moment they are pressed to resolution. There are various stages to activation that you may want to override, intercept or tweak to match the expectations of your power. Most of this pipeline is visible in /datum/action/cooldown/power but uses some parent functions as well. Variables and functions mentioned here can be found in the [Variables & Functions section](#anchor-6) that follows this section.
+
+- Pressing the button starts with trigger(). This is native to actions and immediately passes it to activate(), which will then set the user and pass it into our own eco system by calling try_use(). Once try_use() resolves, the action goes on cooldown.
+ - If _click_to_activate_ is TRUE, this starting pipeline behaves differently right up until try_use(). See the added list after this one for details.
+
+- try_use() is the whole powers-specific pipeline. It starts by checking against can_use()
+
+- can_use() checks against any prerequisite checks, such as silencing, incapacitation, etc. If your action has powers that need validating, such as needing a tail for tail swipe, it should be done in can_use(), before the power actually starts resolving.
+
+- Once can_use() is successfully resolved, we pass onto anti magic checks. If the action is resonant and \_anti_magic_on_target\* is true, this runs can_block_resonance() against the target. If the target can block it (either from resonance or magic immunity), it returns a false, and the pipeline will fail.
+
+- Once the anti-magic checks are resolved, we pass onto do_use_time(). If we have an \_use_time\* specified, it will attempt a do_after() and return a TRUE if uninterrupted. A FALSE stops the actions.
+
+- After this, we successfully reach use_action(). We send a signal for COMSIG_POWER_ACTION_USED, which is useful for powers to intercept powers before they reach their use_action(). use_action() then resolves depending on the specifications of the power. If it returns TRUE, the action is considered a success. If FALSE, it its considered a failure.
+
+- If use_action() returned TRUE, we send the signal for COMSIG_POWER_ACTION_SUCCESS, which again powers can use. After which we run the on_action_success() function if use_action() returned TRUE. This is largely where most costs and resource mechanics are handled.
+
+- Regardless of the outcome of on_action_success(), if use_action() returned TRUE, our pipeline will return a TRUE and confirm that our power was successfully handled. This will communicate back to the base action that the cooldown should start.
+
+This is the step-by-step pipeline that every action follows. As mentioned however, _click_to_activate_ powers behave differently.
+
+- Rather than passing off immediately after triggering, it is passed on to InterceptClickOn(). We override the whole parent function with a powers specific one.
+ - Once a mob clicks while InterceptClickOn() is called, it will route through that part of the pipeline with the clicked target as the target. InterceptClickOn() will continue overriding click behaviour until it returns TRUE.
+
+- First it attempts to perform aim_assist() if \_aim_assist\* is TRUE. It will as explained earlier attempt to lock onto the earliest viable/preferred target. aim_assist() returns a target, and will override the existing click target if it does.
+
+- It will then perform a large variety of targeting based checks.
+ - It checks if the _target_type_ matches the target’s type. This is an istype() call.
+ - After which it checks if the mob is allowed to _target_self,_ returning FALSE if not.
+ - This is followed by a range check if _target_range_ is specified and if so, if the given target is within range.
+
+- Finally, after this, it reroutes back to try_use(), and the pipeline continues as normal. However, after resolution, it will manually set the cooldown in InterceptClickOn(), unset the crosshair and adds a cooldown to the user’s clicker prevent spam-clicks.
+
+Though some smaller steps may be omitted, this is the broad-strokes of both pipelines. Don’t try to reinvent the wheel: step inside of it harmoniously. This was all made so that you don’t need to worry much about the systems outside of the scope of your power.
+
+### Variables & Functions
+
+Just like the power datum, the action datum has some general information variables: _name_, _desc_, _button_icon_ and _button_icon_state_. Name and description should largely match, though with description it is advised to trim the information down to just “what happens if you press the button” and keep the power datum description the longer one. _button_icon_ and _button_icon_state_ are the icon and the icon-state respectively that appear on the button. Pick something funny; reuse sprites, get creative.
+
+**Again, you want to make sure you subtype to the correct parent**. A lot of path-specific mechanics are handled in the actions section of powers, as well as various helpers.
+
+For actions, most of the meat of functionality exists in can_use(), use_action() and on_action_success().
+
+- can_use() handles almost all validation. If the target is cuffed, incapacitated, silenced, etc. Any and all validation should be added to this, before reaching use_action. Returns FALSE if anything is wrong and the power should be stopped, return TRUE if its a-okay to continue. Keeping all your validation in here keeps requirements in one tidy location and prevents signallers and powers from firing when they shouldn’t.
+- use_action() is where most of the functionality of the power should resolve. Return TRUE if the power successfully resolves, return FALSE if not. Assume that all validation has already succeeded at this point that is not intricately part of the functionalities of use_action() itself.
+- on_action_success() resolves after use_action is successful and is mainly used for mechanics such as resource management & returns. Mostly used by specific paths but some powers use it themselves. Return value does not matter. The separation from use_action() is there so we can handle action failures without messing up costs.
+
+There are a lot more variables at play in the action datum. The important ones are as followed:
+
+- active, which is typically used to indicate the TRUE/FALSE (on/off) state of a power. Though not every power will use this, it is important you use this when you do. This is largely so other powers can ascertain if others are active, such as Psyker’s Meditate.
+
+- resonant, which is the setting-specific term for magical. Specifically it is often used to refer to the tier of magic unique to the powers system, which sits below standard ‘magic’, a la wizard, heretic, cult, etc. Setting this to TRUE makes it check against anti-magic on the target and checks if you are silenced before use.
+ - Most Paths set this in their parent version of the action. For most paths you will not need to adjust it, but non-magical powers should have this as FALSE.
+
+- req_stat, determines which state you need to be in to use it. Powers by default require you be conscious, but if you want it to be useable while dead, you’ll have to change this to DEAD.
+ - disabled_by_incapacitate full fills a similar role but checks against stuns, since req_stat doesn’t compare against that.
+ - human_only makes it so that only carbon mobs can use the power.
+ - need_hands_free makes it so you can’t use it while bound.
+
+Action powers offer an use_time system, allowing you to have ‘cast times’ and a built-in do-after before using a power.
+
+- use_time dictates this. If it is 0, it uses the power instantly. Otherwise, it routes through do_use_time() to attempt to perform the action.
+- use_time_flags determines if the action is not interrupted by certain behaviour, e.g IGNORE_HELD_ITEM causes it to not stop when interacting with a held object.
+- use_time_overlay_type allows passing an overlay effect (such as /obj/effect/temp_visual/conjure_rain) to display while using.
+- do_use_time() is the function that handles most of \_use_time\*’s functionality, performing a do_after(). It is wise to override if you want specific things to occur during cast time, such as telegraphs (e.g conjure rain). A FALSE return means the power fails and stops, a TRUE will cause it to keep going.
+
+Targeting goes off of a singular target which is handed along through the entire action pipeline. How people acquire the target depends on the following variables.
+
+- click_to_activate, which dictates targeting behaviour. If TRUE, you are given an aiming reticule and made to target a mob before the action activates, and alters the pipeline to use a targeting mode. Activate again to disable. If FALSE, it will default to you being the target and resolves it instantly on button-press.
+- target_range, which is the maximum range the target has. This is measured based on a get_dist() check between the user and the target’s turf. Since get_dist() only moves horizontally and vertically, this means you have less range when aiming diagonally (meaning the range is more of a sphere than a cube)
+- target_type, which restricts the target to be a specific subtype. This is handy if you want to target mobs or a specific type of item. If specified, it will ignore any and all other objects unless clicking on that target (or a space with one if you have aim_assist = TRUE). Use this whenever possible rather than manual istype() checks.
+- target_self, which determines if the user is allowed to target themselves with their power.
+- aim_assist. Any gamer may revolt at the term, but if your target is not a valid target for target_type, it will search that turf for another valid target instead. This defaults to targeting the specified target_type first, then mobs, then anything else. Keep on TRUE unless it’s something very weird and finicky, as it makes powers significantly more forgiving and causes less clunkiness in use.
+- anti_magic_on_target. This determines if your target has its antimagic resistances checked, which also requires resonant to be TRUE. You shouldn’t need to tweak this unless your power creates an object, such as projectiles, that is responsible for most interactions instead.
+- magic_resistance_types. You can pass any of the anti-magic flags through here and give powers additional magic types to check against. Since we check against normal magic by default, this basically means just mental and holy resistance. Some powers, like Hemomancy, override this for all powers.
+
+Though that wraps up most variables, there are a multitude of functions aimed at helping common actions, such as applying damage against armour and firing projectiles resolve naturally without needing to reinvent it each time.
+
+- apply_damage_with_armor() is a handler that allows applying damage to mobs whilst respecting armour variables. Any form of damage should route through this, as it introduces counterplay and keeps in harmony with the existing game systems.
+
+- fire_projectile() fires a projectile from the first argument’s position towards the second argument’s position, with the third argument being the projectile type. Though this usually assumes the same to be the action’s \_target\* var, this can be anything, allowing you to create complex patterns such as shotgun-blasts. You should use this with projectiles; and any magical projectiles you create should be /obj/projectile/resonant as to aid in anti-magic functionality.
+ - These come with the on_power_projectile_hit() signaller, allowing you to easily communicate back to the power.
+
+## Status Effect Datum
+
+Whilst this isn’t a part of the powers system, its usefulness to the system comes up so often that it is deserving of it’s own section. Status effects allow us to have lingering effects with a large degree of functionality on mobs to offload some of our core functionalities on for powers. Status effects for powers use the /datum/status_effect/power parent-type, which you should aim to use when possible.
+
+Generally speaking, if your power has any form of lingering effect that sticks to a mob, it should use a status effect. It is also an ideal target for dispel functions, should your power require them.
+
+```dm
+/datum/status_effect/power/command_grit
+ id = "command_grit"
+ show_duration = TRUE
+ duration = 15 SECONDS // baseline
+ tick_interval = -1
+ alert_type = /atom/movable/screen/alert/status_effect/command_grit
+
+/datum/status_effect/power/command_grit/on_creation(mob/living/new_owner, commander_modifier)
+ if(isnum(commander_modifier))
+ duration = 15 SECONDS * commander_modifier
+ . = ..()
+
+/datum/status_effect/power/command_grit/on_apply()
+ ADD_TRAIT(owner, TRAIT_ANALGESIA, type)
+ owner.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/damage_slowdown)
+ owner.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/basic_stamina_slowdown)
+ return TRUE
+
+/datum/status_effect/power/command_grit/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_ANALGESIA, type)
+ owner.remove_movespeed_mod_immunities(src, /datum/movespeed_modifier/damage_slowdown)
+ owner.remove_movespeed_mod_immunities(src, /datum/movespeed_modifier/basic_stamina_slowdown)
+ return
+
+/atom/movable/screen/alert/status_effect/command_grit
+ name = "Grit"
+ desc = "You ignore pain for a duration, including the slowdowns from damage and stamina!"
+ icon = 'icons/hud/guardian.dmi'
+ icon_state = "standard"
+```
+
+Example of a status effect, including the alert at the bottom (`Command: Grit`).
+
+There are 3 primary functions you’ll interact with with status effects.
+
+- on_creation(), which fires when the status effect is first created on a mob. Arguments and other information should be passed along from the power and given assignments here.
+- tick(), which fires depending on the specified _tick_interval_, repeating a certain effect. This is a form of processing/on_life, and is ideally used for powers that have an ongoing effect such as a lingering damage-over-time.
+- on_remove() fires when the power expires or is otherwise deleted. This step is particularly useful for communicating back to the power, such as a power expiring.
+
+Status effects have a few important variables.
+
+- id, which is used to check against duplicates of itself and for referencing purposes.
+- duration, which determines how long the status effects lasts until it terminates. If set to -1, it is permanent (use STATUS_EFFECT_PERMANENT instead of -1). This duration can be passed along and over-ridden, as status effects allow for arbitrary arguments. This is best done on_creation. For example, how Command: Assault uses it.
+
+```dm
+/datum/status_effect/power/command_assault/on_creation(mob/living/new_owner, commander_modifier, vulnerable_amount, effect_duration)
+ if(isnum(commander_modifier))
+ duration = effect_duration * commander_modifier
+ if(isnum(vulnerable_amount))
+ damage_increase_percent = vulnerable_amount
+ . = ..()
+```
+
+- tick_interval, which determines when the tick() function should fire. Because of BYOND being funky and having a variable amount of ticks per second, putting in low values (below 1 SECOND) may lead to inconsistent ticks. Setting this to -1 will prevent ticks. If the duration is also set to -1 (permanent), this will prevent any processing, which is good for optimization.
+
+- _status_type_ determines how stacking behaves. Which to use determines on the nature of your power, but you usually do not want stacking for balance-reasons.
+ - STATUS_EFFECT_MULTIPLE allows the effect to stack with itself at infinitum.
+ - STATUS_EFFECT_UNIQUE will only allow 1 instance of it to exist on the mob and will prevent ‘younger’ statuses of the same id from being applied.
+ - STATUS_EFFECT_REPLACE will only allow 1 instance of itself to exist on the mob, but will replace the oldest status with the youngest when reapplied.
+ - STATUS_EFFECT_REFRESH functions as STATUS_EFFECT_UNIQUE but instead resets the duration rather than generating a new instance.
+
+- alert_type shows an alert on the user’s UI. This is a type of /atom/movable/screen/alert/status_effect, and will show a name, description (when hovered over) and icon. These have to be configured and made separately, preferably in the same file as the status effect.
+
+- show_duration shows a timer on the status effect (if it has an alert_type) until when it expires. It is a useful piece of player feedback to give, and should be given to any power that naturally expires after a period.
+
+## Anti-Magic, Silencing & Dispelling
+
+The Powers System adds a new tier of magic to the existing anti-magic system, the ability for mobs to be silenced and unable to use resonant powers, and a dispel functionality to remove existing powers. Most of these are findable in modular_doppler\modular_powers\code\powers_antimagic.dm, and are used for people to be able to combat the strength often provided by magical and supernatural powers.
+
+- Resonant is a new tier of anti-magic introduced with the system. It is largely used to check against immunities to resonant powers using can_block_resonance() function. Existing anti-magic (such as the null rod) applies to it as normal, and certain powers like Psyker’s are susceptible to alternative anti-magics like mental. Some powers grant immunity to specifically resonant magic, which does not apply to other types of magic. Any power with an action datum and magical/supernatural flavouring should have the resonant trait.
+ - Scrying immunity also exists, except is delegated to a mob trait. Resonant and magic immunity also apply to it. Anything that gives information through magical means should check against the target’s scrying resistances.
+
+- Silencing is a mob trait (TRAIT_RESONANCE_SILENCED) that can be given under various circumstances, and is meant to represent people being unable to exercise their magical abilities. Powers marked as resonant (as well as some non-action datum powers) will fail to activate if the user is silenced. Most powers that are magical should aim to integrate this in some capacity; even passive powers that are magical/supernatural in nature should have an if statement that checks against this.
+
+- Dispelling ends any lingering magic effects on the target. This uses a signal (COMSIG_ATOM_DISPEL) which most lingering powers listen to. Any lingering resonant powers should always have listeners for this. Powers, spells and items then send the dispel signal to the target, and any listeners will immediately attempt to dispel themselves, returning the result to the dispeller. The effects of a dispel have to be programmed in; be humorous when you can, and the through line is that dispels should yield instantaneous results. You are allowed to diverge if this would be too detrimental to balance, such as Cultivators, whose power-design is very much ‘all eggs in one basket’.
+ - There is an optional flag for dispelling called DISPEL_CASCADE_CARRIED, which dispels are carried items on the target mob. Normally, dispels do not check components of a target for performance reason.
+
+## Helper Functions
+
+There’s a few generic helper functions you can call for various purposes, to do with the powers system. Most of these exist in mob/living, allowing you to ascertain if a mob has a certain power or a certain path of power.
+
+- add_archetype_power() allows adding powers to mobs, and is generally speaking the best way to call it on mobs.
+ - power_type argument determines the power. This has to be the typepath of the power
+ - client_source is optional, additional info that includes the mobs prefs. This is mainly used for powers with specific preference options like which arm an augment should go on
+ - add_unique is a TRUE/FALSE statement that determines if a power runs their add_unique() call (e.g spawning items).
+
+- remove_archetype_power() does as it says on the tin and removes the specified power from the mob. power_type is the power to remove from the target mob.
+
+- has_archetype_power() returns a TRUE if the given power is on the mob, otherwise FALSE. Accepts power_type as its argument, in the form of the power’s typepath.
+
+- has_power_in_path() returns TRUE if the mob has any power that belongs to the specified power path, for example if you want to know if someone has any thaumaturgic powers.
+ - This accepts the POWER_PATH_X defines, so it’d look something like POWER_PATH_THAUMATURGE.
+
+- get_power() returns a specific instance of a power on a mob if they have it. It takes a power’s typepath as its argument.
+
+- get_power_string() returns a printable string of ALL the powers the mob has into one joined string.
+ - security is a TRUE/FALSE argument. When TRUE it returns the security-specific records. If FALSE, it returns only the power names.
+ - category determines what categories should be passed along. This is based off of _security_record_threat_, so whether a power is a minor or major threat, regardless if the security argument is TRUE or FALSE
+ - include_empty_text determines what happens if there are no powers to return. TRUE means it returns “No powers declared” if Security is TRUE or “None” if Security is FALSE. FALSE on include_empty_text will instead send an empty string.
+
+- transfer_power_datums() lets you transfer all powers from one mob to another. This transfers from the mob that you call the proc on, to the mob specified in the argument.
+
+# Path-Specific Notes on Adding Powers
+
+Paths radically differ in some cases when it comes to how they function, whether for flavour or technical reasons. This section tries to illustrate all these differences, both in design and technical sense.
+
+This section won’t go too in-depth in all the variables for each path, but instead tries to “keep it quick” with all the practical information and largely focusing on the design philosophy.
+
+## Thaumaturge
+
+Mages! Wizards!
+
+- Thaumaturge uses components (modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\\thaumaturge_component.dm) to handle most of its mechanics due to the general complexity of their powers.
+
+- Thaumaturge at its core does not use cooldowns; all of the powers are designed to use alternative resource systems, with the system being determined by the root. Spell preparation uses a mana system, Hemomancy uses a blood system.
+ - Spell Preparation ‘prepares’ a set amount of charges per power that the user may allocate per power, up to 6 max. These cost mana: the thaumaturge has mana equal to 2x the amount of power points invested in the system, and preparing a charge costs the _prep_cost_ of that power; usually the same as its _value_
+ - Hemomancy spends 4x the power’s _prep_cost_ to cast the spell, with your blood as cost. They need to channel a blood hand that takes up a slot to use their powers, and do not benefit from affinity. If exceeding 120% of their blood, they pay the cost multiple times to increase the affinity by +1, up to a maximum of Affinity 5. This is done to prevent exploitative behaviour of filling yourself up to 180% of your blood threshold and having virtually no downsides to using abilities, whilst still giving a practical use to excess amounts of blood.
+
+- Thaumaturges have an affinity system, where items give bonuses to spellcasting. This takes the highest value out of all of them; so carrying Affinity 4 robes and an Affinity 3 hat will give you Affinity 4. Items have to be used as intended; clothes have to be worn and other items have to be held. Full details on what determines what affinity, and the list of affected items can be found at modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_affinity.dm
+ - Hemomancers cannot benefit from item Affinity, but their hand gives them Affinity 3, 4 if a Hemophage.
+
+- _prep_cost_ should almost always be tied to the power’s _value_. Value plays a closer role in balance than with other powers, as it determines how ‘spammable’ the ability is for all roots.
+
+- Costs are handled in on_action_success(). Some powers have a refund chance, based on the \_power_refund_chance\* and \_power_refund_affinity_bonus\* vars. These are handled in that same function.
+
+## Enigmatist
+
+They’re vandalizing everything with their damn circles!
+
+- Isn’t in yet, but it is slated to be based around Runes, a hybrid between Heretic and Cultist, involving a large degree of exploration.
+- Expect it to be about half as big as the entire original powers system.
+- See Future Development
+
+## Theologist
+
+The faithful, not specifically tied to god.
+
+- Theologist uses a central component (/datum/component/theologist_piety) for handling their resource, including the UI element.
+- Theologist is based around piety generation; a resource they build up doing various deeds. Though most roots offer a form of healing to acquire it, some alternative powers such as flagellant allow building it through other means. The piety generation for healing uses THEOLOGIST_PIETY_HEALING_COEFFICIENT (which as of writing is 5 healing = 1 piety). Any other healing should go off of your gut-feeling.
+- Piety is a resource that is either hoarded or spend depending on powers and playstyle. There is no through line, but try to have most powers cost in increments of 5, as the power caps out at 50.
+- Some Theologist powers use the* unholy_mobs *global list. It is recommended to try and incorporate this and grant increased effects against those creatures, for flavour-sake when possible. Likewise, involve the Chaplain and their religion system when possible.
+- Flavour-wise, Theologist shouldn’t suggest being directly related to religions or gods. It is designed flavour wise to be open-ended and be more about strong convictions, e.g being a zealot. This can mean you have a strong philosophical believe, rather than needing to revere a god.
+
+## Psyker
+
+Psychic powers at often grave risk.
+
+- Psyker uses a new organ (modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_organs\\psyker_organ.dm) to handle its stress resource as well as backlash mechanics.
+
+- Stress is a resource that builds up for using abilities. Psyker has near-zero cooldowns and favours powers with indefinite durations to passively build up more manageable doses of stress. Stress decays passively depending on your root organ. Exceeding the first threshold hampers recovery and causes negative effect; further thresholds lead to heavier consequences until an eventual catastrophic stress event triggers, causing permanent negative effects for the Psyker and resting stress.
+ - Adding new events to psyker stress thresholds is relatively easy encouraged, as each are their own compartmentalized datum that gets called by the appropriate organ when a breakdown does happen.
+ - Minor events should be small but subtle things, such as a bloody-nose.
+ - Major events should be a “stop-in-your-tracks and do something now” levels of events, such as vomiting, blinding, etc.
+ - Catastrophic events should either be incredibly dangerous for the psyker or leave permanent lingering harm, acting as a proper deterrent for being so reckless.
+
+ - Being unable to see your exact stress is a deliberate design choice to keep the power as a ‘fragile’ and ‘dangerous’ type of magic.
+
+- Any effect with passive stress generation should be able to be easily turned off on demand; letting Psykers hit the brakes is important for gameplay!
+
+- The roots determine which subtype of organ the psyker gets. This determines both how much stress they generate, how much they can handle, and how they cope with it.
+
+- Psyker powers usually give increased bonuses for having matching negative quirks. For example, scrying is less stress inducing on a blind person. A paraplegic person can levitate with minimal upkeep. When possible, you should incorporate bonuses for matching negative quirks into your psyker powers for flavour reasons.
+
+## Cultivators
+
+We’ve made the Goku analogy a hundred-times, it just isn’t funny anymore.
+
+- Cultivators use a component (modular_doppler\modular_powers\code\powers\resonant\cultivator\\cultivator_energy.dm) for managing their Energy resource.
+
+- Cultivators center around a specific root power called an alignment, which they have to charge up using Aura or through Meditation. Once in this state, they gain great bonuses, even if unarmed and unarmored, allowing them to go on equal playing field with most folks.
+ - Unarmored defence is calculated based on the difference between the highest piece of armour worn of the type on any body-part. This is the best solution possible, as extra armour can’t discriminate against body-part.
+ - Unarmed attack is a flat amount of damage added to punches. Beware the antics of punch-stacking.
+
+- Alignments drains passively while active; and using your cultivator powers drains it more, but often with heightened effects. Most powers for Cultivator should not consume energy out of this state, and preferably offer some form of flavourful way to interact with your ‘theme’ outside of this empowered state.
+
+- Aura works by passively processing in the root power and having a hand-picked list of circumstances that contribute to energy-generation. These usually align thematically with the root; Flamesoul for example loves seeing bonfires and people on fire. Try to cover as much ground as possible, and that almost all of it should be ‘visual’
+
+- The flavour goal is all about associating with a specific phenomena, exuding it constantly around you. Thusly, try to design flavour with this in mind. Make it as clear as possible to all around you that you’re all about X!
+
+## Aberrant
+
+It does a lot of things!
+
+- Aberrant is divided into three sections; beastial, monstrous, and anomalous.
+ - Though beastial and aberrant have overlap and have some powers that are shared among both, beastial is specifically about having animal-like traits and monstrous is about unnatural and monster-like qualities. These both use satiation and hunger as a secondary resource to using their powers. Aberrant does not have a build in cost var, so you’ll have to subtract hunger yourself (by for example using user.adjust_nutrition(hunger_cost))
+ - Anomalous is all about odd phenomena that can’t be explained otherwise.
+
+- Powers in this section are a mixed batch of resonant or not, so you should specify if powers are resonant or not in the description and mark the _resonant_ variable where applicable. If it is supernatural or not explainable through our current sciences, it should be magical. Always err towards magical when in doubt.
+
+## Warfighter
+
+WRAAAAGH!
+
+- There are three categories. These are not mutually exclusive roots, but have different priorities.
+ - Commander is about giving commands, which are debuffs and buffs that linger for a duration.
+ - Martial Artist, which is about tackles, unarmed combat and being a menace even without weapons.
+ - Equipment Specialist, which is about specializing in specific equipment to greatly increase their performance. Shield blocks, explosives timers, etc.
+
+- Commander has its own subtype of action (/datum/action/cooldown/power/warfighter/command) to facilitate its mechanics. It gains bonuses from being a head as the user and being near your own department members. Every single commander action should have some form of scaling that plays into this.
+
+- Martial Arts have the issue that they are tied to the mind as components, which makes them transferable per bodies (unlike other powers). Make sure to properly clean them up when destroyed!
+
+- All powers here should directly relate to combat, but cannot be magical. Peak-performance and exceptional strength are permitted, but it shouldn’t border into the supernatural.
+
+## Expert
+
+Mundane, but neat!
+
+- Powers here shouldn’t be spectacular and in essence anything something someone can do in our current day and age, even if it is exceptional by standards. It is meant to show the best of the best of what people can do.
+- There’s no overarching mechanics; just add whats fun. It is best to keep things simple. Try to tap into gameplay systems such as the skill system that don’t often see a lot of love; people love having a cozy niche.
+- Powers here shouldn’t be magical as that would mean they belong elsewhere, nor should they be directly combat, as that would imply they belong to Warfighter. Clever-use in combat is still allowed, such as Punt.
+
+## Augmented
+
+I never asked for this.
+
+- Augmented behaves more akin to augments than powers, largely to have as much parity as possible with the existing augment system. Some possess a special premium augment component (/datum/component/premium_augment) which adds special mechanics.
+
+- Premium augments use quality, a degrading resource that sinks overtime as well as from use. The efficiency of augments decreases as quality goes down, and even stops working all-together at 0. This can be fixed with a premium augment maintenance surgery or by manually removing the augment and performing the examine steps on it.
+
+- Actions for premium augments use item powers and **do not inherit any of the base functionalities **of the power system’s action system. **Everything must be handled on the augment **rather than the power.
+ - Every premium augment needs an action, as it indicates the percentage. An off/on switch is sufficient.
+
+- Regular augments are mixed in with the archetype as well. Make sure to distinguish powers-unique augments as premium; the flavour is that these are rare, custom-made augments that can’t be mass-produced on the station.
+
+# Step-By-Step Walkthrough on Making a Power
+
+Disclaimer: This walkthrough deliberately skips some best practices and edge cases in order to stay focused on the fundamentals. Once complete, compare it against existing powers and the finished example.
+
+For the practical learners, having an example to work with does wonders. so we’ll be making a small power ourselves to take in the various things we learned, without too much complexity. For this, we will be making an aberrant power that lets us shoot a laser from our eyes. Yes, this is similar to the genetics power, but ours will be _cooler_. This will expect at least a very rough understanding of SS13’s code: if you do not, [DMByExample](https://spacestation13.github.io/DMByExample/hello_world.html) is a good resource. Various links in this guide will link to either it or earlier terms mentioned in the document.
+
+Let us start off with by defining the [Power Datum](#anchor-2) first. This is always a necessary step, and also helps us by setting our idea down on paper. We’ll need to do the [object typepath](https://spacestation13.github.io/DMByExample/objs.html): as it is an aberant power, this should end up becoming something like **/datum/power/aberrant/laser_eyes**. Find a fitting name and description, security text and a value, as well as a _security_record_text_ to describe our power. It’ll be a major threat as well.
+
+Finally, since we’ll be using an action to toggle our laser eyes on and off, we’ll need to specify an _action_path._ Since we want it to inherit the aberrant action path, we’ll name it **/datum/action/cooldown/power/aberrant/laser_eyes** to stay consistent. Finally, we’ll want to make it require the monstrous root, so we’ll add **list(/datum/power/aberrant_root/monstrous)** as a requirement. Remember, requirements are a list!
+
+In the end it should come out looking something like this. Don’t worry about matching the details in the name, description and security record; flavour is subjective.
+
+```dm
+/datum/power/aberrant/laser_eyes
+ name = "Laser Eyes"
+ desc = "Shoot a deadly beam from your eyes, burning your foes! Increases your hunger with each shot."
+ security_record_text = "Subject can shoot lasers from their eyes."
+ security_threat = POWER_THREAT_MAJOR
+ value = 6
+ action_path = /datum/action/cooldown/power/aberrant/laser_eyes
+ required_powers = list(/datum/power/aberrant_root/monstrous)
+```
+
+Now that we have our power datum, it is time to add our matching [Action Datum](#anchor-4). Define it in the same file, using the _action_path_ we had just added. The name and description can be carried over, but we’ll need a _button_icon_ and _button_icon_state_. Shop for a fun one, or take the one from the final example below, which uses a laser impact sprite. Since we’re shooting projectiles, we’ll want to use _click_to_activate_ to be able to point where to shoot the lasers; and as we are shooting a projectile, we need to set _anti_magic_on_target_ to FALSE so that our ability doesn’t whiff when clicking someone that happens to be magic-immune, even if they aren’t the target! Finally, a cooldown would serve us well for balance.
+
+For the sake of future customization and allowing for admin antics, it is in your best interest to use [variables](https://spacestation13.github.io/DMByExample/variables.html) where possible. We’ll want our chosen projectile to be a variable: **_var/obj/projectile/projectile_path_**. Since this is a custom var, make sure to comment on it with ///
+
+With this, we already have a good basis. Now it is time to start writing our [functions](https://spacestation13.github.io/DMByExample/procs.html). We’ll start with our [can_use()](#anchor-6), since we actually need to check if our eyes are functional! These conditionals are part of the fun and follows the philosophy of having obvious weaknesses. Make sure to add another var, this one we’ll call **var/obj/item/organ/eyes/eyes**. The typepath matters, because we’ll be using it for reference later.
+
+For this, we’ll need to get the user’s eyes, the damage value on it, and store it in our new _eyes_ variable. You’ll regularly bump into these scenarios where you’ll need to figure out how to discover something is done, to which my recommendation is to to try and find something that is similar in functionality. Take on the challenge; and once you’ve had enough, go to the next paragraph where I’ll present my answer.
+
+In this case, we want **get_organ_slot(ORGAN_SLOT_EYES)** proc. Now, our ‘user’ in our action datum is only defined as a mob/living/, but that is insufficient for get_organ_slot(), so we’ll need to fix that by [casting](https://spacestation13.github.io/DMByExample/vars/casting.html) it. We’ll need to get and set them as a carbon; which means setting a variable that defines them as a **var/mob/living/carbon**. This is usually done with an **if(!iscarbon())** check, which is what we’ll be doing. Get and set our \_carbon_user\* and then get their organ_slot_eyes. Make sure to add a can_use() function, with user and target as their arguments.
+
+Let us expand it with an [if-statement](https://spacestation13.github.io/DMByExample/flow/if_else.html) to actually check against the damage of the eyes. We’ll be doing **if(eyes.damage >= eyes.maxHealth)**, and returning FALSE if true. This is an [operator](https://spacestation13.github.io/DMByExample/operators.html) that returns true if the left-side variable is bigger or equal to the right-side variable. This way, non-functional eyes won’t let us shoot, but a bit of eye damage isn’t a problem yet.
+
+But wait, what if we have no eyes? Oh no. We’ll need to expand our if-statement a bit to account for that, or we’ll get errors as it tries to look-up something that doesn’t exist! Lets make sure our get_organ_slot succeeded by doing **if(eyes && eyes.damage >= eyes.maxHealth)**. This way, it won’t go onto the second check if we don’t have eyes defined, as && is an operator for ‘and’. In DM, multi-statements are resolved left-to-right and will terminate if any return false.
+
+Make sure to include return ..() in the block so that it can [inherit](https://spacestation13.github.io/DMByExample/objs/inheritance.html) the parent; this in essence calls the effects of the original parent, meaning we can override it with relative safety and ensure all the other validation occurs.
+
+Sweet. So now we have our validation out of the way. Now lets actually shoot freakin laser beams! Fortunately, we already have a good helper in the form of **fire_projectile()**. We’ll just need to add a few [arguments](https://spacestation13.github.io/DMByExample/procs/arguments.html), and add a bit of feedback in the form of a sound.
+
+fire_projectile() takes three arguments; the origin point, the target and the projectile. I’ll give you an existing laser projectile for now for ease of access: we’ll be using **/obj/projectile/beam/laser**. Define it in our earlier \_projectile_path\*. Now, lets make our use_action(). Make sure to define a user and a target, just like with can_use(). We’ll be adding the fire_projectile() function with all three arguments in there; and making it an if-statement. Why? Because that way we can now if our use_action() was a success or not.
+
+Finally, throw in a sound for a bit of feedback, because a laser isn’t as satisfying without a pew. I’ll give you this one for free: **playsound(user, 'sound/items/weapons/lasercannonfire.ogg', 50)**
+
+With that, we are technically done. That’s right, we now shoot lasers!
+
+Of course, there is a lot missing from the minimal version. We aren’t subtracting hunger yet, we aren’t checking if the user is too hungry, and eye damage does not affect the laser yet. Rather than leaving the walkthrough split across several partial snippets, here is one complete example with those additions included:
+
+```dm
+/*
+Shoots lasers from our eyes!
+*/
+/datum/power/aberrant/laser_eyes
+ name = "Laser Eyes"
+ desc = "Shoot a deadly beam from your eyes, burning your foes! Increases your hunger with each shot."
+ security_record_text = "Subject can shoot lasers from their eyes."
+ security_threat = POWER_THREAT_MAJOR
+ value = 6
+ action_path = /datum/action/cooldown/power/aberrant/laser_eyes
+ required_powers = list(/datum/power/aberrant_root/monstrous)
+
+/datum/action/cooldown/power/aberrant/laser_eyes
+ name = "Laser Eyes"
+ desc = "Shoot a deadly beam from your eyes, burning your foes! Increases your hunger with each shot."
+ button_icon = 'icons/effects/effects.dmi'
+ button_icon_state = "impact_laser"
+ cooldown_time = 3 SECONDS
+ click_to_activate = TRUE
+ anti_magic_on_target = FALSE
+ /// The projectile we fire
+ var/obj/projectile/projectile_path = /obj/projectile/beam/laser
+ /// The sound of the projectile we fire
+ var/projectile_sound = 'sound/items/weapons/lasercannonfire.ogg'
+ /// The user's eyes
+ var/obj/item/organ/eyes/eyes
+ /// The hunger cost of the power
+ var/hunger_cost = 10
+
+/datum/action/cooldown/power/aberrant/laser_eyes/can_use(mob/living/user, atom/target)
+ if(!iscarbon(user))
+ return FALSE
+ var/mob/living/carbon/carbon_user = user
+ eyes = carbon_user.get_organ_slot(ORGAN_SLOT_EYES)
+ if(eyes && eyes.damage >= eyes.maxHealth)
+ owner.balloon_alert(user, "eyes non-functional!")
+ return FALSE
+ if(user.nutrition <= NUTRITION_LEVEL_STARVING) // can't use while starving
+ owner.balloon_alert(user, "too hungry!")
+ return FALSE
+ return ..()
+
+/datum/action/cooldown/power/aberrant/laser_eyes/use_action(mob/living/user, atom/target)
+ if(!fire_projectile(user, target, projectile_path))
+ return FALSE
+ playsound(user, projectile_sound, 50)
+ return TRUE
+
+/datum/action/cooldown/power/aberrant/laser_eyes/ready_projectile(obj/projectile/projectile_instance, atom/target, mob/living/user)
+ // We halve the damage that eye loss affects so that it isn't that crippling.
+ if(eyes)
+ projectile_instance.damage = max(0, projectile_instance.damage - (eyes.damage / 2))
+ return ..()
+
+/datum/action/cooldown/power/aberrant/laser_eyes/on_action_success(mob/living/user, atom/target)
+ if(iscarbon(user))
+ user.adjust_nutrition(-hunger_cost)
+```
+
+# Future Powers Development
+
+As of writing, the first iteration of the system is not the final. Whilst the base systems are there, there is still likely that changes are to be had both gameplay and flow.
+
+- Particularly, Aberrant is in not a great position and acts as a kitchen-sink. The goal is to eventually split this off into Aberrant and Imbued, where Aberrant is meant to be a mix of odd and supernatural BIOLOGICAL traits, and Imbued being in a position where anomalous and magical properties DIRECTLY affect you. This should make it so we have no more need for a future kitchen-sink power.
+- Enigmatist is also still in development due to its sheer size.
+- Outside of these there are no large changes expected to systems. Balance, numbers and more are always subject to debate.
diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm
new file mode 100644
index 00000000000000..7d2a0553be47d0
--- /dev/null
+++ b/modular_doppler/modular_powers/code/_power.dm
@@ -0,0 +1,345 @@
+
+// Every power should be coded around being applied on spawn.
+/datum/power
+ /// The name of the power
+ var/name = "Test Power"
+ /// The description of the power
+ var/desc = "This is a test power."
+ /// What the power is worth in preferences, zero = neutral / free
+ var/value = 0
+ /// Flags related to this power.
+ var/power_flags = POWER_HUMAN_ONLY
+ /// Reference to the mob currently tied to this power datum. Powers are not singletons.
+ var/mob/living/power_holder
+ /// if applicable, apply and remove this mob trait
+ var/mob_trait
+ /// Species that cannot pick this power. If species_blacklist_is_whitelist is TRUE, only these species can.
+ var/list/species_blacklist
+ /// If TRUE, species_blacklist becomes a whitelist.
+ var/species_blacklist_is_whitelist = FALSE
+ /// Amount of points this trait is worth towards the hardcore character mode.
+ /// Minus points implies a positive power, positive means its hard.
+ /// This is used to pick the powers assigned to a hardcore character.
+ //// 0 means its not available to hardcore draws.
+ var/hardcore_value = 0
+ /// When making an abstract power (in OOP terms), don't forget to set this var to the type path for that abstract power.
+ var/abstract_parent_type = /datum/power
+ /// max stat below which this power can process (if it has POWER_PROCESSES) and above which it stops.
+ /// If null, then it will process regardless of stat.
+ var/maximum_process_stat = HARD_CRIT
+ /// A list of additional signals to register with update_process()
+ var/list/process_update_signals
+ /// A list of traits that should stop this power from processing.
+ /// Signals for adding and removing this trait will automatically be added to `process_update_signals`.
+ var/list/no_process_traits
+ /// Is it not available in the preference menu?
+ var/available_in_prefs = TRUE
+
+ /// The overarching archetype this belongs to.
+ var/archetype
+ /// The path this belongs to.
+ var/path
+ /// The priority this has.
+ var/priority = NONE
+ /// The powers this requires, if any.
+ var/list/required_powers
+ /// Allow subtypes to count for requirements.
+ var/required_allow_subtypes
+ /// Any one of the required powers satisfies the requirement list.
+ var/required_allow_any
+ /// The text in security records for this power.
+ var/security_record_text
+ /// Security threat classification used for records output.
+ var/security_threat = POWER_THREAT_MINOR
+ /// If FALSE, this specific power instance is hidden from security record power listings.
+ var/include_in_security_records = TRUE
+
+ /// The path, if applicable, to the action.
+ var/datum/action/cooldown/power/action_path
+
+ /// Where items were spawned for the power, if any.
+ var/list/where_items_spawned
+ /// If true, the backpack automatically opens on post_add(). Usually set to TRUE when an item is equipped inside the player's backpack.
+ var/open_backpack = FALSE
+
+/datum/power/New()
+ . = ..()
+ for(var/trait in no_process_traits)
+ LAZYADD(process_update_signals, list(SIGNAL_ADDTRAIT(trait), SIGNAL_REMOVETRAIT(trait)))
+
+/datum/power/Destroy()
+ if(power_holder)
+ remove_from_current_holder()
+ return ..()
+
+/// Called when power_holder is qdeleting. Simply qdels this datum and lets Destroy() handle the rest.
+/datum/power/proc/on_holder_qdeleting(mob/living/source, force)
+ SIGNAL_HANDLER
+ qdel(src)
+
+/**
+ * Adds the power to a new power_holder.
+ *
+ * Performs logic to make sure new_holder is a valid holder of this power.
+ * Returns FALSEy if there was some kind of error. Returns TRUE otherwise.
+ * Arguments:
+ * * new_holder - The mob to add this power to.
+ * * power_transfer - If this is being added to the holder as part of a power transfer. Powers can use this to decide not to spawn new items or apply any other one-time effects.
+ */
+/datum/power/proc/add_to_holder(mob/living/new_holder, power_transfer = FALSE, client/client_source, unique = TRUE)
+ if(!new_holder)
+ CRASH("Power attempted to be added to null mob.")
+
+ if((power_flags & POWER_HUMAN_ONLY) && !ishuman(new_holder))
+ CRASH("Human only power attempted to be added to non-human mob.")
+
+ if(new_holder.has_archetype_power(type))
+ CRASH("Power attempted to be added to mob which already had this power.")
+
+ if(power_holder)
+ CRASH("Attempted to add power to a holder when it already has a holder.")
+
+ power_holder = new_holder
+ power_holder.powers += src
+ // If we weren't passed a client source try to use a present one
+ client_source ||= power_holder.client
+
+ if(mob_trait)
+ ADD_TRAIT(power_holder, mob_trait, POWER_TRAIT)
+
+ add(client_source)
+
+ if(power_flags & POWER_PROCESSES)
+ if(!isnull(maximum_process_stat))
+ RegisterSignal(power_holder, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed))
+ if(process_update_signals)
+ RegisterSignals(power_holder, process_update_signals, PROC_REF(update_process))
+ if(should_process())
+ START_PROCESSING(SSpowers, src)
+
+ if(!power_transfer)
+ if (unique)
+ add_unique(client_source)
+
+ if(power_holder.client)
+ post_add()
+ else
+ RegisterSignal(power_holder, COMSIG_MOB_LOGIN, PROC_REF(on_power_holder_first_login))
+
+ RegisterSignal(power_holder, COMSIG_QDELETING, PROC_REF(on_holder_qdeleting))
+
+ return TRUE
+
+/// Removes the power from the current power_holder.
+/datum/power/proc/remove_from_current_holder(power_transfer = FALSE)
+ if(!power_holder)
+ CRASH("Attempted to remove power from the current holder when it has no current holder.")
+
+ UnregisterSignal(power_holder, list(COMSIG_MOB_STATCHANGE, COMSIG_MOB_LOGIN, COMSIG_QDELETING))
+ if(process_update_signals)
+ UnregisterSignal(power_holder, process_update_signals)
+
+ power_holder.powers -= src
+
+ if(mob_trait && !QDELETED(power_holder))
+ REMOVE_TRAIT(power_holder, mob_trait, POWER_TRAIT)
+
+ if(power_flags & POWER_PROCESSES)
+ STOP_PROCESSING(SSpowers, src)
+
+ remove()
+
+ if(!QDELETED(power_holder))
+ power_holder.refresh_security_power_records()
+
+ power_holder = null
+
+/**
+ * On client connection set power preferences.
+ *
+ * Run post_add to set the client preferences for the power.
+ * Clear the attached signal for login.
+ * Used when the power has been gained and no client is attached to the mob.
+ */
+/datum/power/proc/on_power_holder_first_login(mob/living/source)
+ SIGNAL_HANDLER
+
+ UnregisterSignal(source, COMSIG_MOB_LOGIN)
+ post_add()
+
+/// Any effect that should be applied every single time the power is added to any mob, even when transferred.
+/datum/power/proc/add(client/client_source)
+ return
+
+/// Returns the text this power should contribute to security records.
+/datum/power/proc/get_security_record_text()
+ return security_record_text
+
+/// Any effects from the proc that should not be done multiple times if the power is transferred between mobs.
+/// Put stuff like spawning items in here.
+/datum/power/proc/add_unique(client/client_source)
+ return
+
+/// Removal of any reversible effects added by the power.
+/datum/power/proc/remove()
+ return
+
+/// Any special effects or chat messages which should be applied.
+/// This proc is guaranteed to run if the mob has a client when the power is added.
+/// Otherwise, it runs once on the next COMSIG_MOB_LOGIN.
+/datum/power/proc/post_add()
+ SHOULD_CALL_PARENT(TRUE)
+ // Grants appropriate actions in the UI
+ if(action_path)
+ var/new_action_path = grant_action(action_path)
+ action_path = new_action_path
+ // If we give items to the player and open_backpack is true, have it open on round start.
+ if(open_backpack)
+ var/mob/living/carbon/human/human_holder = power_holder
+ // post_add() can be called via delayed callback. Check they still have a backpack equipped before trying to open it.
+ if(human_holder.back)
+ human_holder.back.atom_storage.show_contents(human_holder)
+ // Informs the players of any spawned items.
+ for(var/chat_string in where_items_spawned)
+ to_chat(power_holder, chat_string)
+
+ where_items_spawned = null
+ power_holder?.refresh_security_power_records() // ensures that post_add features are included in the records.
+ return
+
+/// Adds activateable power buttons.
+/datum/power/proc/grant_action(datum/action/cooldown/power/power_path)
+ if(!ispath(power_path) || !power_holder)
+ return FALSE
+
+ var/datum/action/cooldown/power/new_action = new power_path(src)
+ new_action.origin_power = src
+ new_action.Grant(power_holder)
+
+ return new_action
+
+/// Constructs [GLOB.all_power_constant_data] by iterating through a typecache of pregen data, ignoring abstract types, and instantiating the rest.
+/proc/generate_power_constant_data()
+ RETURN_TYPE(/list/datum/power_constant_data)
+
+ var/list/datum/power_constant_data/all_constant_data = list()
+
+ for (var/datum/power_constant_data/iterated_path as anything in typecacheof(path = /datum/power_constant_data, ignore_root_path = TRUE))
+ if (initial(iterated_path.abstract_type) == iterated_path)
+ continue
+
+ if (!isnull(all_constant_data[initial(iterated_path.associated_typepath)]))
+ stack_trace("pre-existing pregen data for [initial(iterated_path.associated_typepath)] when [iterated_path] was being considered: [all_constant_data[initial(iterated_path.associated_typepath)]]. \
+ this is definitely a bug, and is probably because one of the two pregen data have the wrong power typepath defined. [iterated_path] will not be instantiated")
+ continue
+
+ var/datum/power_constant_data/pregen_data = new iterated_path
+ all_constant_data[pregen_data.associated_typepath] = pregen_data
+
+ return all_constant_data
+
+GLOBAL_LIST_INIT_TYPED(all_power_constant_data, /datum/power_constant_data, generate_power_constant_data())
+
+/// A singleton datum representing constant data and procs used by powers.
+/datum/power_constant_data
+ abstract_type = /datum/power_constant_data
+
+ /// The typepath of the power we will be associated with in the global list.
+ var/datum/power/associated_typepath
+
+ /// A lazylist of preference datum typepaths. Any character pref put in here will be rendered in the powers page under a dropdown.
+ var/list/datum/preference/customization_options
+
+/datum/power_constant_data/New()
+ . = ..()
+
+ ASSERT(abstract_type != type && !isnull(associated_typepath), "associated_typepath null - please set it! occurred on: [src.type]")
+
+/// Returns a list of savefile_keys derived from the preference typepaths in [customization_options]. Used in powers middleware to supply the preferences to render.
+/datum/power_constant_data/proc/get_customization_data()
+ RETURN_TYPE(/list)
+
+ var/list/customization_data = list()
+
+ for (var/datum/preference/pref_type as anything in customization_options)
+ var/datum/preference/pref_instance = GLOB.preference_entries[pref_type]
+ if (isnull(pref_instance))
+ stack_trace("get_customization_data was called before instantiation of [pref_type]!")
+ continue // just in case its a fluke and its only this one that's not instantiated, we'll check the other pref entries
+
+ customization_data += pref_instance.savefile_key
+
+ return customization_data
+
+/// Is this power customizable? If true, a button will appear within the power's description box in the powers page, and upon clicking it,
+/// will open a customization menu for the power.
+/datum/power_constant_data/proc/is_customizable()
+ return LAZYLEN(customization_options) > 0
+
+/datum/power_constant_data/Destroy(force)
+ var/error_message = "[src], a singleton power constant data instance, was destroyed! This should not happen!"
+ if (force)
+ error_message += " NOTE: This Destroy() was called with force == TRUE. This instance will be deleted and replaced with a new one."
+ stack_trace(error_message)
+
+ if (!force)
+ return QDEL_HINT_LETMELIVE
+
+ . = ..()
+
+ GLOB.all_power_constant_data[associated_typepath] = new src.type //recover
+
+/// Returns if the power holder should process currently or not.
+/datum/power/proc/should_process()
+ SHOULD_CALL_PARENT(TRUE)
+ SHOULD_BE_PURE(TRUE)
+ if(QDELETED(power_holder))
+ return FALSE
+ if(!(power_flags & POWER_PROCESSES))
+ return FALSE
+ if(!isnull(maximum_process_stat) && power_holder.stat >= maximum_process_stat)
+ return FALSE
+ for(var/trait in no_process_traits)
+ if(HAS_TRAIT(power_holder, trait))
+ return FALSE
+ return TRUE
+
+/// Checks to see if the power should be processing, and starts/stops it.
+/datum/power/proc/update_process()
+ SIGNAL_HANDLER
+ SHOULD_NOT_OVERRIDE(TRUE)
+ if(should_process())
+ START_PROCESSING(SSpowers, src)
+ else
+ STOP_PROCESSING(SSpowers, src)
+
+/// Updates processing status whenever the mob's stat changes.
+/datum/power/proc/on_stat_changed(mob/living/source, new_stat)
+ SIGNAL_HANDLER
+ update_process()
+
+
+/**
+ * Handles inserting an item in any of the valid slots provided, then allows for post_add notification.
+ *
+ * If no valid slot is available for an item, the item is left at the mob's feet.
+ * Arguments:
+ * * power_item - The item to give to the power holder. If the item is a path, the item will be spawned in first on the player's turf.
+ * * valid_slots - List of LOCATION_X that is fed into [/mob/living/carbon/proc/equip_in_one_of_slots].
+ * * flavour_text - Optional flavour text to append to the where_items_spawned string after the item's location.
+ * * default_location - If the item isn't possible to equip in a valid slot, this is a description of where the item was spawned.
+ * * notify_player - If TRUE, adds strings to where_items_spawned list to be output to the player in [/datum/power/item_power/post_add()]
+ */
+/datum/power/proc/give_item_to_holder(obj/item/power_item, list/valid_slots, flavour_text = null, default_location = "at your feet", notify_player = FALSE)
+ if(ispath(power_item))
+ power_item = new power_item(get_turf(power_holder))
+
+ var/mob/living/carbon/human/human_holder = power_holder
+
+ var/where = human_holder.equip_in_one_of_slots(power_item, valid_slots, qdel_on_fail = FALSE, indirect_action = TRUE) || default_location
+
+ if(where == LOCATION_BACKPACK)
+ open_backpack = TRUE
+
+ if(notify_player)
+ LAZYADD(where_items_spawned, span_boldnotice("You have \a [power_item] [where]. [flavour_text]"))
diff --git a/modular_doppler/modular_powers/code/_resonant_projectile.dm b/modular_doppler/modular_powers/code/_resonant_projectile.dm
new file mode 100644
index 00000000000000..8b61aa0ce4479c
--- /dev/null
+++ b/modular_doppler/modular_powers/code/_resonant_projectile.dm
@@ -0,0 +1,55 @@
+// Ideally if you make projectiles for this system that are resonant based, you use this one to actually auto-handle the antimagic stuff.
+// Otherwise this is largely similar to obj/projectile/magic
+/obj/projectile/resonant
+ name = "bolt"
+ icon_state = "energy"
+ damage = 0 // MOST magic projectiles pass the "not a hostile projectile" test, despite many having negative effects
+ damage_type = OXY
+ armour_penetration = 100
+ armor_flag = NONE
+ /// determines what type of antimagic can block the spell projectile.
+ /// We have to play coy with the existing magic resistance system, for checking against resonance use victim.can_block_resonance(antimagic_charge_cost)
+ var/antimagic_flags = MAGIC_RESISTANCE
+ /// determines the drain cost on the antimagic item
+ var/antimagic_charge_cost = ANTIRESONANCE_BASE_CHARGE_COST
+
+ /// The power that made the projectile.
+ var/datum/action/cooldown/power/creating_power
+
+// TODO: actually uhh, add resonant anti-magic to this lmao.
+/obj/projectile/resonant/prehit_pierce(atom/target)
+ . = ..()
+
+ if(isliving(target))
+ var/mob/living/victim = target
+ if(victim.can_block_resonance(antimagic_charge_cost) || 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
+
+// Signalers for dispels; in the event you're shooting into an antimagic zone or something like that.
+/obj/projectile/resonant/fire(fire_angle, atom/direct_target)
+ SHOULD_CALL_PARENT(TRUE)
+ . = ..()
+ RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+/obj/projectile/resonant/Destroy()
+ SHOULD_CALL_PARENT(TRUE)
+ . = ..()
+ UnregisterSignal(src, COMSIG_ATOM_DISPEL)
+
+/// Vanishes the projectile when it is dispelled.
+/obj/projectile/resonant/proc/on_dispel(obj/projectile/projectile, atom/dispeller)
+ SIGNAL_HANDLER
+ if(dispeller)
+ projectile.visible_message(span_warning("[name] disappears into thin air as it makes contact with [dispeller]!"))
+ else
+ projectile.visible_message(span_warning("[name] disappears into thin air!"))
+ qdel(projectile)
diff --git a/modular_doppler/modular_powers/code/cargo/antiresonant_cuffs.dm b/modular_doppler/modular_powers/code/cargo/antiresonant_cuffs.dm
new file mode 100644
index 00000000000000..a113729baa740f
--- /dev/null
+++ b/modular_doppler/modular_powers/code/cargo/antiresonant_cuffs.dm
@@ -0,0 +1,9 @@
+/datum/supply_pack/security/antiresonant_cuffs
+ name = "Eschatite Handcuff Crate"
+ desc = "A crate containing 5 special-crafted handcuffs that suppress resonant powers."
+ cost = CARGO_CRATE_VALUE * 2
+ access_any = list(ACCESS_SECURITY)
+ contains = list(/obj/item/restraints/handcuffs/antiresonant = 5)
+ crate_name = "eschatite handcuff crate"
+ crate_type = /obj/structure/closet/crate/secure/gear
+
diff --git a/modular_doppler/modular_powers/code/cargo/reality_anchor.dm b/modular_doppler/modular_powers/code/cargo/reality_anchor.dm
new file mode 100644
index 00000000000000..ba8a0687eb4f55
--- /dev/null
+++ b/modular_doppler/modular_powers/code/cargo/reality_anchor.dm
@@ -0,0 +1,8 @@
+/datum/supply_pack/security/reality_anchor
+ name = "Reality Anchor Crate"
+ desc = "A miniature reality anchor for suppressing resonant phenomena."
+ cost = CARGO_CRATE_VALUE * 20
+ access_any = list(ACCESS_SECURITY)
+ contains = list(/obj/structure/reality_anchor)
+ crate_name = "reality anchor crate"
+ crate_type = /obj/structure/closet/crate/secure/weapon
diff --git a/modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm b/modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm
new file mode 100644
index 00000000000000..8152e6034c8c8f
--- /dev/null
+++ b/modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm
@@ -0,0 +1,85 @@
+/datum/supply_pack/costumes_toys/thaumaturgic
+ name = "Thaumaturgic Crate"
+ desc = "Contains 3 spell focusi for Thaumaturges to wield; plus 3 additional sets of random (discount) robes and hats to help the proccess."
+ cost = CARGO_CRATE_VALUE * 5
+ contains = list(
+ /obj/item/spell_focus = 3,
+ /obj/item/clothing/head/wizard/fake = 3,
+ /obj/item/clothing/suit/wizrobe/fake = 3,
+ )
+ crate_name = "thaumaturge crate"
+ crate_type = /obj/structure/closet/crate/wooden
+
+ /// Amount of hats in the crate (not including the random chance for real robes).
+ var/num_hats = 3
+ /// Amount of robes in the crate (not including the random chance for real robes).
+ var/num_robes = 3
+ /// Pool of hats that the crate can come with
+ var/list/hat_pool = list(
+ /obj/item/clothing/head/wizard/fake,
+ /obj/item/clothing/head/costume/witchwig,
+ /obj/item/clothing/head/collectable/wizard,
+ /obj/item/clothing/head/wizard/marisa/fake,
+ /obj/item/clothing/head/wizard/tape/fake,
+ /obj/item/clothing/head/wizard/chanterelle,
+ /obj/item/clothing/head/wizard/secwiz,
+ /obj/item/clothing/head/wizard/viszard
+ )
+ /// Pool of robes that the crate can come with
+ var/list/robe_pool = list(
+ /obj/item/clothing/suit/wizrobe/fake,
+ /obj/item/clothing/suit/wizrobe/marisa/fake,
+ /obj/item/clothing/suit/wizrobe/tape/fake,
+ /obj/item/clothing/suit/wizrobe/secwiz,
+ /obj/item/clothing/suit/wizrobe/viszard
+ )
+
+ /// There's a small chance that we manage to sneak in real wizard robes, in percentages.
+ var/real_robe_set_chance = 5
+ /// List of robe combos that can be sneaked in.
+ var/list/real_robe_sets = list(
+ list(
+ /obj/item/clothing/suit/wizrobe/magusblue,
+ /obj/item/clothing/head/wizard/magus,
+ ),
+ list(
+ /obj/item/clothing/head/wizard/magus,
+ /obj/item/clothing/suit/wizrobe/magusred,
+ ),
+ list(
+ /obj/item/clothing/head/wizard/black,
+ /obj/item/clothing/suit/wizrobe/black,
+ ),
+ list(
+ /obj/item/clothing/head/wizard/tape,
+ /obj/item/clothing/suit/wizrobe/tape,
+ ),
+ list(
+ /obj/item/clothing/head/wizard/santa,
+ /obj/item/clothing/suit/wizrobe/santa,
+ ),
+ list(
+ /obj/item/clothing/head/wizard,
+ /obj/item/clothing/suit/wizrobe,
+ ),
+ )
+// Fills it with at least 3 spell focuses and a random selection of hats and robes.
+/datum/supply_pack/costumes_toys/thaumaturgic/fill(obj/structure/closet/crate/C)
+ for(var/spawn_index in 1 to 3)
+ new /obj/item/spell_focus(C)
+
+ // chance for real robes
+ if(prob(real_robe_set_chance))
+ var/list/selected_set = pick(real_robe_sets)
+ for(var/robe_item_type in selected_set)
+ new robe_item_type(C)
+
+ var/list/hats = hat_pool.Copy()
+ for(var/spawn_index in 1 to min(num_hats, length(hats)))
+ var/hat_type = pick_n_take(hats)
+ new hat_type(C)
+
+ var/list/robes = robe_pool.Copy()
+ for(var/spawn_index in 1 to min(num_robes, length(robes)))
+ var/robe_type = pick_n_take(robes)
+ new robe_type(C)
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm
new file mode 100644
index 00000000000000..1bbdf8fa9a5002
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm
@@ -0,0 +1,226 @@
+/datum/power/augmented
+ name = "Augmented Power"
+ desc = "I never asked for this (abstract type to appear. You shouldn't be seeing this.)"
+
+ power_flags = POWER_HUMAN_ONLY
+ archetype = POWER_ARCHETYPE_MORTAL
+ path = POWER_PATH_AUGMENTED
+ priority = POWER_PRIORITY_BASIC
+ abstract_parent_type = /datum/power/augmented
+
+ /// The augment added in the quirk.
+ var/augment
+
+ /// Should the augment be disabled if they're a prisoner.
+ var/disable_if_prisoner = TRUE
+
+ /// Override for arm selection (VV/admin or other callers). Defaults to user prefs.
+ var/arm_override = AUGMENTED_ARM_USE_PREFS
+
+// default text for augments
+/datum/power/augmented/get_security_record_text()
+ if(security_record_text)
+ return security_record_text
+ if(!augment)
+ return ""
+
+ var/obj/item/organ/augment_path = augment
+ var/augment_name = initial(augment_path.name)
+ return "Subject has a [augment_name]."
+
+// Responsible for adding augments
+/datum/power/augmented/add_unique(client/client_source)
+ var/mob/living/carbon/carbon_holder = power_holder
+ if(!augment || !power_holder)
+ return
+ if(disable_if_prisoner && carbon_holder.mind?.assigned_role.title == JOB_PRISONER)
+ to_chat(carbon_holder, span_warning("Due to your job, the [name] power has been disabled."))
+ return
+
+ // All checks passed, time to actually give the item.
+ var/obj/item/organ/implant = new augment()
+
+ // Yes. We do all this. Just to get people's arms. Having two is infinitely more difficult
+ // In essence we check if the arm is given through VV; if so we skip most pref checking and use arm_override instead. Otherwise we use the prefs as normal.
+ if(implant.zone in GLOB.arm_zones)
+ var/left_match
+ var/right_match
+ if(arm_override == AUGMENTED_ARM_USE_PREFS) // Version that uses prefs
+ var/augment_left = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_left)
+ var/augment_right = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_right)
+ left_match = augment_matches_pref(augment_left)
+ right_match = augment_matches_pref(augment_right)
+ else // VV version that uses override.
+ left_match = (arm_override == AUGMENTED_ARM_LEFT || arm_override == AUGMENTED_ARM_BOTH)
+ right_match = (arm_override == AUGMENTED_ARM_RIGHT || arm_override == AUGMENTED_ARM_BOTH)
+
+ if(left_match && right_match)
+ var/obj/item/organ/left_implant = new augment()
+ left_implant.zone = BODY_ZONE_L_ARM
+ left_implant.slot = ORGAN_SLOT_LEFT_ARM_AUG
+ left_implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED)
+
+ var/obj/item/organ/right_implant = new augment()
+ right_implant.zone = BODY_ZONE_R_ARM
+ right_implant.slot = ORGAN_SLOT_RIGHT_ARM_AUG
+ right_implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED)
+ return
+ else if(left_match)
+ implant.zone = BODY_ZONE_L_ARM
+ implant.slot = ORGAN_SLOT_LEFT_ARM_AUG
+ else if(right_match)
+ implant.zone = BODY_ZONE_R_ARM
+ implant.slot = ORGAN_SLOT_RIGHT_ARM_AUG
+ else
+ return
+ implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED)
+ return
+
+/// Removes any augments spawned by this power.
+/datum/power/augmented/remove()
+ if(!augment || !power_holder)
+ return
+ var/mob/living/carbon/carbon_holder = power_holder
+ var/obj/item/organ/augment_path = augment
+ var/zone = initial(augment_path.zone)
+
+ // We don't need to dance with preferences here, just throw out the augment if its on the person.
+ if(zone in GLOB.arm_zones)
+ var/obj/item/organ/left_implant = carbon_holder.get_organ_slot(ORGAN_SLOT_LEFT_ARM_AUG)
+ if(istype(left_implant, augment_path))
+ left_implant.Remove(carbon_holder, special = TRUE)
+ qdel(left_implant)
+
+ var/obj/item/organ/right_implant = carbon_holder.get_organ_slot(ORGAN_SLOT_RIGHT_ARM_AUG)
+ if(istype(right_implant, augment_path))
+ right_implant.Remove(carbon_holder, special = TRUE)
+ qdel(right_implant)
+ return
+
+ var/slot = initial(augment_path.slot)
+ if(!slot)
+ return
+ var/obj/item/organ/implant = carbon_holder.get_organ_slot(slot)
+ if(istype(implant, augment_path))
+ implant.Remove(carbon_holder, special = TRUE)
+ qdel(implant)
+ return
+
+/// Used to get the location zones for augment_location_label
+/datum/power/augmented/proc/get_augment_location_label()
+ if(!augment)
+ return null
+ var/label
+ var/obj/item/organ/augment_path = augment
+ var/zone = initial(augment_path.zone)
+ var/slot = initial(augment_path.slot)
+ // I'd love if like, the weird slots like ORGAN_SLOT_BRAIN_CNS didn't return with weird strings like "brain_antistun".
+ // For UX we basically tell the base slots. We might have issues wtih overlap for the misc. category in the future but uhh.
+ // Just add it manually here and kick the can further down the road.
+ var/slot_label = GLOB.organ_slot_labels[slot]
+ if(slot_label)
+ return slot_label
+ if(zone in GLOB.arm_zones)
+ label = "Arms"
+ else if(zone in GLOB.leg_zones)
+ label = "Legs"
+ else if(zone == BODY_ZONE_HEAD)
+ label = "Head"
+ else if(zone == BODY_ZONE_CHEST)
+ label = "Chest"
+ else
+ label = "Misc."
+ return label
+
+// Labels for organ slots used in augment UI.
+// A lot of these are niche, and I've pre-populated with basically anything I think is relevant in the future (and the appendix lmao).
+// If yours is missing, just add it.
+GLOBAL_LIST_INIT(organ_slot_labels, list(
+ ORGAN_SLOT_HUD = "Eye HUD",
+ ORGAN_SLOT_EYES = "Eyes",
+ ORGAN_SLOT_EARS = "Ears",
+ ORGAN_SLOT_BRAIN = "Brain",
+ ORGAN_SLOT_BRAIN_CEREBELLUM = "Brain (Cerebellum)",
+ ORGAN_SLOT_BRAIN_CNS = "Brain (CNS)",
+ ORGAN_SLOT_HEART = "Heart",
+ ORGAN_SLOT_LUNGS = "Lungs",
+ ORGAN_SLOT_LIVER = "Liver",
+ ORGAN_SLOT_STOMACH = "Stomach",
+ ORGAN_SLOT_TONGUE = "Tongue",
+ ORGAN_SLOT_VOICE = "Vocal Cords",
+ ORGAN_SLOT_SPINE = "Spine",
+ ORGAN_SLOT_APPENDIX = "Appendix",
+ ORGAN_SLOT_BREATHING_TUBE = "Breathing Tube",
+ ORGAN_SLOT_HEART_AID = "Heart Aid",
+ ORGAN_SLOT_STOMACH_AID = "Stomach Aid",
+ ORGAN_SLOT_THRUSTERS = "Thrusters",
+))
+
+// Global list of arm augment power names for preference validation.
+// ALL THIS EFFORT JUST FOR SOME ARMS.
+GLOBAL_LIST_INIT(arm_augment_values, generate_arm_augment_values())
+
+/// This is to populate the global list above. It only adds augments in powers, so you can't cheat to give yourself an esword arm.
+/proc/generate_arm_augment_values()
+ var/list/values = list()
+ for(var/datum/power/augmented/power_type as anything in subtypesof(/datum/power/augmented))
+ if(initial(power_type.abstract_parent_type) == power_type)
+ continue
+ var/obj/item/organ/augment_path = initial(power_type.augment)
+ if(!augment_path)
+ continue
+ var/zone = initial(augment_path.zone)
+ if(zone in GLOB.arm_zones)
+ values += initial(power_type.name)
+ return values
+
+/// Bit of validation to make sure the augment is in fact in the user's prefs.
+/datum/power/augmented/proc/augment_matches_pref(value)
+ if(isnull(value) || value == AUGMENTED_NO_AUGMENT || !augment)
+ return FALSE
+ if(value == name)
+ return TRUE
+ if(istext(value) && value == "[augment]")
+ return TRUE
+ return FALSE
+
+/// Global arm loadout: left/right slots store the chosen augment for each arm.
+/datum/preference/choiced/augment_left
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "augment_left"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/augment_left/create_default_value()
+ return AUGMENTED_NO_AUGMENT
+
+/datum/preference/choiced/augment_left/init_possible_values()
+ return list(AUGMENTED_NO_AUGMENT) + GLOB.arm_augment_values
+
+/datum/preference/choiced/augment_left/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return TRUE
+
+/datum/preference/choiced/augment_left/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/datum/preference/choiced/augment_right
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "augment_right"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/augment_right/create_default_value()
+ return AUGMENTED_NO_AUGMENT
+
+/datum/preference/choiced/augment_right/init_possible_values()
+ return list(AUGMENTED_NO_AUGMENT) + GLOB.arm_augment_values
+
+/datum/preference/choiced/augment_right/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return TRUE
+
+/datum/preference/choiced/augment_right/apply_to_human(mob/living/carbon/human/target, value)
+ return
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_action.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_action.dm
new file mode 100644
index 00000000000000..c78b6f12914fad
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_action.dm
@@ -0,0 +1,148 @@
+/*
+ Custom actions for premium augments, meant to show the progress bar with quality wear.
+ A downside to this system is that all our premium augments need an action button to see quality. This is all to have parity with existing augments without becoming the 'snowflake' augment
+*/
+/datum/action/item_action/organ_action/premium
+ name = "Premium Augment"
+ check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED
+ background_icon_state = "bg_default"
+ overlay_icon_state = "bg_mod_border"
+
+ /// The border overlay. This is declared seperately so active_overlay can swap with it.
+ var/base_overlay_icon_state
+ /// The special border overlay if the abiltiy is in an 'active' state
+ var/active_overlay_icon_state = "bg_spell_border_active_blue"
+
+ /// Reference to the premium component datum
+ var/datum/component/premium_augment/premium_component
+ /// Overlay that shows the % of quality ontop of the action button.
+ var/mutable_appearance/quality_overlay
+ /// Defers action button creation until hud exists.
+ var/pending_hud_grant = FALSE
+
+/datum/action/item_action/organ_action/premium/New(Target)
+ ..()
+ if(active_overlay_icon_state)
+ base_overlay_icon_state ||= overlay_icon_state
+ var/obj/item/organ/organ_target = target
+ premium_component = organ_target?.premium_component
+ premium_component?.register_quality_action(src)
+ update_quality_overlay()
+
+/datum/action/item_action/organ_action/premium/Destroy()
+ premium_component?.unregister_quality_action(src)
+ return ..()
+
+/datum/action/item_action/organ_action/premium/Grant(mob/grant_to)
+ . = ..()
+ if(!premium_component)
+ var/obj/item/organ/organ_target = target
+ premium_component = organ_target?.premium_component
+ premium_component?.register_quality_action(src)
+ update_arm_label()
+ addtimer(CALLBACK(src, PROC_REF(update_quality_overlay)), 1) // Adresses a bug that the percentage is not visible at round start.
+
+// We have to delay giving the action because we communicate with the button, and this causes runtimes at roundstart. We use signalers to delay it until the huds there.
+/datum/action/item_action/organ_action/premium/GiveAction(mob/viewer)
+ if(!viewer)
+ return
+ if(!viewer.hud_used)
+ // Still grant the action even without a HUD so unit tests and headless mobs pass.
+ LAZYOR(viewer.actions, src)
+ if(!pending_hud_grant)
+ pending_hud_grant = TRUE
+ RegisterSignal(viewer, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created), override = TRUE)
+ return
+ if(pending_hud_grant)
+ pending_hud_grant = FALSE
+ UnregisterSignal(viewer, COMSIG_MOB_HUD_CREATED)
+ return ..()
+
+/// Waits until the HUD is created to then give the action, largely to properly render the percentage overlay.
+/datum/action/item_action/organ_action/premium/proc/on_hud_created(mob/source)
+ SIGNAL_HANDLER
+ GiveAction(source)
+
+/// Updates the text label to differentiate between left and right arm.
+/datum/action/item_action/organ_action/premium/proc/update_arm_label()
+ if(!istype(src, /datum/action/item_action/organ_action/premium/use))
+ return
+ var/obj/item/organ/organ_target = target
+ if(!organ_target)
+ return
+ name = "Toggle [organ_target.name][arm_side_suffix(organ_target)]"
+ build_all_button_icons(UPDATE_BUTTON_NAME | UPDATE_BUTTON_ICON | UPDATE_BUTTON_OVERLAY)
+
+/datum/action/item_action/organ_action/premium/Remove(mob/remove_from)
+ if(remove_from)
+ UnregisterSignal(remove_from, COMSIG_MOB_HUD_CREATED)
+ pending_hud_grant = FALSE
+ return ..()
+
+/datum/action/item_action/organ_action/premium/IsAvailable(feedback = FALSE)
+ . = ..()
+ if(!premium_component)
+ var/obj/item/organ/organ_target = target
+ premium_component = organ_target?.premium_component
+ return .
+
+/// Applies the maptext on the button indicating quality.
+/datum/action/item_action/organ_action/premium/proc/update_quality_overlay()
+ var/atom/movable/ui_element = get_atom_moveable()
+ if(!ui_element || !premium_component)
+ return
+ ui_element.cut_overlay(quality_overlay)
+ quality_overlay = new/mutable_appearance
+ quality_overlay.plane = ABOVE_HUD_PLANE
+ quality_overlay.maptext_width = 32
+ quality_overlay.maptext_height = 16
+ quality_overlay.maptext_x = 4
+ quality_overlay.maptext_y = 0
+ var/percent = clamp(round(premium_component.quality), 0, 100)
+ quality_overlay.maptext = MAPTEXT("[percent]%")
+ ui_element.add_overlay(quality_overlay)
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+
+/// Gets the button that is tied to the action.
+/datum/action/item_action/organ_action/premium/proc/get_atom_moveable()
+ for(var/datum/hud/hud_instance as anything in viewers)
+ var/atom/movable/screen/movable/action_button/action_button_instance = viewers[hud_instance]
+ if(istype(action_button_instance, /atom/movable/screen/movable/action_button))
+ return action_button_instance
+
+/datum/action/item_action/organ_action/premium/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force = FALSE)
+ if(active_overlay_icon_state)
+ overlay_icon_state = is_action_active(current_button) ? active_overlay_icon_state : base_overlay_icon_state
+ . = ..()
+ return .
+
+// Override to determine if an augment is currently active or not.
+/datum/action/item_action/organ_action/premium/is_action_active(atom/movable/screen/movable/action_button/current_button)
+ var/obj/item/organ/organ_target = target
+ return organ_target?.is_action_active() || FALSE
+
+/datum/action/item_action/organ_action/premium/use
+ name = "Toggle Premium Augment"
+
+/datum/action/item_action/organ_action/premium/use/New(Target)
+ ..()
+ var/obj/item/organ/organ_target = target
+ name = "Toggle [organ_target.name][arm_side_suffix(organ_target)]"
+
+/// Adds a suffix to left and right arm actions since you can have two actions and it might get confusing.
+/datum/action/item_action/organ_action/premium/proc/arm_side_suffix(obj/item/organ/organ_target)
+ if(!istype(organ_target, /obj/item/organ/cyberimp/arm))
+ return ""
+ if(organ_target.zone == BODY_ZONE_L_ARM)
+ return " (Left)"
+ if(organ_target.zone == BODY_ZONE_R_ARM)
+ return " (Right)"
+ return ""
+
+/datum/action/item_action/organ_action/premium/use/do_effect(trigger_flags)
+ var/obj/item/organ/organ_target = target
+ if(!organ_target)
+ return FALSE
+ organ_target.use_action()
+ build_all_button_icons(UPDATE_BUTTON_OVERLAY | UPDATE_BUTTON_STATUS)
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm
new file mode 100644
index 00000000000000..5d437cdcfa3454
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm
@@ -0,0 +1,282 @@
+// Responsible for handling most of the premium augment interactions away from the base cyberimplant.
+/datum/component/premium_augment
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+ /// Host organ that owns this premium augment logic.
+ var/obj/item/organ/host
+ /// Current quality percentage (0..100)
+ var/quality = AUGMENTED_PREMIUM_QUALITY_START
+ /// How often augments should tick down their decay.
+ var/decay_interval = AUGMENTED_DECAY_INTERVAL
+ /// How much augments should decay when the decay tick does occur.
+ var/decay_amount = AUGMENTED_DECAY_AMOUNT
+ /// Round-time for when we last decayed.
+ var/last_decay_time = 0
+ /// Actions that render the quality bar.
+ var/list/premium_actions
+ /// Refurbish flow state.
+ var/refurb_stage = 1
+ /// Sequence of refurb steps. Override per-augment for customization.
+ var/list/refurb_sequence = list(
+ AUGMENTED_REFURBISH_OPEN,
+ AUGMENTED_REFURBISH_PARTS,
+ AUGMENTED_REFURBISH_CALIBRATE,
+ AUGMENTED_REFURBISH_CLOSE,
+ )
+ /// Parts required during refurb. Override per-augment for customization.
+ var/list/refurb_parts = list(
+ /obj/item/stack/sheet/iron = 2,
+ /obj/item/stack/cable_coil = 1,
+ )
+ /// What parts are left that need to be added to a refurbish in progress.
+ var/list/refurb_parts_remaining
+
+/datum/component/premium_augment/Initialize()
+ if(!istype(parent, /obj/item/organ))
+ return COMPONENT_INCOMPATIBLE
+ host = parent
+ if(!host.premium_component)
+ host.premium_component = src
+ last_decay_time = world.time
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/component/premium_augment/Destroy()
+ STOP_PROCESSING(SSfastprocess, src)
+ premium_actions = null
+ if(host && host.premium_component == src) // null ref datum before destruction
+ host.premium_component = null
+ host = null
+ return ..()
+
+/// Whether the premium augment can function at all.
+/datum/component/premium_augment/proc/can_function()
+ return quality > 0
+
+/// Returns a tier label for UI or logic.
+/datum/component/premium_augment/proc/quality_tier()
+ if(quality > AUGMENTED_PREMIUM_THRESHOLD_OPTIMAL)
+ return "optimal"
+ if(quality > AUGMENTED_PREMIUM_THRESHOLD_HIGH)
+ return "standard"
+ if(quality > AUGMENTED_PREMIUM_THRESHOLD_MEDIUM)
+ return "compromised"
+ if(quality > AUGMENTED_PREMIUM_THRESHOLD_LOW)
+ return "failing"
+ return "broken"
+
+/// Performance multiplier based purely on quality tiers.
+/datum/component/premium_augment/proc/perf_mult()
+ return get_efficiency()
+
+/// Returns the efficiency value based on quality tiers.
+/datum/component/premium_augment/proc/get_efficiency()
+ if(quality > AUGMENTED_PREMIUM_THRESHOLD_OPTIMAL)
+ return AUGMENTED_PREMIUM_EFFICIENCY_OPTIMAL
+ if(quality > AUGMENTED_PREMIUM_THRESHOLD_HIGH)
+ return AUGMENTED_PREMIUM_EFFICIENCY_HIGH
+ if(quality > AUGMENTED_PREMIUM_THRESHOLD_MEDIUM)
+ return AUGMENTED_PREMIUM_EFFICIENCY_MEDIUM
+ if(quality > AUGMENTED_PREMIUM_THRESHOLD_LOW)
+ return AUGMENTED_PREMIUM_EFFICIENCY_LOW
+ return AUGMENTED_PREMIUM_EFFICIENCY_BROKEN
+
+/// Adjust quality by amount, clamped to [0..AUGMENTED_PREMIUM_QUALITY_MAX] (or override).
+/datum/component/premium_augment/proc/adjust_quality(amount, override_cap)
+ if(!isnum(amount))
+ return
+ var/cap_to = isnum(override_cap) ? override_cap : AUGMENTED_PREMIUM_QUALITY_MAX
+ quality = clamp(quality + amount, 0, cap_to)
+ update_quality_actions()
+
+/// Passive decay processing.
+/datum/component/premium_augment/process(seconds_per_tick)
+ if(decay_amount <= 0 || decay_interval <= 0)
+ return
+ if(world.time - last_decay_time < decay_interval)
+ return
+ adjust_quality(-decay_amount)
+ last_decay_time = world.time
+
+/// Register an action that should display the quality bar.
+/datum/component/premium_augment/proc/register_quality_action(datum/action/item_action/organ_action/premium/action)
+ if(!action)
+ return
+ LAZYADD(premium_actions, action)
+ action.update_quality_overlay()
+
+/// Unregister a quality bar action.
+/datum/component/premium_augment/proc/unregister_quality_action(datum/action/item_action/organ_action/premium/action)
+ if(!premium_actions || !action)
+ return
+ premium_actions -= action
+
+/// Update all registered action quality bars.
+/datum/component/premium_augment/proc/update_quality_actions()
+ if(!LAZYLEN(premium_actions))
+ return
+ for(var/datum/action/item_action/organ_action/premium/action as anything in premium_actions)
+ if(QDELETED(action))
+ premium_actions -= action
+ continue
+ action.update_quality_overlay()
+
+/// Premium maintenance: restores quality up to 75%.
+/datum/component/premium_augment/proc/apply_premium_maintenance(amount)
+ if(amount <= 0)
+ return
+ adjust_quality(amount, AUGMENTED_PREMIUM_QUALITY_START)
+
+/// Refurbish: restores quality up to 100%.
+/datum/component/premium_augment/proc/refurbish(amount)
+ if(amount <= 0)
+ return
+ adjust_quality(amount, AUGMENTED_PREMIUM_QUALITY_MAX)
+
+/// Handle refurbish interactions while the implant is out of the body.
+/datum/component/premium_augment/proc/handle_refurbish_interaction(mob/user, obj/item/tool, obj/item/organ/augment)
+ if(!user || !tool || !augment)
+ return FALSE
+ if(augment.owner) // I don't even know how you would do this; the manual says to take it out first >:C
+ to_chat(user, span_warning("You need to remove [augment] before refurbishing it."))
+ return TRUE
+ var/step = get_refurb_step()
+ if(!step)
+ return FALSE
+
+ switch(step)
+ if(AUGMENTED_REFURBISH_OPEN)
+ if(tool.tool_behaviour != TOOL_SCREWDRIVER)
+ to_chat(user, span_warning("You need a screwdriver to open [augment]'s casing."))
+ return TRUE
+ to_chat(user, span_notice("You open [augment]'s casing."))
+ tool.play_tool_sound(augment)
+ advance_refurb_step()
+ return TRUE
+
+ if(AUGMENTED_REFURBISH_PARTS)
+ ensure_refurb_parts()
+
+ // Saves typepath, amount needed and how much was used to pass on to later in the function.
+ var/typepath
+ var/needed
+ var/use_amount
+
+ // Stack-specific interactions
+ if(istype(tool, /obj/item/stack))
+ var/obj/item/stack/stack = tool
+ typepath = stack.merge_type ? stack.merge_type : stack.type
+ needed = refurb_parts_remaining[typepath]
+
+ // Wrong item, right subtype.
+ if(!needed)
+ to_chat(user, span_warning("[stack] doesn't fit [augment]'s parts."))
+ return TRUE
+
+ // Not enough in a stack
+ var/available = stack.amount
+ use_amount = min(needed, available)
+ if(use_amount <= 0 || !stack.use(use_amount))
+ to_chat(user, span_warning("You need more [stack] to continue."))
+ return TRUE
+ needed -= use_amount
+ // Non-stack parts.
+ else
+ typepath = tool.type
+ needed = refurb_parts_remaining[typepath]
+ // Wrong item
+ if(!needed)
+ to_chat(user, span_warning("[tool] doesn't fit [augment]'s parts."))
+ return TRUE
+ needed -= 1
+ qdel(tool)
+
+ // Succesful use interaction
+ if(needed <= 0)
+ refurb_parts_remaining -= typepath
+ else
+ refurb_parts_remaining[typepath] = needed
+ to_chat(user, span_notice("You replace worn parts inside [augment]."))
+ tool.play_tool_sound(augment)
+ if(!LAZYLEN(refurb_parts_remaining))
+ advance_refurb_step()
+ return TRUE
+
+ if(AUGMENTED_REFURBISH_CALIBRATE)
+ if(tool.tool_behaviour != TOOL_MULTITOOL)
+ to_chat(user, span_warning("You need a multitool to calibrate [augment]."))
+ return TRUE
+ to_chat(user, span_notice("You calibrate [augment]'s diagnostics."))
+ tool.play_tool_sound(augment)
+ advance_refurb_step()
+ return TRUE
+
+ if(AUGMENTED_REFURBISH_CLOSE)
+ if(tool.tool_behaviour != TOOL_SCREWDRIVER)
+ to_chat(user, span_warning("You need a screwdriver to close [augment]'s casing."))
+ return TRUE
+ refurbish(AUGMENTED_PREMIUM_QUALITY_MAX)
+ tool.play_tool_sound(augment)
+ reset_refurb()
+ to_chat(user, span_notice("You finish refurbishing [augment]. Looks about as new as it can get."))
+ return TRUE
+
+ return FALSE
+
+/// Returns lines to show when examining a premium augment for refurbishing.
+/datum/component/premium_augment/proc/get_refurb_examine_lines(obj/item/organ/augment)
+ var/list/lines = list()
+ if(!augment)
+ return lines
+ lines += span_notice("Premium quality: [round(quality)]%.")
+ if(augment.owner)
+ lines += span_warning("Remove [augment] before refurbishing it.")
+ return lines
+
+ var/step = get_refurb_step()
+ if(!step)
+ return lines
+
+ switch(step)
+ if(AUGMENTED_REFURBISH_OPEN)
+ lines += span_notice("Refurbish step: Open the casing with a screwdriver.")
+ if(AUGMENTED_REFURBISH_PARTS)
+ ensure_refurb_parts()
+ if(!LAZYLEN(refurb_parts_remaining))
+ lines += span_notice("Refurbish step: Parts replaced. This isn't meant to show! Why is it not telling you to use a multitool?! PANIC!")
+ else
+ lines += span_notice("Refurbish step: Replace worn parts.")
+ for(var/typepath in refurb_parts_remaining)
+ var/amount = refurb_parts_remaining[typepath]
+ var/display_name = initial(typepath:name)
+ lines += span_notice(" - [display_name] x[amount]")
+ if(AUGMENTED_REFURBISH_CALIBRATE)
+ lines += span_notice("Refurbish step: Calibrate diagnostics with a multitool.")
+ if(AUGMENTED_REFURBISH_CLOSE)
+ lines += span_notice("Refurbish step: Close the casing with a screwdriver to finish.")
+ return lines
+
+/// Gets the current step we're on in the refurbish process.
+/datum/component/premium_augment/proc/get_refurb_step()
+ if(!LAZYLEN(refurb_sequence))
+ return null
+ refurb_stage = clamp(refurb_stage, 1, refurb_sequence.len)
+ return refurb_sequence[refurb_stage]
+
+/// Moves us up to the next refurbish phase.
+/datum/component/premium_augment/proc/advance_refurb_step()
+ refurb_stage++
+ refurb_parts_remaining = null
+ if(refurb_stage > refurb_sequence.len)
+ refurb_stage = refurb_sequence.len
+
+/// Resets refurbishing back to the first stage which is opening it.
+/datum/component/premium_augment/proc/reset_refurb()
+ refurb_stage = 1
+ refurb_parts_remaining = null
+
+/// Gets all the required refurb parts and adds them to refurb parts remaining.
+/datum/component/premium_augment/proc/ensure_refurb_parts()
+ if(refurb_parts_remaining)
+ return
+ refurb_parts_remaining = list()
+ for(var/typepath in refurb_parts)
+ refurb_parts_remaining[typepath] = refurb_parts[typepath]
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment_organ.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment_organ.dm
new file mode 100644
index 00000000000000..140f7ff120436a
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment_organ.dm
@@ -0,0 +1,21 @@
+// File containing modular edits to obj/item/organ
+
+/obj/item/organ
+ /// Whether this organ supports premium augment mechanics.
+ var/premium = FALSE
+ /// Component for premium augment mechanics.
+ var/datum/component/premium_augment/premium_component
+
+/// Overrides attackby to allow premium mechanics to handle it in their refurbish action
+/obj/item/organ/attackby(obj/item/tool, mob/user, params)
+ if(premium_component && premium_component.handle_refurbish_interaction(user, tool, src))
+ return
+ return ..()
+
+/// Default premium action hook. Override per organ.
+/obj/item/organ/proc/use_action()
+ return FALSE
+
+/// Premium augments can override this to report their "on" state for button overlays.
+/obj/item/organ/proc/is_action_active()
+ return FALSE
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm
new file mode 100644
index 00000000000000..f02266d947ad62
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm
@@ -0,0 +1,257 @@
+/*
+ Teleports you to medbay, once. Either on demand or when you soft-crit. Needs to be refurbished after & can be interupted.
+*/
+/datum/power/augmented/auto_retriever
+ name = "Premium ANGL Auto Retriever"
+ desc = "Some assets are far too wealthy to risk losing. Created by DeForest, this allows their premium customers to be rescued from the most grievous of circumstances; and recently came with a support API for other healthcare providers.\
+ \n Once you reach critical condition or when manually activated, you begin a slow (and obvious) 10 second teleport towards your station's medbay lobby (regardless of Z-level).\
+ Once it fires, a warning message is issued over the radio. The teleportation sets the quality to 0%, and can be interrupted by Epinephrine, Atropine or Stabilizing Agent in the bloodstream, EMP, or healing you above the critical threshold, after which it loses 25% quality and enters a several minute cooldown period.\
+ \n Decreases in quality twice as fast. Lower quality decreases the speed of the teleport."
+ security_record_text = "Subject has a ANGL Auto Retriever and will teleport to medbay if critically injured."
+ security_threat = POWER_THREAT_MAJOR
+
+ value = 6
+ augment = /obj/item/organ/cyberimp/chest/auto_retriever
+
+/obj/item/organ/cyberimp/chest/auto_retriever
+ name = "ANGL Auto Retriever"
+ desc = "Some assets are far too wealthy to risk losing. Created by DeForest, this allows their premium customers to be rescued from the most grievous of circumstances; and recently came with a support API for other healthcare providers.\
+ \n Once you reach critical condition or when manually activated, you begin a slow (and obvious) 10 second teleport towards your station's medbay lobby (regardless of Z-level).\
+ Once it fires, a warning message is issued over the radio. The teleportation sets the quality to 0%, and can be interrupted by Epinephrine, Atropine or Stabilizing Agent in the bloodstream, EMP, or healing you above the critical threshold, after which it loses 25% quality and enters a several minute cooldown period.\
+ \n Decreases in quality twice as fast. Lower quality decreases the speed of the teleport."
+ icon_state = "reviver_implant"
+ slot = ORGAN_SLOT_HEART_AID
+
+ actions_types = list(/datum/action/item_action/organ_action/premium/use)
+ premium = TRUE
+ /// On or off state.
+ var/enabled = TRUE
+
+ /// Are we in the process of teleporting
+ var/teleporting = FALSE
+ /// Reference ID for the timer proc.
+ var/teleport_timer_id
+ /// Time it takes to spool up the teleport.
+ var/teleport_spool_time = 10 SECONDS
+ /// The sound that plays while spooling up.
+ var/teleport_charge_sound = 'sound/effects/magic/lightning_chargeup.ogg'
+
+ // Frequencies are used to indicate that a teleporter is going faster or slower, since it both increases pace and pitch.
+ /// The standard sound frequency used at 75%
+ var/teleport_sound_base_frequency = 44000
+ /// The lowest sound frequency used at the lowest tier
+ var/teleport_sound_min_frequency = 32000
+ /// The highest sound frequency used at the highest tier
+ var/teleport_sound_max_frequency = 55000
+
+ /// Cooldowns for TP
+ var/tp_cooldown = 3 MINUTES
+ /// Cooldown deceleration for TP
+ COOLDOWN_DECLARE(teleport_cooldown)
+
+ /// Cooldowns for EMP
+ var/emp_cooldown = 30 SECONDS
+ /// Cooldown decleration for EMP
+ COOLDOWN_DECLARE(emp_reenable_cooldown)
+
+
+ /// Internal radio used for relaying to medbay.
+ var/obj/item/radio/internal_radio
+
+ /// Ref for the sparking overlay.
+ var/mutable_appearance/teleport_spark_overlay
+ /// Icon of the sparks on TP
+ var/teleport_spark_icon = 'icons/effects/effects.dmi'
+ /// Icon state of the sparks on TP
+ var/teleport_spark_state = "lightning"
+ /// Layer of the sparks on TP
+ var/teleport_spark_layer = ABOVE_MOB_LAYER
+
+/obj/item/organ/cyberimp/chest/auto_retriever/Initialize(mapload)
+ . = ..()
+ if(premium_component)
+ premium_component.refurb_parts = list(
+ /obj/item/stack/sheet/iron = 1,
+ /obj/item/stack/sheet/bluespace_crystal = 1,
+ /obj/item/stack/cable_coil = 2,
+ /obj/item/stock_parts/scanning_module/triphasic = 1)
+ premium_component.decay_interval = AUGMENTED_DECAY_INTERVAL / 2 // decays twice as fast.
+
+ // We give it a radio to be able to speak to the medbay frequency.
+ internal_radio = new /obj/item/radio(src)
+ internal_radio.keyslot = new /obj/item/encryptionkey/headset_med
+ internal_radio.subspace_transmission = TRUE
+ internal_radio.canhear_range = 0 // no free medbay radio 4u
+ internal_radio.recalculateChannels()
+
+/obj/item/organ/cyberimp/chest/auto_retriever/Destroy()
+ if(teleport_timer_id)
+ deltimer(teleport_timer_id)
+ teleport_timer_id = null
+ QDEL_NULL(internal_radio)
+ return ..()
+
+// Checks if we're in deep shit and need teleporting out.
+/obj/item/organ/cyberimp/chest/auto_retriever/on_life(seconds_per_tick, times_fired)
+ if(!owner || !enabled)
+ return
+ if(teleporting)
+ if(should_cancel_teleport())
+ cancel_teleport()
+ return
+ if(!premium_component?.can_function())
+ return
+ if(!COOLDOWN_FINISHED(src, teleport_cooldown))
+ return
+ if(owner.reagents?.has_reagent(/datum/reagent/medicine/epinephrine) || owner.reagents?.has_reagent(/datum/reagent/medicine/atropine))
+ return
+ if(owner.stat >= SOFT_CRIT && owner.stat != DEAD)
+ start_teleport()
+
+/// Starts spooling up and notifying literally everyone they are going to poof.
+/obj/item/organ/cyberimp/chest/auto_retriever/proc/start_teleport()
+ if(!owner)
+ return
+ if(teleporting || !enabled)
+ return
+ if(!premium_component?.can_function())
+ return
+ if(!COOLDOWN_FINISHED(src, teleport_cooldown))
+ return
+ teleporting = TRUE
+ // Modifies the tp by efficiency
+ var/efficiency = premium_component?.get_efficiency() || 1
+ var/spool_time = round(teleport_spool_time / max(efficiency, 0.01))
+ var/teleport_seconds = round(spool_time / (1 SECONDS))
+ var/message = "Patient health critical; commencing teleportation in [teleport_seconds] seconds. Stabilize patient to cancel."
+ augment_speak(message)
+ apply_teleport_effects(spool_time)
+ var/sound_frequency = clamp(round(teleport_sound_base_frequency * efficiency), teleport_sound_min_frequency, teleport_sound_max_frequency)
+ if(sound_frequency > teleport_sound_min_frequency && spool_time > 2 SECONDS)
+ var/sound_ratio = spool_time / max(spool_time - 2 SECONDS, 1)
+ sound_frequency = clamp(round(sound_frequency * sound_ratio), teleport_sound_min_frequency, teleport_sound_max_frequency)
+ owner.playsound_local(owner, teleport_charge_sound, 75, TRUE, frequency = sound_frequency)
+ teleport_timer_id = addtimer(CALLBACK(src, PROC_REF(finish_teleport)), spool_time, TIMER_STOPPABLE)
+
+/// We go POOF, away.
+/obj/item/organ/cyberimp/chest/auto_retriever/proc/finish_teleport()
+ if(!teleporting)
+ return
+ teleporting = FALSE
+ if(teleport_timer_id)
+ deltimer(teleport_timer_id)
+ teleport_timer_id = null
+ clear_teleport_effects()
+ if(!owner || owner.stat < SOFT_CRIT || owner.stat == DEAD)
+ return
+
+ // We try to TP to the lobby first; if there's no lobby we teleport them to the medbay.
+ var/turf/destination = pick_open_turf_from_area(/area/station/medical/medbay/lobby)
+ if(!destination)
+ destination = pick_open_turf_from_area(/area/station/medical/medbay, subtypes = TRUE)
+ if(!destination)
+ return
+
+ var/teleport_success = do_teleport(owner, destination, channel = TELEPORT_CHANNEL_QUANTUM)
+ if(!teleport_success)
+ return
+
+ augment_speak("Auto Retriever alert: [owner.real_name] has teleported to Medbay for emergency treatment.", RADIO_CHANNEL_MEDICAL)
+
+ // Sets it to 0. Go and get it refurbished.
+ if(premium_component)
+ premium_component.adjust_quality(-premium_component.quality)
+
+/// Cancel if stabilized, epinephrine applied, or EMP'd.
+/obj/item/organ/cyberimp/chest/auto_retriever/proc/should_cancel_teleport()
+ if(!owner)
+ return FALSE
+ if(owner.stat < SOFT_CRIT)
+ return TRUE
+ if(owner.reagents?.has_reagent(/datum/reagent/medicine/epinephrine) || owner.reagents?.has_reagent(/datum/reagent/medicine/atropine) || owner.reagents?.has_reagent(/datum/reagent/stabilizing_agent))
+ return TRUE
+ return FALSE
+
+/// Stops a teleport that is in progress.
+/obj/item/organ/cyberimp/chest/auto_retriever/proc/cancel_teleport()
+ if(!teleporting)
+ return
+ teleporting = FALSE
+ if(teleport_timer_id)
+ deltimer(teleport_timer_id)
+ teleport_timer_id = null
+ clear_teleport_effects()
+ augment_speak("Teleportation cancelled; entering cooldown.")
+ COOLDOWN_START(src, teleport_cooldown, tp_cooldown)
+ if(premium_component)
+ premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MODERATE)
+
+/// When we get EMP'd.
+/obj/item/organ/cyberimp/chest/auto_retriever/emp_act(severity)
+ . = ..()
+ if(. & EMP_PROTECT_SELF)
+ return
+ if(premium_component)
+ premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR)
+ enabled = FALSE
+ COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown)
+ premium_component?.update_quality_actions()
+ to_chat(owner, span_warning("Your [name] becomes disabled!"))
+ cancel_teleport()
+
+/// Makes the augment speak, either locally or through the radio.
+/obj/item/organ/cyberimp/chest/auto_retriever/proc/augment_speak(message, channel)
+ if(!message)
+ return
+ var/list/message_mods = list(SAY_MOD_VERB = "states")
+ if(channel)
+ if(internal_radio)
+ internal_radio.talk_into(src, message, channel, message_mods = message_mods)
+ return
+ say(message, forced = "auto retriever", message_mods = message_mods)
+
+// Toggle the auto-retriever on/off (gate for activation).
+/obj/item/organ/cyberimp/chest/auto_retriever/use_action()
+ if(!owner)
+ return FALSE
+ if(!enabled && !COOLDOWN_FINISHED(src, emp_reenable_cooldown))
+ to_chat(owner, span_warning("Your [name] is temporarily disabled from EMP interference."))
+ return FALSE
+ enabled = !enabled
+ if(enabled)
+ to_chat(owner, span_notice("Your [name] is toggled on; it will now activate when you reach critical condition."))
+ else
+ to_chat(owner, span_notice("Your [name] is toggled off."))
+ return enabled
+
+/obj/item/organ/cyberimp/chest/auto_retriever/is_action_active()
+ return enabled
+
+/// Apply the sparking visual effect + jitter.
+/obj/item/organ/cyberimp/chest/auto_retriever/proc/apply_teleport_effects(spool_time)
+ if(!owner)
+ return
+ owner.set_jitter_if_lower(spool_time)
+ if(!teleport_spark_overlay)
+ teleport_spark_overlay = mutable_appearance(teleport_spark_icon, teleport_spark_state, teleport_spark_layer)
+ teleport_spark_overlay.appearance_flags |= KEEP_APART
+ owner.add_overlay(teleport_spark_overlay)
+
+/// Removes the active sparking overlay on the mob.
+/obj/item/organ/cyberimp/chest/auto_retriever/proc/clear_teleport_effects()
+ if(!owner || !teleport_spark_overlay)
+ return
+ owner.cut_overlay(teleport_spark_overlay)
+
+/// Finds an open space to teleport to.
+/obj/item/organ/cyberimp/chest/auto_retriever/proc/pick_open_turf_from_area(area_type, subtypes = FALSE)
+ var/list/turfs = get_area_turfs(area_type, subtypes = subtypes)
+ if(!LAZYLEN(turfs))
+ return null
+ var/list/open_turfs = list()
+ for(var/turf/turf_candidate as anything in turfs)
+ if(!turf_candidate.density)
+ open_turfs += turf_candidate
+ if(!LAZYLEN(open_turfs))
+ return null
+ return pick(open_turfs)
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm
new file mode 100644
index 00000000000000..5d490bb27fbcff
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm
@@ -0,0 +1,106 @@
+/*
+ Blocks mental magic and scrying. Rather than adding the antimagic component we handle it here because we need to handle charge_cost in our own way (convert it to quality).
+*/
+/datum/power/augmented/mental_shielding
+ name = "Premium TNFL Mental Shielding Implant"
+ desc = " Based on the nullifying effects that tinfoil has on certain magical phenomena, this dermal implant created by Oracle Neuro-Systems creates a protective coating around your brain.\
+ \n Creates a barrier that blocks resonant based scrying, as well as mental abilities used on you (including magic stronger than Resonant).\
+ \n Blocking mental abilities consumes quality, increasing consumption rate the lower the quality is."
+ security_record_text = "Subject has a TNFL Mental Shielding Implant and is immune to scrying and mental-based resonance."
+ security_threat = POWER_THREAT_MAJOR
+ value = 6
+ augment = /obj/item/organ/cyberimp/brain/mental_shielding
+
+/obj/item/organ/cyberimp/brain/mental_shielding
+ name = "TNFL Mental Shielding Implant"
+ desc = "Based on the nullifying effects that tinfoil has on certain magical phenomena, this dermal implant created by Oracle Neuro-Systems creates a protective coating around your brain. \
+ Creates a barrier that blocks resonant based scrying, as well as mental abilities used on you (including magic stronger than Resonant). \
+ Blocking mental abilities consumes quality, increasing consumption rate the lower the quality is."
+ icon_state = "brain_implant_connector"
+ slot = ORGAN_SLOT_BRAIN_CNS
+ actions_types = list(/datum/action/item_action/organ_action/premium/use)
+ premium = TRUE
+ /// On or off state of the implant
+ var/enabled = TRUE
+
+ /// the factor with which we multiply the final cost of anti-mental effects
+ var/mental_mult = 5
+
+ // EMP cooldown decleration
+ COOLDOWN_DECLARE(emp_reenable_cooldown)
+ /// EMP cooldown time
+ var/emp_cooldown = 30 SECONDS
+
+/obj/item/organ/cyberimp/brain/mental_shielding/Initialize(mapload)
+ . = ..()
+ if(premium_component)
+ premium_component.refurb_parts = list(
+ /obj/item/stack/sheet/iron = 1,
+ /obj/item/stack/sheet/mineral/uranium = 1,
+ /obj/item/stack/cable_coil = 2,
+ /obj/item/stock_parts/scanning_module/triphasic = 1)
+
+// Registers antimagic signals
+/obj/item/organ/cyberimp/brain/mental_shielding/on_mob_insert(mob/living/carbon/receiver, special, movement_flags)
+ . = ..()
+ RegisterSignal(receiver, COMSIG_MOB_RECEIVE_MAGIC, PROC_REF(on_receive_magic), override = TRUE)
+ if(enabled)
+ ADD_TRAIT(receiver, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT)
+
+// Unregisters antimagic signals
+/obj/item/organ/cyberimp/brain/mental_shielding/on_mob_remove(mob/living/carbon/owner, special, movement_flags)
+ . = ..()
+ UnregisterSignal(owner, COMSIG_MOB_RECEIVE_MAGIC)
+ REMOVE_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT)
+
+// When we get EMP'd.
+/obj/item/organ/cyberimp/brain/mental_shielding/emp_act(severity)
+ . = ..()
+ if(. & EMP_PROTECT_SELF)
+ return
+ if(premium_component)
+ premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR)
+ enabled = FALSE
+ COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown)
+ premium_component?.update_quality_actions()
+ to_chat(owner, span_warning("Your [name] becomes disabled!"))
+
+/// Listener to check if it can block antimental. Basically we just check if the quality is not 0.
+/obj/item/organ/cyberimp/brain/mental_shielding/proc/on_receive_magic(mob/living/carbon/source, casted_magic_flags, charge_cost, list/antimagic_sources)
+ SIGNAL_HANDLER
+ if(!enabled || !premium_component?.can_function())
+ return NONE
+ if(!(casted_magic_flags & MAGIC_RESISTANCE_MIND))
+ return NONE
+ antimagic_sources += src
+ var/adjusted_cost = process_quality_cost(max(1, charge_cost))
+ premium_component.adjust_quality(-adjusted_cost)
+ return COMPONENT_MAGIC_BLOCKED
+
+/// Convert an antimagic charge cost into a quality cost.
+/obj/item/organ/cyberimp/brain/mental_shielding/proc/process_quality_cost(raw_cost)
+ if(raw_cost <= 0 || !premium_component)
+ return 0
+ var/efficiency = premium_component.get_efficiency() || 0
+ if(efficiency <= 0)
+ return 0
+ var/mult = AUGMENTED_PREMIUM_QUALITY_MINOR * (1 / efficiency)
+ return max(1, round(raw_cost * mult))
+
+/obj/item/organ/cyberimp/brain/mental_shielding/use_action()
+ if(!owner)
+ return FALSE
+ if(!enabled && !COOLDOWN_FINISHED(src, emp_reenable_cooldown))
+ to_chat(owner, span_warning("Your [name] is temporarily disabled from EMP interference."))
+ return FALSE
+ enabled = !enabled
+ if(enabled)
+ ADD_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT)
+ to_chat(owner, span_notice("Your [name] is toggled on; it will now block any mental effects targeting you."))
+ else
+ REMOVE_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT)
+ to_chat(owner, span_notice("Your [name] is toggled off!"))
+ return enabled
+
+/obj/item/organ/cyberimp/brain/mental_shielding/is_action_active()
+ return enabled
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm
new file mode 100644
index 00000000000000..1db7d4da3158e3
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm
@@ -0,0 +1,128 @@
+/*
+ Its an arm that makes you punch harder and be activated to punch EVEN HARDER.
+*/
+/datum/power/augmented/pneumatic_arm
+ name = "Premium DSTR Pneumatic Arm"
+ desc = "A popular choice for the augmented bodyguards and manufactured by Praetor Dynamics. Passively increases your punch damage by +5 with that arm. \
+ \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 1 space (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\
+ \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)."
+ security_record_text = "Subject has a DSTR Pneumatic Arm, increasing their lethality with unarmed strikes."
+ security_threat = POWER_THREAT_MAJOR
+
+ value = 4 // balance around 2 arms.
+ augment = /obj/item/organ/cyberimp/arm/pneumatic_arm
+
+/obj/item/organ/cyberimp/arm/pneumatic_arm
+ name = "DSTR Pneumatic Arm"
+ desc = "A popular choice for the augmented bodyguards and manufactured by Praetor Dynamics. Passively increases your punch damage by +5 with that arm. \
+ \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 1 space (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\
+ \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)."
+ icon_state = "toolkit_generic"
+
+ actions_types = list(/datum/action/item_action/organ_action/premium/use)
+ premium = TRUE
+
+ /// Going to deal the extra damage + knockback when punching
+ var/overcharged = FALSE
+
+ /// Bonus damage while not active
+ var/bonus_passive_damage = 5
+ /// Bonus damage while active
+ var/bonus_active_damage = 15
+
+ /// Knockback on punch while active
+ var/knockback = 1
+ /// Is the throw 'safe'? False means it can cause wallstuns and such.
+ var/gentle_throw = FALSE
+
+ /// EMP cooldown decleration
+ COOLDOWN_DECLARE(emp_reenable_cooldown)
+ /// EMP cooldown time
+ var/emp_cooldown = 30 SECONDS
+
+/obj/item/organ/cyberimp/arm/pneumatic_arm/Initialize(mapload)
+ . = ..()
+ if(premium_component)
+ premium_component.refurb_parts = list(
+ /obj/item/stack/sheet/iron = 5,
+ /obj/item/stack/sheet/plasteel = 2,
+ /obj/item/stack/cable_coil = 2,
+ /obj/item/stock_parts/servo/femto = 1)
+
+/obj/item/organ/cyberimp/arm/pneumatic_arm/on_mob_insert(mob/living/carbon/arm_owner)
+ . = ..()
+ RegisterSignal(arm_owner, COMSIG_HUMAN_UNARMED_HIT, PROC_REF(on_unarmed_hit))
+
+/obj/item/organ/cyberimp/arm/pneumatic_arm/on_mob_remove(mob/living/carbon/arm_owner)
+ . = ..()
+ UnregisterSignal(arm_owner, COMSIG_HUMAN_UNARMED_HIT)
+
+// On EMP
+/obj/item/organ/cyberimp/arm/pneumatic_arm/emp_act(severity)
+ . = ..()
+ if(. & EMP_PROTECT_SELF)
+ return
+ if(premium_component)
+ premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR)
+ overcharged = FALSE
+ COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown)
+ premium_component?.update_quality_actions()
+ to_chat(owner, span_warning("Your [name] becomes disabled!"))
+
+/// Triggers the on-hit with punch effects, either pasive or active.
+/obj/item/organ/cyberimp/arm/pneumatic_arm/proc/on_unarmed_hit(mob/living/user, mob/living/target, obj/item/bodypart/affecting, damage, armor_block, limb_sharpness)
+ SIGNAL_HANDLER
+ if(!target || !premium_component?.can_function())
+ return
+
+ // No bonus damage if EMP'd
+ if(!COOLDOWN_FINISHED(src, emp_reenable_cooldown))
+ return
+
+ // Only applies bonus damage when the arm is the active arm.
+ if(user.get_active_hand() != hand)
+ return
+
+ var/efficiency = premium_component.get_efficiency()
+ if(efficiency <= 0)
+ return
+
+ // Bonus damage when punching
+ var/passive_damage = round(bonus_passive_damage * efficiency, DAMAGE_PRECISION)
+ if(passive_damage > 0)
+ target.apply_damage(passive_damage, BRUTE, affecting, armor_block, sharpness = limb_sharpness)
+
+ // If active; smack extra-hard.
+ if(overcharged)
+ var/active_damage = round(bonus_active_damage * efficiency, DAMAGE_PRECISION)
+ if(active_damage > 0)
+ target.apply_damage(active_damage, BRUTE, affecting, armor_block, sharpness = limb_sharpness)
+
+ if(ismovable(target))
+ var/throw_dir = get_dir(user, target)
+ if(throw_dir)
+ var/atom/throw_target = get_edge_target_turf(target, throw_dir)
+ target.throw_at(throw_target, knockback, 2, user, gentle = gentle_throw)
+ to_chat(target, span_userdanger("[user]'s punch sends you flying!"))
+ playsound(target, 'sound/items/weapons/resonator_blast.ogg', 75, TRUE)
+ premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR)
+
+/obj/item/organ/cyberimp/arm/pneumatic_arm/use_action()
+ if(!owner)
+ return FALSE
+ if(!overcharged && !COOLDOWN_FINISHED(src, emp_reenable_cooldown))
+ to_chat(owner, span_warning("Your [name] is temporarily disabled from EMP interference."))
+ return FALSE
+ if(!premium_component?.can_function())
+ to_chat(owner, span_warning("Your [name] fails to respond; it seems broken!"))
+ return FALSE
+ if(overcharged)
+ to_chat(owner, span_notice("You return your [name] to its standard settings."))
+ overcharged = FALSE
+ return TRUE
+ overcharged = TRUE
+ to_chat(owner, span_notice("You overcharge your [name]. Your next punch will knock back your target."))
+ return TRUE
+
+/obj/item/organ/cyberimp/arm/pneumatic_arm/is_action_active()
+ return overcharged
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm
new file mode 100644
index 00000000000000..dcbbaff1c291dd
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm
@@ -0,0 +1,154 @@
+/*
+ Its an arm that makes you punch harder and be activated to punch EVEN HARDER.
+*/
+/datum/power/augmented/precognition_eyes
+ name = "Premium PRCG Precognitive Scanners"
+ desc = "Though some market it as being able to see the future, this invention by Oracle Neuro-Systems is instead a specialized AI recognition model hooked into a BULLET DODGER skillchip, allowing you to automatically dodge any incoming projectiles.\
+ \n This doesn't come without drawbacks, as the visual load is exhausting and suffers from the same drawbacks as the skillchip by tiring you out, causing more exhaustion than usual. This has no safeguard, meaning you can be stamina-critted by any projectiles.\
+ \n Requires a BULLET DODGER Skillchip to function; comes pre-packaged with one at roundstart."
+ security_record_text = "Subject has PRCG Precognitive Scanners, allowing them to automatically dodge projectiles at the cost of their stamina."
+ security_threat = POWER_THREAT_MAJOR // it is still a chemsprayer if you put murder chems in this it will kill
+
+ value = 8
+ augment = /obj/item/organ/eyes/robotic/precognition_eyes
+
+/obj/item/organ/eyes/robotic/precognition_eyes
+ name = "PRCG Precognitive Scanners"
+ desc = "Though some market it as being able to see the future, this invention by Oracle Neuro-Systems is instead a specialized AI recognition model hooked into a BULLET DODGER skillchip, allowing you to automatically dodge any incoming projectiles.\
+ \n This doesn't come without drawbacks, as the visual load is exhausting and suffers from the same drawbacks as the skillchip by tiring you out, causing more exhaustion than usual. This has no safeguard, meaning you can be stamina-critted by any projectiles.\
+ \n Requires a BULLET DODGER Skillchip to function."
+ icon_state = "eyes_cyber_xray"
+
+ actions_types = list(/datum/action/item_action/organ_action/premium/use)
+ premium = TRUE
+ /// On or off state of the implant
+ var/enabled = TRUE
+
+ /// How much quality do we lose on trigger?
+ var/quality_loss = AUGMENTED_PREMIUM_QUALITY_MINOR / 2
+ /// Skillchip installed by this augment.
+ var/obj/item/skillchip/installed_chip
+ /// Did we add an extra skillchip slot?
+ var/added_skillchip_slot = FALSE
+ /// The minimum stamloss gained from this. Normally it is the projectile's damage * efficiency.
+ var/dodge_stamloss = 30 // higher than normal taunting. Git gud.
+ /// EMP cooldown decleration
+ COOLDOWN_DECLARE(emp_reenable_cooldown)
+ /// EMP cooldown duration
+ var/emp_cooldown = 30 SECONDS
+
+
+/obj/item/organ/eyes/robotic/precognition_eyes/Initialize(mapload)
+ . = ..()
+ if(premium_component)
+ premium_component.refurb_parts = list(
+ /obj/item/stack/sheet/glass = 2,
+ /obj/item/stack/cable_coil = 1,
+ /obj/item/stock_parts/scanning_module/triphasic = 1)
+
+// Listeners if we are about to be hit by projectiles.
+/obj/item/organ/eyes/robotic/precognition_eyes/on_mob_insert(mob/living/carbon/owner_mob)
+ . = ..()
+ grant_matrix_taunt(owner_mob)
+ RegisterSignal(owner_mob, COMSIG_PROJECTILE_PREHIT, PROC_REF(on_projectile_prehit))
+
+/obj/item/organ/eyes/robotic/precognition_eyes/on_mob_remove(mob/living/carbon/owner_mob)
+ . = ..()
+ if(owner_mob)
+ UnregisterSignal(owner_mob, COMSIG_PROJECTILE_PREHIT)
+ remove_matrix_taunt(owner_mob)
+
+/// Grants the skillchip that's required to use it
+/obj/item/organ/eyes/robotic/precognition_eyes/proc/grant_matrix_taunt(mob/living/carbon/owner_mob)
+ if(!owner_mob || installed_chip)
+ return
+ var/obj/item/organ/brain/brain = owner_mob.get_organ_slot(ORGAN_SLOT_BRAIN)
+ if(!brain)
+ return
+ if(has_matrix_taunt(brain))
+ return
+ brain.max_skillchip_slots += 1
+ added_skillchip_slot = TRUE
+ installed_chip = new /obj/item/skillchip/matrix_taunt()
+ owner_mob.implant_skillchip(installed_chip, force = TRUE)
+ installed_chip.try_activate_skillchip(silent = TRUE, force = TRUE)
+
+/// Removes the skillchip; you don't get to keep it without the augment.
+/obj/item/organ/eyes/robotic/precognition_eyes/proc/remove_matrix_taunt(mob/living/carbon/owner_mob)
+ if(!owner_mob)
+ return
+ var/obj/item/organ/brain/brain = owner_mob.get_organ_slot(ORGAN_SLOT_BRAIN)
+ if(added_skillchip_slot && brain)
+ brain.max_skillchip_slots = max(brain.max_skillchip_slots - 1, 0)
+ brain.update_skillchips()
+ added_skillchip_slot = FALSE
+ if(installed_chip)
+ owner_mob.remove_skillchip(installed_chip, silent = TRUE)
+ QDEL_NULL(installed_chip)
+
+/// Checks if we have the required skillchip.
+/obj/item/organ/eyes/robotic/precognition_eyes/proc/has_matrix_taunt(obj/item/organ/brain/brain)
+ if(!brain || !length(brain.skillchips))
+ return FALSE
+ for(var/obj/item/skillchip/skillchip as anything in brain.skillchips)
+ if(istype(skillchip, /obj/item/skillchip/matrix_taunt))
+ return TRUE
+ return FALSE
+
+// On EMP
+/obj/item/organ/eyes/robotic/precognition_eyes/emp_act(severity)
+ . = ..()
+ if(. & EMP_PROTECT_SELF)
+ return
+ if(premium_component)
+ premium_component.adjust_quality(-quality_loss)
+ enabled = FALSE
+ COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown)
+ premium_component?.update_quality_actions()
+ to_chat(owner, span_warning("Your [name] becomes disabled!"))
+
+// On using the action.
+/obj/item/organ/eyes/robotic/precognition_eyes/use_action()
+ if(!owner)
+ return FALSE
+ if(!enabled && !COOLDOWN_FINISHED(src, emp_reenable_cooldown))
+ to_chat(owner, span_warning("Your [name] is temporarily disabled from EMP interference."))
+ return FALSE
+ enabled = !enabled
+ if(enabled)
+ to_chat(owner, span_notice("Your [name] is toggled on; it will now auto-dodge projectiles."))
+ else
+ to_chat(owner, span_notice("Your [name] is toggled off."))
+ return enabled
+
+/obj/item/organ/eyes/robotic/precognition_eyes/is_action_active()
+ return enabled
+
+/// Applies the dodge effects on pre-hit.
+/obj/item/organ/eyes/robotic/precognition_eyes/proc/on_projectile_prehit(mob/living/source, obj/projectile/proj)
+ SIGNAL_HANDLER
+ if(source != owner)
+ return NONE
+ if(!enabled)
+ return NONE
+ if(!premium_component?.can_function())
+ return NONE
+ if(source.stat != CONSCIOUS || HAS_TRAIT(source, TRAIT_INCAPACITATED))
+ return NONE
+ if(HAS_TRAIT(source, TRAIT_UNHITTABLE_BY_PROJECTILES))
+ return NONE
+ ADD_TRAIT(source, TRAIT_UNHITTABLE_BY_PROJECTILES, AUGMENTATION_TRAIT)
+
+ // stam + quality loss.
+ var/efficiency = premium_component?.get_efficiency() || 1
+ var/base_cost = dodge_stamloss
+ // If the projectile deals more damage, we use that for stamina cost instead of dodge_stamloss.
+ if(proj)
+ base_cost = max(base_cost, proj.damage)
+ source.adjustStaminaLoss(round(base_cost * (1 / max(efficiency, 0.01))))
+ premium_component?.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR)
+ source.visible_message(span_warning("[source] dodges the [proj] with little effort!"), span_danger("You automatically dodge the [proj]!"))
+
+ addtimer(TRAIT_CALLBACK_REMOVE(source, TRAIT_UNHITTABLE_BY_PROJECTILES, AUGMENTATION_TRAIT), 0.1 SECONDS)
+ source.block_projectile_effects() // does all the vfx
+ return PROJECTILE_INTERRUPT_HIT_PHASE
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm
new file mode 100644
index 00000000000000..8931749dff07e5
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm
@@ -0,0 +1,196 @@
+/*
+ Spray reagents EVERYWHERE!
+*/
+/datum/power/augmented/reagent_cannon
+ name = "Premium SPRY Reagent Cannon"
+ desc = "Usually included in various company contracts, those who work in mega-scale botanics and cleaning need to push for optimal efficiency. Manufactured by Nex-Zephyr, this beauty will be your lifelong replacement of a spray bottle.\
+ \n When activated, transform your arm into a chemsprayer, allowing you to deploy chemicals rapidly in a large area. Capable of containing up to 600 chemicals. \
+ \n Because this is an incredibly invasive augment, this requires a cybernetic arm to wield effectively. Your arm will be replaced with a synthetic variant at roundstart to facilitate this."
+ security_record_text = "Subject has an industrial SRPY Reagent cannon embedded in their arm."
+ security_threat = POWER_THREAT_MAJOR // it is still a chemsprayer if you put murder chems in this it will kill
+
+ value = 5
+ augment = /obj/item/organ/cyberimp/arm/toolkit/reagent_cannon
+
+// Replaces the existing arm with a robot limb.
+/datum/power/augmented/reagent_cannon/add_unique(client/client_source)
+ var/mob/living/carbon/human/human_holder = power_holder
+ if(!augment || !human_holder)
+ return
+ var/augment_left = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_left)
+ var/augment_right = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_right)
+ var/left_match = augment_matches_pref(augment_left)
+ var/right_match = augment_matches_pref(augment_right)
+
+ if(left_match)
+ replace_arm_with_robot(human_holder, BODY_ZONE_L_ARM)
+ if(right_match)
+ replace_arm_with_robot(human_holder, BODY_ZONE_R_ARM)
+ return ..()
+
+/// Swaps your arm with a robotic one because feeble human arms aren't good enough for this.
+/datum/power/augmented/reagent_cannon/proc/replace_arm_with_robot(mob/living/carbon/human/human_holder, arm_zone)
+ if(!human_holder)
+ return
+ var/obj/item/bodypart/existing = human_holder.get_bodypart(arm_zone)
+ if(existing && (existing.bodytype & BODYTYPE_ROBOTIC)) // we already have robo arms.
+ return
+ if(arm_zone == BODY_ZONE_L_ARM)
+ human_holder.del_and_replace_bodypart(new /obj/item/bodypart/arm/left/robot, special = TRUE)
+ else if(arm_zone == BODY_ZONE_R_ARM)
+ human_holder.del_and_replace_bodypart(new /obj/item/bodypart/arm/right/robot, special = TRUE)
+
+/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon
+ name = "SPRY Reagent Cannon"
+ desc = "Usually included in various company contracts, those who work in mega-scale botanics and cleaning need to push for optimal efficiency. Manufactured by Nex-Zephyr, this beauty will be your lifelong replacement of a spray bottle.\
+ \n When activated, transform your arm into a chemsprayer, allowing you to deploy chemicals rapidly in a large area. Capable of containing up to 600 chemicals. \
+ \n Because this is an incredibly invasive augment, this requires a cybernetic arm to wield effectively."
+ icon = 'icons/obj/weapons/guns/ballistic.dmi'
+ icon_state = "chemsprayer"
+
+ actions_types = list(/datum/action/item_action/organ_action/premium/use)
+ premium = TRUE
+
+ items_to_create = list(/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon)
+
+ /// Base chance not to consume quality on spray, scaling with amount sprayed and quality.
+ var/quality_chance = 40
+
+ /// EMP cooldown declaration
+ COOLDOWN_DECLARE(emp_reenable_cooldown)
+ /// EMP cooldown duration
+ var/emp_cooldown = 30 SECONDS
+
+/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/Initialize(mapload)
+ . = ..()
+ if(premium_component)
+ premium_component.refurb_parts = list(
+ /obj/item/stack/sheet/plastic = 5,
+ /obj/item/stack/sheet/iron = 2,
+ /obj/item/stack/cable_coil = 2,
+ /obj/item/stock_parts/matter_bin/bluespace = 1)
+
+// Only fits in cybernetic arms because fluff and also how the fuck does it fit elsewhere.
+/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/on_mob_insert(mob/living/carbon/arm_owner)
+ . = ..()
+ if(!has_robotic_arm())
+ to_chat(arm_owner, span_warning("Your [name] does not fit in a non-cybernetic arm!"))
+ return
+
+// On EMP
+/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/emp_act(severity)
+ . = ..()
+ if(. & EMP_PROTECT_SELF)
+ return
+ if(premium_component)
+ premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR)
+ Retract()
+ COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown)
+ premium_component?.update_quality_actions()
+ to_chat(owner, span_warning("Your [name] becomes disabled!"))
+
+/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/use_action()
+ if(!owner)
+ return FALSE
+ if(!has_robotic_arm())
+ to_chat(owner, span_warning("Your [name] can't function with a non-cybernetic arm."))
+ return FALSE
+ if(!premium_component?.can_function())
+ to_chat(owner, span_warning("Your [name] fails to respond; it seems broken!"))
+ return FALSE
+ if(!COOLDOWN_FINISHED(src, emp_reenable_cooldown) && !is_action_active())
+ to_chat(owner, span_warning("Your [name] is temporarily disabled from EMP interference."))
+ return FALSE
+ var/obj/item/active = active_item
+ if(active && !(active in src))
+ return Retract()
+ if(!LAZYLEN(contents))
+ return FALSE
+ Extend(contents[1])
+ return TRUE
+
+/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/is_action_active()
+ return active_item && !(active_item in src)
+
+/// All around check if theres a robotic arm.
+/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/proc/has_robotic_arm()
+ var/obj/item/bodypart/arm_part = hand
+ if(!arm_part)
+ return FALSE
+ return (arm_part.bodytype & BODYTYPE_ROBOTIC)
+
+/// Chance to deduct quality based on amount used.
+/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/proc/on_spray_used(reagents_used)
+ if(!premium_component)
+ return
+ if(!premium_component.can_function())
+ return
+ var/efficiency = premium_component.get_efficiency()
+ var/chance_no_consume = (quality_chance * efficiency) - max(reagents_used, 0)
+ if(prob(clamp(chance_no_consume, 0, 100)))
+ return
+ premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_TRIVIAL * 2)
+
+
+// The chem sprayer specifically designed for the augment.
+/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon
+ name = "Premium SPRY Reagent Cannon"
+ desc = "A chem sprayer integrated into a premium arm augment. Really it's a miracle you even have an operable hand with the size of this thing. Comes with a 'focused' mode which tightens the spread of the cannon."
+ var/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/host_implant
+ /// 0 = spray wide, 1 = stream wide, 2 = spray focused, 3 = stream focused
+ var/mode = 0
+ /// Focused mode only targets the center tile (1-wide)
+ var/focused_mode = FALSE
+
+/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/Initialize(mapload)
+ . = ..()
+ if(istype(loc, /obj/item/organ/cyberimp/arm/toolkit/reagent_cannon))
+ host_implant = loc
+
+// We use a delta to get the amount we used and then pass that along to the augment for quality degredation.
+/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/try_spray(atom/target, mob/user)
+ var/before = reagents?.total_volume || 0
+ . = ..()
+ if(.)
+ var/after = reagents?.total_volume || 0
+ var/used = max(before - after, 0)
+ host_implant?.on_spray_used(used)
+ return .
+
+// Allows us to basically toggle between 1x or 3x spray.
+/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/spray(atom/A, mob/user)
+ if(!host_implant?.premium_component.can_function())
+ to_chat(user, span_warning("Your [name] fails to respond; it seems broken!"))
+ return FALSE
+ var/turf/target_turf = get_turf(A)
+ if(focused_mode)
+ call(src, /obj/item/reagent_containers/spray/proc/spray)(target_turf, user) // only way we can get a 1x1 spray because the chemsprayer is our parent and that overrides standard spray rules.
+ return
+ ..()
+
+// Allows us to switch between focused (1x wide) or unfocused (3x wide)
+/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/toggle_stream_mode(mob/user)
+ if(stream_range == spray_range || !stream_range || !spray_range || possible_transfer_amounts.len > 2 || !can_toggle_range)
+ return
+ mode = (mode + 1) % 4
+ switch(mode)
+ if(0)
+ stream_mode = FALSE
+ focused_mode = FALSE
+ current_range = spray_range
+ to_chat(user, span_notice("You switch the nozzle setting to \"spray\"."))
+ if(1)
+ stream_mode = TRUE
+ focused_mode = FALSE
+ current_range = stream_range
+ to_chat(user, span_notice("You switch the nozzle setting to \"stream\"."))
+ if(2)
+ stream_mode = FALSE
+ focused_mode = TRUE
+ current_range = spray_range
+ to_chat(user, span_notice("You switch the nozzle setting to \"spray (focused)\"."))
+ if(3)
+ stream_mode = TRUE
+ focused_mode = TRUE
+ current_range = stream_range
+ to_chat(user, span_notice("You switch the nozzle setting to \"stream (focused)\"."))
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm
new file mode 100644
index 00000000000000..bd753d65f0bf7c
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm
@@ -0,0 +1,121 @@
+/* A lot of these augments are very simple; rather than making a new .dm file for each we just put them all here.*/
+
+/*
+ARMS
+*/
+
+/datum/power/augmented/razor_claws
+ name = "Razor Claws"
+ desc = "Grants razor-sharp claws in your arms, which can be extended and retracted at will. \
+ Can also be used as wirecutters."
+
+ value = 2
+ augment = /obj/item/organ/cyberimp/arm/toolkit/razor_claws
+
+/datum/power/augmented/botany_toolkit
+ name = "Hydroponics Toolset Implant"
+ desc = "A rather simple arm implant containing tools used in gardening and botanical research."
+
+ value = 3
+ augment = /obj/item/organ/cyberimp/arm/toolkit/botany
+
+/datum/power/augmented/sanitation_toolkit
+ name = "Sanitation Toolset Implant"
+ desc = "A set of janitorial tools on the user's arm."
+
+ value = 3
+ augment = /obj/item/organ/cyberimp/arm/toolkit/janitor
+
+/datum/power/augmented/surgery_toolkit
+ name = "Surgical Toolset Implant"
+ desc = "A set of surgical tools hidden behind a concealed panel on the user's arm."
+
+ value = 5
+ augment = /obj/item/organ/cyberimp/arm/toolkit/surgery
+
+/datum/power/augmented/toolset_toolkit
+ name = "Integrated Toolset Implant"
+ desc = "A stripped-down version of the engineering cyborg toolset, designed to be installed on subject's arm. Contain advanced versions of every tool."
+
+ value = 9
+ augment = /obj/item/organ/cyberimp/arm/toolkit/toolset
+
+/datum/power/augmented/drill_arm
+ name = "Integrated Drill Implant"
+ desc = "Extending from a stabilization bracer built into the upper forearm, this implant allows for a steel mining drill to extend over the user's hand."
+
+ value = 4
+ augment = /obj/item/organ/cyberimp/arm/toolkit/mining_drill
+
+/* I'm not including this one baseline because its just too fkn stron for unarmed stacking.
+/datum/power/augmented/strong_arm
+ name = "Strong Arm Implant"
+ desc = "When implanted, this cybernetic implant will enhance the muscles of the arm to deliver more power-per-action. Install one in each arm \
+ to pry open doors with your bare hands!"
+
+ value = 10 // door forcing + unarmed stacking with cultivator make this a potential balance hazard.
+ augment = /obj/item/organ/cyberimp/arm/strongarm*/
+
+/*
+CHEST
+The game sometimes calls this spine.
+*/
+/datum/power/augmented/spinal_implant
+ name = "Herculean Gravitronic Spinal Implant"
+ desc = "This gravitronic spinal interface is able to improve the athletics of a user, allowing them greater physical ability. \
+ Contains a slot which can be upgraded with a gravity anomaly core, improving its performance."
+
+ value = 3
+ augment = /obj/item/organ/cyberimp/chest/spine
+
+/datum/power/augmented/nutriment_pump
+ name = "Nutriment Pump Implant"
+ desc = "This implant will synthesize and pump into your bloodstream a small amount of nutriment when you are starving."
+
+ value = 3
+ augment = /obj/item/organ/cyberimp/chest/nutriment
+/*
+EYE HUDS.
+Keep in mind these are HUDS. Not actual eye replacements.
+*/
+
+/datum/power/augmented/med_hud
+ name = "Medical HUD Implant"
+ desc = "These cybernetic eye implants will display a medical HUD over everything you see."
+
+ value = 4
+ augment = /obj/item/organ/cyberimp/eyes/hud/medical
+ disable_if_prisoner = FALSE
+
+/datum/power/augmented/diagnostic_hud
+ name = "Diagnostic HUD Implant"
+ desc = "These cybernetic eye implants will display a diagnostic HUD over everything you see."
+
+ value = 2
+ augment = /obj/item/organ/cyberimp/eyes/hud/diagnostic
+ disable_if_prisoner = FALSE
+
+/*
+EYES.
+Not to be confused with HUD eyes above.
+*/
+
+/datum/power/augmented/flashproof_eyes
+ name = "Shielded Robotic Eyes"
+ desc = "These reactive micro-shields will protect you from welders and flashes without obscuring your vision."
+
+ value = 4
+ augment = /obj/item/organ/eyes/robotic/shield
+ disable_if_prisoner = FALSE // don't go ripping out a man's eyes.
+
+/*
+INTERNAL (basically anything that isnt standard slots)
+*/
+
+/datum/power/augmented/skillchip_connector
+ name = "CNS Skillchip Connector Implant"
+ desc = "This cybernetic adds a port to the back of your head, where you can remove or add skillchips at will."
+
+ value = 2
+ augment = /obj/item/organ/cyberimp/brain/connector
+ disable_if_prisoner = FALSE
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery.dm
new file mode 100644
index 00000000000000..f2bfaa68420644
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery.dm
@@ -0,0 +1,128 @@
+/// Surgery to service premium augments and restore their maintenance quality.
+
+/datum/surgery/premium_augment_maintenance
+ name = "Premium augment maintenance"
+ desc = "Perform maintenance on premium augments, restoring them up to their standard operating quality."
+ surgery_flags = SURGERY_REQUIRE_RESTING | SURGERY_REQUIRE_LIMB | SURGERY_REQUIRES_REAL_LIMB
+ /// Selected premium augment to service for this surgery.
+ var/obj/item/organ/cyberimp/selected_premium
+ /// Zone used when selecting the premium augment.
+ var/selected_premium_zone
+ possible_locs = list(
+ BODY_ZONE_HEAD,
+ BODY_ZONE_CHEST,
+ BODY_ZONE_L_ARM,
+ BODY_ZONE_R_ARM,
+ BODY_ZONE_L_LEG,
+ BODY_ZONE_R_LEG,
+ BODY_ZONE_PRECISE_EYES,
+ BODY_ZONE_PRECISE_MOUTH,
+ BODY_ZONE_PRECISE_GROIN,
+ BODY_ZONE_PRECISE_L_HAND,
+ BODY_ZONE_PRECISE_R_HAND,
+ BODY_ZONE_PRECISE_L_FOOT,
+ BODY_ZONE_PRECISE_R_FOOT,
+ )
+ steps = list(
+ /datum/surgery_step/incise,
+ /datum/surgery_step/retract_skin,
+ /datum/surgery_step/clamp_bleeders,
+ /datum/surgery_step/premium_augment_access,
+ /datum/surgery_step/premium_augment_maintenance,
+ /datum/surgery_step/close,
+ )
+
+/datum/surgery/premium_augment_maintenance/can_start(mob/user, mob/living/carbon/target)
+ . = ..()
+ if(!.)
+ return .
+ var/list/premium_augments = get_premium_augments_for_zone(target, user.zone_selected)
+ return LAZYLEN(premium_augments)
+
+/// Gets any premium augments that are in the selected zone
+/datum/surgery/premium_augment_maintenance/proc/get_premium_augments_for_zone(mob/living/carbon/target, target_zone)
+ if(!target)
+ return null
+ var/list/organs = target.get_organs_for_zone(target_zone)
+ var/list/premium_augments = list()
+ for(var/obj/item/organ/organ as anything in organs)
+ var/obj/item/organ/cyberimp/implant = organ
+ if(istype(implant) && implant.premium)
+ premium_augments += implant
+ return premium_augments
+
+/// Gets which premium augment is chosen in the selected zone.
+/datum/surgery/premium_augment_maintenance/proc/get_selected_premium(mob/user, mob/living/carbon/target, target_zone, obj/item/tool)
+ if(!target)
+ return null
+
+ if(selected_premium && selected_premium.owner == target && selected_premium.premium && selected_premium.zone == target_zone)
+ return selected_premium
+
+ selected_premium = null
+ selected_premium_zone = null
+
+ var/list/premium_augments = get_premium_augments_for_zone(target, target_zone)
+ if(!LAZYLEN(premium_augments))
+ return null
+
+ if(LAZYLEN(premium_augments) == 1)
+ selected_premium = premium_augments[1]
+ selected_premium_zone = target_zone
+ return selected_premium
+
+ var/list/options = list()
+ for(var/obj/item/organ/cyberimp/implant as anything in premium_augments)
+ var/label = implant.name
+ if(options[label])
+ label = "[label] ([implant.type])"
+ options[label] = implant
+
+ var/chosen = tgui_input_list(user, "Service which premium augment?", "Surgery", sort_list(options))
+ if(isnull(chosen))
+ return null
+
+ if(!(user && target && user.Adjacent(target)))
+ return null
+
+ var/obj/item/held_tool = user.get_active_held_item()
+ if(held_tool)
+ held_tool = held_tool.get_proxy_attacker_for(target, user)
+ if(held_tool != tool)
+ return null
+
+ selected_premium = options[chosen]
+ if(!selected_premium || selected_premium.owner != target || !selected_premium.premium)
+ selected_premium = null
+ return null
+
+ selected_premium_zone = target_zone
+ return selected_premium
+
+/datum/surgery/premium_augment_maintenance/mechanic
+ name = "Premium augment maintenance"
+ requires_bodypart_type = BODYTYPE_ROBOTIC
+ surgery_flags = SURGERY_SELF_OPERABLE | SURGERY_REQUIRE_LIMB | SURGERY_CHECK_TOOL_BEHAVIOUR
+ possible_locs = list(
+ BODY_ZONE_HEAD,
+ BODY_ZONE_CHEST,
+ BODY_ZONE_L_ARM,
+ BODY_ZONE_R_ARM,
+ BODY_ZONE_L_LEG,
+ BODY_ZONE_R_LEG,
+ BODY_ZONE_PRECISE_EYES,
+ BODY_ZONE_PRECISE_MOUTH,
+ BODY_ZONE_PRECISE_GROIN,
+ BODY_ZONE_PRECISE_L_HAND,
+ BODY_ZONE_PRECISE_R_HAND,
+ BODY_ZONE_PRECISE_L_FOOT,
+ BODY_ZONE_PRECISE_R_FOOT,
+ )
+ steps = list(
+ /datum/surgery_step/mechanic_open,
+ /datum/surgery_step/open_hatch,
+ /datum/surgery_step/prepare_electronics,
+ /datum/surgery_step/premium_augment_access,
+ /datum/surgery_step/premium_augment_maintenance,
+ /datum/surgery_step/mechanic_close,
+ )
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm
new file mode 100644
index 00000000000000..e8cf88579f719f
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm
@@ -0,0 +1,154 @@
+// Custom steps for premium augment maintenance surgery.
+
+// Surgery step: open access panel before servicing.
+/datum/surgery_step/premium_augment_access
+ name = "open maintenance panel (screwdriver)"
+ implements = list(
+ TOOL_SCREWDRIVER = 100,
+ TOOL_SCALPEL = 75,
+ /obj/item/knife = 50,
+ /obj/item = 10) // 10% success with any sharp item.
+ time = 2.6 SECONDS
+ preop_sound = 'sound/items/tools/screwdriver.ogg'
+ success_sound = 'sound/items/tools/screwdriver2.ogg'
+ surgery_effects_mood = TRUE
+
+/// Gets the premium augments that exist in the selected zone.
+/datum/surgery_step/premium_augment_access/proc/get_premium_augments_for_zone(mob/living/carbon/target, target_zone)
+ if(!target)
+ return null
+ var/list/organs = target.get_organs_for_zone(target_zone)
+ var/list/premium_augments = list()
+ for(var/obj/item/organ/organ as anything in organs)
+ if(organ.premium)
+ premium_augments += organ
+ return premium_augments
+
+/datum/surgery_step/premium_augment_access/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
+ var/obj/item/organ/target_implant
+ var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery
+ if(istype(premium_surgery))
+ target_implant = premium_surgery.get_selected_premium(user, target, target_zone, tool)
+ else
+ var/list/premium_augments = get_premium_augments_for_zone(target, target_zone)
+ if(LAZYLEN(premium_augments) == 1)
+ target_implant = premium_augments[1]
+
+ if(!target_implant)
+ if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH)
+ target_zone = check_zone(target_zone)
+ to_chat(user, span_warning("You can't find any premium augments to access in [target]'s [target.parse_zone_with_bodypart(target_zone)]."))
+ return SURGERY_STEP_FAIL
+
+ if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH)
+ target_zone = check_zone(target_zone)
+ display_results(
+ user,
+ target,
+ span_notice("You begin opening the access panel to [target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)]..."),
+ span_notice("[user] begins opening an access panel in [target]'s [target.parse_zone_with_bodypart(target_zone)]."),
+ span_notice("[user] begins opening something inside [target]'s [target.parse_zone_with_bodypart(target_zone)]."),
+ )
+ display_pain(target, "You feel a sharp, uncomfortable pressure in your [target.parse_zone_with_bodypart(target_zone)]!")
+
+/datum/surgery_step/premium_augment_access/success(mob/living/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery, default_display_results = FALSE)
+ var/obj/item/organ/target_implant
+ var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery
+ if(istype(premium_surgery))
+ target_implant = premium_surgery.selected_premium
+ if(!target_implant || target_implant.owner != target || !target_implant.premium || target_implant.zone != target_zone)
+ target_implant = null
+ if(!target_implant)
+ to_chat(user, span_warning("[target] has no premium augments there to access!"))
+ return ..()
+
+ if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH)
+ target_zone = check_zone(target_zone)
+ display_results(
+ user,
+ target,
+ span_notice("You open access to [target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)]."),
+ span_notice("[user] opens access to premium augment hardware in [target]'s [target.parse_zone_with_bodypart(target_zone)]."),
+ span_notice("[user] opens access to something inside [target]'s [target.parse_zone_with_bodypart(target_zone)]."),
+ )
+ return ..()
+
+// Surgery step: perform the actual maintenance.
+/datum/surgery_step/premium_augment_maintenance
+ name = "service premium augment (multitool)"
+ implements = list(
+ TOOL_MULTITOOL = 100,
+ TOOL_WIRECUTTER = 65,
+ )
+ time = 4 SECONDS
+ preop_sound = 'sound/items/tools/ratchet.ogg'
+ success_sound = 'sound/machines/airlock/doorclick.ogg'
+ surgery_effects_mood = TRUE
+
+/// Yes you aren't seeing double. Gets the premium augments in the selected zone. They're two seperate surgries.
+/datum/surgery_step/premium_augment_maintenance/proc/get_premium_augments_for_zone(mob/living/carbon/target, target_zone)
+ if(!target)
+ return null
+ var/list/organs = target.get_organs_for_zone(target_zone)
+ var/list/premium_augments = list()
+ for(var/obj/item/organ/organ as anything in organs)
+ if(organ.premium)
+ premium_augments += organ
+ return premium_augments
+
+/datum/surgery_step/premium_augment_maintenance/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery)
+ var/obj/item/organ/target_implant
+ var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery
+ if(istype(premium_surgery))
+ target_implant = premium_surgery.get_selected_premium(user, target, target_zone, tool)
+ else
+ var/list/premium_augments = get_premium_augments_for_zone(target, target_zone)
+ if(LAZYLEN(premium_augments) == 1)
+ target_implant = premium_augments[1]
+
+ if(!target_implant)
+ if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH)
+ target_zone = check_zone(target_zone)
+ to_chat(user, span_warning("You can't find any premium augments to service in [target]'s [target.parse_zone_with_bodypart(target_zone)]."))
+ return SURGERY_STEP_FAIL
+ if(target_implant.premium_component && target_implant.premium_component.quality <= 0)
+ if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH)
+ target_zone = check_zone(target_zone)
+ to_chat(user, span_warning("[target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)] is broken and needs refurbishing first."))
+ return SURGERY_STEP_FAIL
+
+ if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH)
+ target_zone = check_zone(target_zone)
+ display_results(
+ user,
+ target,
+ span_notice("You begin servicing [target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)]..."),
+ span_notice("[user] begins servicing the premium augment hardware in [target]'s [target.parse_zone_with_bodypart(target_zone)]."),
+ span_notice("[user] begins servicing something inside [target]'s [target.parse_zone_with_bodypart(target_zone)]."),
+ )
+ display_pain(target, "You feel a sharp, uncomfortable pressure in your [target.parse_zone_with_bodypart(target_zone)]!")
+
+/datum/surgery_step/premium_augment_maintenance/success(mob/living/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery, default_display_results = FALSE)
+ var/obj/item/organ/target_implant
+ var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery
+ if(istype(premium_surgery))
+ target_implant = premium_surgery.selected_premium
+ if(!target_implant || target_implant.owner != target || !target_implant.premium || target_implant.zone != target_zone)
+ target_implant = null
+ if(!target_implant)
+ to_chat(user, span_warning("[target] has no premium augments there to service!"))
+ return ..()
+
+ target_implant.premium_component?.apply_premium_maintenance(AUGMENTED_PREMIUM_QUALITY_START)
+
+ if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH)
+ target_zone = check_zone(target_zone)
+ display_results(
+ user,
+ target,
+ span_notice("You successfully service [target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)]."),
+ span_notice("[user] successfully services [target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)]."),
+ span_notice("[user] successfully services something inside [target]'s [target.parse_zone_with_bodypart(target_zone)]."),
+ )
+ log_combat(user, target, "serviced premium augments in", addition="COMBAT MODE: [uppertext(user.combat_mode)]")
+ return ..()
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_action.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_action.dm
new file mode 100644
index 00000000000000..b2fe36bea8430e
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_action.dm
@@ -0,0 +1,3 @@
+/datum/action/cooldown/power/expert
+ name = "abstract expert power action - ahelp this"
+ resonant = FALSE
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_power.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_power.dm
new file mode 100644
index 00000000000000..6cafa2611b7a60
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_power.dm
@@ -0,0 +1,8 @@
+/datum/power/expert
+ name = "Expert Power"
+ desc = "I wanna be the very best, like no one ever was. To catch the abstract types is my real test, to report them is my cause!"
+
+ archetype = POWER_ARCHETYPE_MORTAL
+ path = POWER_PATH_EXPERT
+ priority = POWER_PRIORITY_BASIC
+ abstract_parent_type = /datum/power/expert
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm
new file mode 100644
index 00000000000000..0b4e0b432a83fd
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm
@@ -0,0 +1,37 @@
+/datum/power/expert/creature_tamer
+ name = "Creature Tamer"
+ desc = "You're always met with success when taming creatures. Grants you the 'Tame Creature' ability, allowing you to automatically tame any normally tameable creatures. Now you too can have your very own space carp pet."
+ security_record_text = "Subject has an affinity for taming creatures."
+ value = 2
+ required_powers = list(/datum/power/expert/zoologist)
+ action_path = /datum/action/cooldown/power/expert/creature_tamer
+
+/datum/action/cooldown/power/expert/creature_tamer
+ name = "Tame Creature"
+ desc = "Tame a creature that is already tameable, granting all the bonuses that you would've gained from taming it normally."
+ button_icon = 'icons/obj/clothing/neck.dmi'
+ button_icon_state = "petcollar"
+
+ target_type = /mob/living
+ target_range = 1
+ click_to_activate = TRUE
+ cooldown_time = 5
+
+/datum/action/cooldown/power/expert/creature_tamer/use_action(mob/living/user, mob/living/target)
+ if(target.stat == DEAD)
+ user.balloon_alert(user, "they're dead, they won't make for good friends like this!")
+ return FALSE
+
+ var/datum/component/tameable/tameable_component = target.GetComponent(/datum/component/tameable)
+ if(!tameable_component)
+ user.balloon_alert(user, "can't be tamed!")
+ return FALSE
+
+ // We actually unfriend them to prevent an ai issue.
+ target.unfriend(user)
+ SEND_SIGNAL(target, COMSIG_SIMPLEMOB_SENTIENCEPOTION, user) // This basically tells it to instantly succeed at being tamed.
+
+ //shows hearts to all
+ var/image/heart = image('icons/effects/effects.dmi', loc = target, icon_state = "love_hearts", layer = ABOVE_MOB_LAYER)
+ flick_overlay_global(heart, GLOB.clients, 25)
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/eye_for_ingredients.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/eye_for_ingredients.dm
new file mode 100644
index 00000000000000..8628f4db7a368c
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/eye_for_ingredients.dm
@@ -0,0 +1,10 @@
+/*
+ Gives TRAIT_REAGENT_SCANNER which is basically what science goggles do.
+*/
+
+/datum/power/expert/eye_for_ingredients
+ name = "Eye for Ingredients"
+ desc = "You've interacted with food, drinks and/or chemicals so often, you can see at a glance if something's off with it. You can see the precise reagent contents of all containers by simply examining it."
+ security_record_text = "Subject has a keen eye for spotting substances inside food, drinks and chemicals."
+ mob_trait = TRAIT_REAGENT_SCANNER
+ value = 3
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/false_power.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/false_power.dm
new file mode 100644
index 00000000000000..d8aa345a7e9dee
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/false_power.dm
@@ -0,0 +1,100 @@
+/*
+ Fill the sec records with a fake power. Or really anything else you want to write down.
+*/
+
+/datum/power/expert/false_power
+ name = "False Power"
+ desc = "A bit of misinformation about your capabilities and its immediately on record. Allows you to add a 'fake' power entry to your Security Records, tailored to your design."
+ value = 1
+
+/datum/power/expert/false_power/add(client/client_source)
+ apply_false_power_prefs(client_source)
+
+/datum/power/expert/false_power/post_add()
+ apply_false_power_prefs(power_holder?.client)
+ . = ..()
+
+/datum/power/expert/false_power/get_security_record_text()
+ var/custom_record = power_holder?.client?.prefs?.read_preference(/datum/preference/text/false_power_entry)
+ if(isnull(custom_record))
+ var/datum/preference/text/false_power_entry/pref_entry = GLOB.preference_entries[/datum/preference/text/false_power_entry]
+ custom_record = pref_entry?.create_default_value() || security_record_text
+
+ if(!istext(custom_record))
+ return security_record_text
+
+ custom_record = trim(custom_record)
+ if(isnull(reject_bad_text(custom_record, 100, ascii_only = TRUE)))
+ return security_record_text
+
+ return custom_record
+
+/// Gets the false powers settings from the user's preference.
+/datum/power/expert/false_power/proc/apply_false_power_prefs(client/client_source)
+ if(!client_source)
+ security_threat = POWER_THREAT_MINOR
+ return
+
+ var/severity_pref = client_source.prefs?.read_preference(/datum/preference/choiced/false_power_severity)
+ switch(severity_pref)
+ if("Major")
+ security_threat = POWER_THREAT_MAJOR
+ else
+ security_threat = POWER_THREAT_MINOR
+
+// Preference choice for the fake security record entry text.
+/datum/preference/text/false_power_entry
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "false_power_entry"
+ savefile_identifier = PREFERENCE_CHARACTER
+ can_randomize = FALSE
+ maximum_value_length = 100
+
+/datum/preference/text/false_power_entry/create_default_value()
+ return "Subject has been observed displaying unusual abilities."
+
+/datum/preference/text/false_power_entry/is_valid(value)
+ if(!istext(value))
+ return FALSE
+
+ var/trimmed_value = trim(value)
+ if(length(trimmed_value) < 1)
+ return FALSE
+
+ return !isnull(reject_bad_text(trimmed_value, maximum_value_length, ascii_only = TRUE))
+
+/datum/preference/text/false_power_entry/deserialize(input, datum/preferences/preferences)
+ var/value = ..()
+ if(!istext(value))
+ return null
+
+ value = trim(value)
+ if(!is_valid(value))
+ return null
+
+ return value
+
+/datum/preference/text/false_power_entry/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+// Preference choice for fake power severity in security records.
+/datum/preference/choiced/false_power_severity
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "false_power_severity"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/false_power_severity/create_default_value()
+ return "Minor"
+
+/datum/preference/choiced/false_power_severity/init_possible_values()
+ return list("Minor", "Major")
+
+/datum/preference/choiced/false_power_severity/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/datum/power_constant_data/false_power
+ associated_typepath = /datum/power/expert/false_power
+ customization_options = list(
+ /datum/preference/text/false_power_entry,
+ /datum/preference/choiced/false_power_severity
+ )
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/filthy_rich.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/filthy_rich.dm
new file mode 100644
index 00000000000000..8f9bf1d8726b25
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/filthy_rich.dm
@@ -0,0 +1,22 @@
+/*
+ Screw the rules I have EVEN MORE money.
+ Why are you even on this ship if you make this much bank.
+*/
+
+/datum/power/expert/filthy_rich
+ name = "Filthy Rich"
+ desc = "With this much disposable money it's even a question as to why you even work anymore. You start with 10000 extra credits (includes the amount from being Rich already). And probably tons more in off-shore savings accounts."
+ security_record_text = "Subject has an exorbant amount of wealth and resources at their disposal."
+ value = 8
+ required_powers = list(/datum/power/expert/rich)
+
+ // we just make it the same as rich but reduced because we are lazy.
+ var/riches = 7500
+
+/datum/power/expert/filthy_rich/add_unique(client/client_source)
+ var/mob/living/carbon/human/human_holder = power_holder
+ if(!human_holder.account_id)
+ return
+ var/datum/bank_account/account = SSeconomy.bank_accounts_by_id["[human_holder.account_id]"]
+ account.account_balance += riches
+
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/heavy_lifter.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/heavy_lifter.dm
new file mode 100644
index 00000000000000..d6e378865d3332
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/heavy_lifter.dm
@@ -0,0 +1,33 @@
+/*
+ Allows you to drag some heavy objects and people more efficiently. Also athletics boost.
+*/
+
+/datum/power/expert/heavy_lifter
+ name = "Heavy Lifter"
+ desc = "A strong back does a lot when it comes to carrying closets. You ignore the slowdown from dragging objects and having creatures grabbed and/or carried. You also start off as a Journeyman in the Athletics skill. \
+ All other slowdowns such as stamina, items, damage, etc. still apply as normal."
+ security_record_text = "Subject possesses a high degree of strength and is capable of hauling objects without being slowed down."
+ value = 5
+ /// how much xp we start with on average.
+ var/starting_xp_base = SKILL_EXP_JOURNEYMAN
+ /// tracks how much was given for removal later.
+ var/xp_given = 0
+
+/datum/power/expert/heavy_lifter/post_add()
+ ..()
+ // Grab slowdowns all share the same movespeed id.
+ power_holder.add_movespeed_mod_immunities(src, MOVESPEED_ID_MOB_GRAB_STATE)
+ power_holder.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/bulky_drag)
+ // Fireman carry slowdown.
+ power_holder.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/human_carry)
+
+ /// We give a degree of randomness to the amount of xp given.
+ var/xp_mult = rand(100, 150) / 100
+ xp_given = starting_xp_base * xp_mult
+ power_holder.mind?.adjust_experience(/datum/skill/athletics, xp_given)
+
+/datum/power/expert/heavy_lifter/remove()
+ power_holder.remove_movespeed_mod_immunities(src, MOVESPEED_ID_MOB_GRAB_STATE)
+ power_holder.remove_movespeed_mod_immunities(src, (/datum/movespeed_modifier/bulky_drag))
+ power_holder.remove_movespeed_mod_immunities(src, (/datum/movespeed_modifier/human_carry))
+ power_holder.mind?.adjust_experience(/datum/skill/athletics, -xp_given)
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/hidden_powers.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/hidden_powers.dm
new file mode 100644
index 00000000000000..2734b77124f900
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/hidden_powers.dm
@@ -0,0 +1,43 @@
+/datum/power/expert/hidden_powers
+ name = "Hidden Powers"
+ desc = "Your capabilities were never put on paper, for one reason or another. Your powers are not visible in the security records.\
+ \n If you have False Power, it will be the only preserved record of your powers."
+ value = 3
+ /// Tracks each power's original security-record visibility so we can restore it on remove.
+ var/list/original_visibility = list()
+
+/datum/power/expert/hidden_powers/add(client/client_source)
+ apply_hidden_visibility()
+
+/datum/power/expert/hidden_powers/remove()
+ restore_hidden_visibility()
+
+/// Applies the hidden flag to all powers; in essence hiding them all.
+/datum/power/expert/hidden_powers/proc/apply_hidden_visibility()
+ if(!power_holder)
+ return
+
+ for(var/datum/power/power_instance as anything in power_holder.powers)
+ if(!(power_instance in original_visibility))
+ original_visibility[power_instance] = power_instance.include_in_security_records
+
+ if(istype(power_instance, /datum/power/expert/false_power)) // false power explicitly stays visible
+ power_instance.include_in_security_records = TRUE
+ else
+ power_instance.include_in_security_records = FALSE
+
+ power_holder.refresh_security_power_records()
+
+/// Undoes the visibility changes from hidden powers
+/datum/power/expert/hidden_powers/proc/restore_hidden_visibility()
+ if(!power_holder)
+ original_visibility.Cut()
+ return
+
+ for(var/datum/power/power_instance as anything in original_visibility)
+ if(QDELETED(power_instance))
+ continue
+ power_instance.include_in_security_records = original_visibility[power_instance]
+
+ original_visibility.Cut()
+ power_holder.refresh_security_power_records()
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/master_surgeon.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/master_surgeon.dm
new file mode 100644
index 00000000000000..d918af85aca055
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/master_surgeon.dm
@@ -0,0 +1,27 @@
+/*
+ 1.5x speed/action success chance on surgery.
+ Fun fact fail_prob_index is flat amounts so we are actually giving a -50 flat which is hella busted, but also imho surgery failure chance doesn't exist outside of ghetto.
+*/
+
+/datum/power/expert/master_surgeon
+ name = "Master Surgeon"
+ desc = " Surgery takes composure and skill which you have aplenty. Increases your success rate and action speed with surgery by a factor of 1.5x."
+ security_record_text = "Subject has an unusual skill in surgery."
+ value = 4
+ /// 1.5x faster => multiply time by 1/1.5
+ var/surgery_speed_mult = 1 / 1.5
+ /// Flat reduction to failure chance (percentage points)
+ var/surgery_fail_reduction = 50
+
+/datum/power/expert/master_surgeon/add()
+ RegisterSignal(power_holder, COMSIG_LIVING_INITIATE_SURGERY_STEP, PROC_REF(apply_surgery_bonuses))
+
+/datum/power/expert/master_surgeon/remove()
+ UnregisterSignal(power_holder, COMSIG_LIVING_INITIATE_SURGERY_STEP)
+
+/// Applies the modifiers to surgery when we perform a step.
+/datum/power/expert/master_surgeon/proc/apply_surgery_bonuses(mob/living/_source, mob/living/user, mob/living/target, target_zone, obj/item/tool, datum/surgery/surgery, datum/surgery_step/step, list/modifiers)
+ SIGNAL_HANDLER
+ modifiers[FAIL_PROB_INDEX] -= surgery_fail_reduction
+ modifiers[SPEED_MOD_INDEX] *= surgery_speed_mult
+
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm
new file mode 100644
index 00000000000000..0db49f65053346
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm
@@ -0,0 +1,56 @@
+/*
+Hides your voice as unknown while active. Act out the machivalean you always wanted to be, for good or bad.
+*/
+
+/datum/power/expert/obfuscate_voice
+ name = "Obfuscate Voice"
+ desc = "Like an actor, the sheer range in your voice is enough, with a little effort, to sound like someone entirely unfamiliar. Grants the 'Obfuscate Voice' action, making your voice unrecognizeable while active."
+ security_record_text = "Subject can change their voice to be distinctly different from their normal voice."
+ value = 5
+
+ action_path = /datum/action/cooldown/power/expert/obfuscate_voice
+
+/datum/action/cooldown/power/expert/obfuscate_voice
+ name = "Obfuscate Voice"
+ desc = "Makes your voice unrecognizeable while active."
+ button_icon = 'icons/mob/actions/actions_items.dmi'
+ button_icon_state = "bci_say"
+
+ /// The current in use status effect
+ var/datum/status_effect/power/obfuscate_voice/active_effect
+
+
+/datum/action/cooldown/power/expert/obfuscate_voice/use_action(mob/living/user, atom/target)
+ if(active_effect)
+ qdel(active_effect)
+ active_effect = null
+ active = FALSE
+ return TRUE
+
+ active_effect = user.apply_status_effect(/datum/status_effect/power/obfuscate_voice, src)
+ active = TRUE
+ return TRUE
+
+// We pass it on to a status effect both as a convenient handler, and also user feedback that its active with the alert pop-up.
+/datum/status_effect/power/obfuscate_voice
+ id = "obfuscate_voice"
+ duration = STATUS_EFFECT_PERMANENT
+ alert_type = /atom/movable/screen/alert/status_effect/obfuscate_voice
+ var/datum/action/cooldown/power/expert/obfuscate_voice/source_action
+
+/datum/status_effect/power/obfuscate_voice/on_creation(mob/living/new_owner, datum/action/cooldown/power/expert/obfuscate_voice/passed_action)
+ . = ..()
+ source_action = passed_action
+
+/datum/status_effect/power/obfuscate_voice/on_apply()
+ ADD_TRAIT(owner, TRAIT_UNKNOWN_VOICE, TRAIT_STATUS_EFFECT(id))
+ return TRUE
+
+/datum/status_effect/power/obfuscate_voice/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_UNKNOWN_VOICE, TRAIT_STATUS_EFFECT(id))
+ return
+
+/atom/movable/screen/alert/status_effect/obfuscate_voice
+ name = "Obfuscate Voice"
+ desc = "Your voice is masked and will appear as 'Unknown' when speaking. Toggle the power again to disable."
+ icon_state = "mute" // swap if you have a better icon
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm
new file mode 100644
index 00000000000000..e1bcd55321d410
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm
@@ -0,0 +1,58 @@
+// Lets you speak a lot of things; but not as many lots as Curator.
+/datum/power/expert/omnilingual
+ name = "Omnilingual"
+ desc = "You speak an absurd amount of languages; you are able to understand and speak every language at full proficiency. Does not apply to languages not available to your character at character selection."
+ value = 4
+ /// Saved list of languages that were given by this power to remove when the power is removed.
+ var/list/given_languages_list = list()
+
+/datum/power/expert/omnilingual/get_security_record_text()
+ var/datum/language_holder/holder = power_holder?.get_language_holder()
+ var/total_languages = LAZYLEN(holder?.spoken_languages)
+ return "Subject has fluency in [total_languages] languages."
+
+// Iterate through the language prefs list. If they have it, skip, otherwise, give it to them and add it to given_languages_list.
+/datum/power/expert/omnilingual/add()
+ if(!power_holder)
+ return
+
+ var/datum/species/species = null
+ if(istype(power_holder, /mob/living/carbon/human))
+ var/mob/living/carbon/human/human_holder = power_holder
+ species = human_holder.dna?.species
+
+ var/datum/language_holder/lang_holder = null
+ if(species)
+ lang_holder = new species.species_language_holder()
+
+ given_languages_list = list()
+ // Doppler languages specifically filter all languages, so we mimmick those filters.
+ for (var/language_name in GLOB.all_languages_by_priority)
+ var/datum/language/language = GLOB.language_datum_instances[language_name]
+
+ // If we already have the language, skip
+ if(power_holder.has_language(language.type, ALL))
+ continue
+
+ // Skips secret languages.
+ if(language.secret && !(species && (language.type in species.language_prefs_whitelist)))
+ continue
+
+ // Trims languages not available to your species.
+ if(species && species.always_customizable && lang_holder && !(language.type in lang_holder.spoken_languages))
+ continue
+
+ power_holder.grant_language(language.type, ALL, src)
+ given_languages_list += language.type
+
+ if(lang_holder)
+ qdel(lang_holder)
+
+// Removes all languages that were given through omnilingual.
+/datum/power/expert/omnilingual/remove()
+ if(!power_holder)
+ return
+
+ for(var/datum/language/language_type as anything in given_languages_list)
+ power_holder.remove_language(language_type, ALL, src)
+ given_languages_list.Cut()
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm
new file mode 100644
index 00000000000000..2a43b0bf6a6d58
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm
@@ -0,0 +1,88 @@
+/*
+ Kicks an item horizontally/vertically/diagonally in a straight line. Dense objects stun and damage on impact, otherwise acts as a throw.
+ Inspired by the pent-up frustrations of several Cargo members on Doppler.
+ Scales with Athletics. Hit the bar so you can hit them with their MAIL.
+*/
+
+/datum/power/expert/punt
+ name = "Punt"
+ desc = "Using your foot or some other part of your body, you send an object barreling down a long distance away from you. If someone is hit by the object and it is solid, they are knocked down and take damage. \
+ Distance (and damage) scale with your Athletics skill. Double distance on crates and non-bulky objects! Requires Heavy Lifter."
+ security_record_text = "Subject has expertise in punting objects across large distances."
+ value = 3
+ required_powers = list(/datum/power/expert/heavy_lifter)
+ action_path = /datum/action/cooldown/power/expert/punt
+
+/datum/action/cooldown/power/expert/punt
+ name = "Punt"
+ desc = "You send an object barreling down a long distance away from you. If someone is hit by the object and it is solid, they are knocked down and take damage. \
+ Distance (and damage) scale with your Athletics skill. Double distance on crates and non-bulky objects!"
+ button_icon = 'icons/mob/actions/actions_elites.dmi' // another placeholder
+ button_icon_state = "herald_teleshot"
+
+ target_type = /obj/
+ target_range = 1
+ click_to_activate = TRUE
+ cooldown_time = 10
+
+ /// The base distance we punt. Keep in mind this is without the athletics bonus (they'll at least be journeyman so +2)
+ var/base_range = 1
+ /// how much damage punt impact does if its a solid object.
+ var/base_damage = 5
+
+/datum/action/cooldown/power/expert/punt/use_action(mob/living/user, obj/target)
+ if(!target || target.anchored || !isturf(target.loc))
+ user.balloon_alert(user, "can't move that!")
+ return FALSE
+
+ // Half your athletics skill rounded is added to the distance
+ var/athletics = round((user.mind?.get_skill_level(/datum/skill/athletics) || 0) / 2)
+
+ var/range = base_range + athletics
+
+ // items that are normal or smaller, or if its a crate (cargo rejoice), get punted twice as far.
+ if(istype(target, /obj/structure/closet/crate))
+ range *= 2
+ if(isitem(target))
+ var/obj/item/target_item = target
+ if(target_item.w_class <= WEIGHT_CLASS_NORMAL)
+ range *= 2
+ // If we're legendary we get a bit more throw distance; enough to be able to offscreen people.
+ if(user.mind?.get_skill_level(/datum/skill/athletics) >= SKILL_LEVEL_LEGENDARY)
+ range += 2
+
+ var/dir = get_dir(user, target)
+ user.setDir(dir)
+ var/turf/target_turf = get_ranged_target_turf(target, dir, range)
+
+ RegisterSignal(target, COMSIG_MOVABLE_IMPACT, PROC_REF(punt_impact))
+ playsound(user, 'sound/effects/meteorimpact.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+ target.throw_at(target_turf, range = range, speed = target.density ? 3 : 4, thrower = user, spin = isitem(target))
+ return TRUE
+
+/// Listener that handles on impact effect such as damage, knock down and other feedback.
+/datum/action/cooldown/power/expert/punt/proc/punt_impact(atom/movable/source, atom/hit_atom, datum/thrownthing/thrownthing)
+ SIGNAL_HANDLER
+ UnregisterSignal(source, COMSIG_MOVABLE_IMPACT)
+
+ // Base damage + athletics skill level * 2 (journeyman = 4*2=8)
+ var/damage = base_damage + round((owner.mind?.get_skill_level(/datum/skill/athletics) || 0) * 2)
+ // Dense objects are treated as damaging projectiles.
+ if(source.density)
+ if(isliving(hit_atom)) // if you manage to line up the shot you deserve this
+ var/mob/living/living_atom = hit_atom
+ var/mob/thrower = thrownthing?.get_thrower() || owner
+
+ living_atom.apply_damage(damage, BRUTE)
+ living_atom.Knockdown(2 SECONDS)
+ playsound(living_atom, 'sound/items/lead_pipe_hit.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) // I am not sorry for this choice in sound effect
+
+ // logging
+ living_atom.log_message("was punted by an object from [thrower] for [damage] damage.", LOG_VICTIM)
+ thrower.log_message("punted an object at [living_atom] for [damage] damage.", LOG_ATTACK)
+
+ if(!thrower || get_dist(thrower, hit_atom) >= 12) //if you hit someone offscreen, which can't be done without legendary or backpedaling.
+ thrower.playsound_local(thrower, 'sound/items/weapons/homerun.ogg', 75)
+ to_chat(thrower, span_boldnotice("You can't see it, but you've got a hunch you just hit a fantastic shot."))
+ else if(hit_atom.uses_integrity) // sorry about the window ma'am
+ hit_atom.take_damage(damage, BRUTE, MELEE)
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm
new file mode 100644
index 00000000000000..e1df4e26bfa752
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm
@@ -0,0 +1,19 @@
+/*
+ Screw the rules I have money.
+*/
+
+/datum/power/expert/rich
+ name = "Rich"
+ desc = "Whether through good savings, connections or just nepotism; you have way more spendable cash on hand than your peers. You start the shift with 2500 extra credits in your account."
+ value = 5
+ security_record_text = "Subject has access to a high amount of wealth and resources."
+ // how rich are we?
+ var/riches = 2500
+
+/datum/power/expert/rich/add_unique(client/client_source)
+ var/mob/living/carbon/human/human_holder = power_holder
+ if(!human_holder.account_id)
+ return
+ var/datum/bank_account/account = SSeconomy.bank_accounts_by_id["[human_holder.account_id]"]
+ account.account_balance += riches
+
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm
new file mode 100644
index 00000000000000..03a948a5ab9d18
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm
@@ -0,0 +1,24 @@
+/*
+ No item slowdowns. Basically always having your stuff coated in slime speed pots.
+*/
+
+/datum/power/expert/strider
+ name = "Strider"
+ desc = "Your strength is herculean. You ignore all slowdowns from held & worn items. \
+ You also start out at Master proficiency athletics."
+ security_record_text = "Subject has an incredibly strong physique and carry heavy equipment without issue."
+ value = 6
+ required_powers = list(/datum/power/expert/heavy_lifter)
+
+ /// how much xp we start with on average. Since the prerequisite skill gives journeyman, we subtract that.
+ var/starting_xp_base = SKILL_EXP_MASTER - SKILL_EXP_JOURNEYMAN
+
+/datum/power/expert/strider/post_add()
+ ..()
+ power_holder.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/equipment_speedmod)
+ power_holder.mind?.adjust_experience(/datum/skill/athletics, starting_xp_base)
+
+/datum/power/expert/strider/remove()
+ power_holder.remove_movespeed_mod_immunities(src, (/datum/movespeed_modifier/equipment_speedmod))
+ power_holder.mind?.adjust_experience(/datum/skill/athletics, -starting_xp_base)
+
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm
new file mode 100644
index 00000000000000..3bb38a8d008138
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm
@@ -0,0 +1,49 @@
+/*
+ Make friends with just about any simple creature. Doesn't save your friends though.
+*/
+/datum/power/expert/zoologist
+ name = "Zoologist"
+ desc = "You are capable of befriending just about any creature, given the opportunity. You gain the 'Befriend Creature' ability; using it on a mob in melee range will befriend it and any of it's other nearby cousins. \
+ This doesn't prevent them from turning hostile on other creatures. You can befriend just about any creature that can also be revived with a Lazurs Injector. There's no limit to how many creatures you can befriend."
+ security_record_text = "Subject has an unusual ability to befriend any and all animals."
+ value = 4
+
+ action_path = /datum/action/cooldown/power/expert/zoologist
+
+/datum/action/cooldown/power/expert/zoologist
+ name = "Befriend Creature"
+ desc = "Befriends a mob in melee range, as well as any of it's other nearby cousins. This doesn't prevent them from turning hostile on other creatures. \
+ You can befriend just about any creature that can also be revived with a Lazurs Injector. There's no limit to how many creatures you can befriend."
+ button_icon = 'icons/mob/simple/pets.dmi'
+ button_icon_state = "cat_sit"
+
+ target_type = /mob/living
+ target_range = 1
+ click_to_activate = TRUE
+ cooldown_time = 5
+
+/datum/action/cooldown/power/expert/zoologist/use_action(mob/living/user, mob/living/target)
+ // eligibility like Lazarus injector
+ if(!target?.compare_sentience_type(SENTIENCE_ORGANIC))
+ user.balloon_alert(user, "invalid creature!")
+ return FALSE
+ if (target.stat == DEAD)
+ user.balloon_alert(user, "they're dead, they won't make for good friends like this!")
+ return
+
+ /// sets the range which is basically screen width
+ var/range_tiles = world.view
+
+ for(var/mob/living/friendshiptarget in view(range_tiles, target))
+ // same typepath (exact) or subtype
+ if(friendshiptarget.type == target.type || istype(friendshiptarget, target.type))
+ var/image/heart = image('icons/effects/effects.dmi', loc = friendshiptarget, icon_state = "love_hearts", layer = ABOVE_MOB_LAYER)
+ friendshiptarget.flick_overlay(heart, list(user.client), 25, ABOVE_MOB_LAYER)
+ friendshiptarget.befriend(user)
+ return TRUE
+
+/obj/effect/temp_visual/tame_hearts
+ name = "hearts"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "love_hearts"
+ duration = 25
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/_command_action.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_command_action.dm
new file mode 100644
index 00000000000000..a6fb0486729a65
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_command_action.dm
@@ -0,0 +1,134 @@
+
+/* Commands work in their own little way so they get a different typepath. */
+/datum/action/cooldown/power/warfighter/command
+ name = "COMMAND abstract parent type"
+ desc = "This is what the Karens in management think they have. Great power. But really this doesn't do anything; this is just an abstract type. Demand to speak to the manager of the server and that they fix this."
+
+ click_to_activate = TRUE
+ target_self = FALSE
+ // we validate hands free differently given you can give commands with your voice and all that.
+ need_hands_free = FALSE
+ // silicons I hate to break it to you but you aren't included.
+ target_type = /mob/living/carbon
+
+ /// does this power scale off of department members in view instead of the target being in view? (for targeting non-allies usually)
+ var/department_los_scaling = FALSE
+
+ /// is the user a command staff
+ var/command_bonus = FALSE
+ /// is the target part of the user's department?
+ var/department_bonus = FALSE
+
+ /// the total effectiveness modifier for commander powers
+ var/commander_modifier = WARFIGHTER_COMMANDER_BASE_MULT
+
+ /// the symbol displayed over the target's head when using the action
+ var/action_symbol = "point"
+
+// Registers signaler for action use so we can use it as a rider for setting the command bonus.
+/datum/action/cooldown/power/warfighter/command/Grant(mob/grant_to)
+ . = ..()
+ RegisterSignal(grant_to, COMSIG_POWER_ACTION_USED, PROC_REF(on_power_action_used))
+
+/datum/action/cooldown/power/warfighter/command/Destroy()
+ return ..()
+
+/datum/action/cooldown/power/warfighter/command/Remove(mob/removed_from)
+ . = ..()
+ UnregisterSignal(removed_from, COMSIG_POWER_ACTION_USED)
+
+/// Is the user a member of the command department.
+/datum/action/cooldown/power/warfighter/command/proc/is_command_staff(mob/living/user)
+ var/datum/job/assigned = user?.mind?.assigned_role
+ if(!assigned)
+ return FALSE
+ return (assigned.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) || (assigned.job_flags & JOB_HEAD_OF_STAFF)
+
+/// Are the user and target of the same department?
+/datum/action/cooldown/power/warfighter/command/proc/is_same_department(mob/living/user, mob/living/target)
+ var/datum/job/user_job = user?.mind?.assigned_role
+ var/datum/job/target_job = target?.mind?.assigned_role
+ if(!user_job || !target_job)
+ return FALSE
+ return (user_job.departments_bitflags & target_job.departments_bitflags)
+
+/// Counts same-department allies that can perceive the user (can hear OR can see), excluding the user.
+/datum/action/cooldown/power/warfighter/command/proc/count_department_members_in_los(mob/living/user)
+ var/member_count = 0
+ for(var/mob/living/carbon/department_member in view(user))
+ if(department_member == user)
+ continue
+ if(!is_same_department(user, department_member))
+ continue
+ if(!department_member.can_hear() && !can_see(department_member, user))
+ continue
+ member_count++
+ return member_count
+
+/datum/action/cooldown/power/warfighter/command/can_use(mob/living/user, mob/living/target)
+ . = ..()
+ // If the target can't hear or see you
+ if(!target.can_hear() && !can_see(target, user))
+ owner.balloon_alert(user, "target can't perceive you!")
+ return FALSE
+ // If we can't talk nor use our hands.
+ if(!user.can_speak() && HAS_TRAIT(user, TRAIT_HANDS_BLOCKED))
+ owner.balloon_alert(user, "you're unable to relay your commands!")
+ return FALSE
+
+//// Sets commander modifier bonuses at action use time via mob-level COMSIG_POWER_ACTION_USED.
+/datum/action/cooldown/power/warfighter/command/proc/on_power_action_used(mob/living/source, datum/action/cooldown/power/action, atom/target)
+ SIGNAL_HANDLER
+ if(action != src)
+ return
+ var/mob/living/user = source
+ var/mob/living/target_mob = target
+ commander_modifier = WARFIGHTER_COMMANDER_BASE_MULT
+ command_bonus = is_command_staff(user)
+ department_bonus = FALSE
+ // If we scale off of department members in los instead of the target being department members.
+ if(department_los_scaling)
+ var/department_member_count = count_department_members_in_los(user)
+ if(department_member_count > 0)
+ commander_modifier += WARFIGHTER_COMMANDER_DEPARTMENT_BONUS * 0.5 * department_member_count
+ // Standard scaling
+ else
+ department_bonus = is_same_department(user, target_mob)
+ if(department_bonus)
+ commander_modifier += WARFIGHTER_COMMANDER_DEPARTMENT_BONUS
+ // Bonus if head of staff
+ if(command_bonus)
+ commander_modifier += WARFIGHTER_COMMANDER_HEAD_BONUS
+
+/datum/action/cooldown/power/warfighter/command/on_action_success(mob/living/user, mob/living/target)
+ SHOULD_CALL_PARENT(TRUE)
+ . = ..()
+ var/mutable_appearance/user_symbol = mutable_appearance('icons/effects/callouts.dmi', "danger")
+ user_symbol.pixel_y = 16
+ user_symbol.color = "#cc3d3d"
+ SET_PLANE_EXPLICIT(user_symbol, ABOVE_LIGHTING_PLANE, user)
+ var/mutable_appearance/target_symbol = mutable_appearance('icons/effects/callouts.dmi', action_symbol)
+ target_symbol.pixel_y = 16
+ target_symbol.color = "#cc3d3d"
+ SET_PLANE_EXPLICIT(target_symbol, ABOVE_LIGHTING_PLANE, target)
+ // applies the status effect overlay
+ user.flick_overlay_static(user_symbol, 2 SECONDS)
+ target.flick_overlay_static(target_symbol, 2 SECONDS)
+
+ /// plays the sound to only the target and the user given that it's kind-of obnoxious.
+ var/turf/origin = get_turf(user)
+ var/sound_file = 'sound/items/whistle/whistle.ogg'
+ user.playsound_local(origin, sound_file, 40, TRUE)
+ target.playsound_local(origin, sound_file, 40, TRUE)
+
+ // starts the gcd
+ start_command_gcd(user)
+
+// starts a gcd for all warfighter powers to prevent SPAM.
+/datum/action/cooldown/power/warfighter/command/proc/start_command_gcd(mob/living/user)
+ for(var/datum/action/A as anything in user.actions)
+ if(!istype(A, /datum/action/cooldown/power/warfighter/command))
+ continue
+ var/datum/action/cooldown/power/warfighter/command/C = A
+ if(C.next_use_time <= world.time)
+ C.StartCooldownSelf(2 SECONDS)
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_action.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_action.dm
new file mode 100644
index 00000000000000..c90015bdda64b4
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_action.dm
@@ -0,0 +1,4 @@
+/datum/action/cooldown/power/warfighter
+ name = "abstract expert warfighter action - ahelp this"
+ resonant = FALSE
+ background_icon_state = "bg_cult"
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_power.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_power.dm
new file mode 100644
index 00000000000000..3fad79071cbb4b
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_power.dm
@@ -0,0 +1,8 @@
+/datum/power/warfighter
+ name = "Warfighter Power"
+ desc = "Odysseus wouldn't want you to see what's inside the Trojan Horse now would they? Report this abstract type, or suffer ill consequence."
+
+ archetype = POWER_ARCHETYPE_MORTAL
+ path = POWER_PATH_WARFIGHTER
+ priority = POWER_PRIORITY_BASIC
+ abstract_parent_type = /datum/power/warfighter
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_assault.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_assault.dm
new file mode 100644
index 00000000000000..4fad2fce86e930
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_assault.dm
@@ -0,0 +1,69 @@
+/datum/power/warfighter/command_assault
+ name = "Command: Assault"
+ desc = "Command nearby allies to focus the target. The target takes 20% more damage for 15 seconds. Duration scales with your command modifier. This status effect cannot stack. \
+ \n This scales off of department members within your line of sight (at half efficiency), rather than the target being in your department. Head of staff bonuses still apply."
+ security_record_text = "Subject can rally others to increase their effective combat power against one target."
+ security_threat = POWER_THREAT_MAJOR // power literally only used for murder
+ value = 5
+ action_path = /datum/action/cooldown/power/warfighter/command/assault
+
+/datum/action/cooldown/power/warfighter/command/assault
+ name = "Command: Assault"
+ desc = "Command nearby allies to focus the target. The target takes 20% more damage for 15 seconds. Duration scales with your command modifier. This status effect cannot stack."
+
+ department_los_scaling = TRUE
+ cooldown_time = 600
+ button_icon = 'icons/hud/guardian.dmi'
+ button_icon_state = "assassin"
+ action_symbol = "attack"
+
+ /// how much extra damage the target takes
+ var/vulnerable_amount = 20
+ /// how long the effect lasts
+ var/effect_duration = 15 SECONDS
+
+/datum/action/cooldown/power/warfighter/command/assault/use_action(mob/living/user, mob/living/carbon/target)
+ target.apply_status_effect(/datum/status_effect/power/command_assault, commander_modifier, vulnerable_amount, effect_duration)
+ return TRUE
+
+/*
+ Status effect that handles the damage multiplier.
+*/
+/datum/status_effect/power/command_assault
+ id = "command_assault"
+ status_type = STATUS_EFFECT_REPLACE
+ show_duration = TRUE
+ duration = 15 SECONDS
+ alert_type = /atom/movable/screen/alert/status_effect/command_assault
+ /// Percentage increase to incoming damage while this is active.
+ var/damage_increase_percent
+
+// Gets the commander modifier, the vuln amount and effect duration from the base.
+/datum/status_effect/power/command_assault/on_creation(mob/living/new_owner, commander_modifier, vulnerable_amount, effect_duration)
+ if(isnum(commander_modifier))
+ duration = effect_duration * commander_modifier
+ if(isnum(vulnerable_amount))
+ damage_increase_percent = vulnerable_amount
+ . = ..()
+
+// Signaler for multiplying damage
+/datum/status_effect/power/command_assault/on_apply()
+ RegisterSignal(owner, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS, PROC_REF(modify_incoming_damage))
+ return TRUE
+
+// Signaler for multiplying damage
+/datum/status_effect/power/command_assault/on_remove()
+ if(owner)
+ UnregisterSignal(owner, COMSIG_MOB_APPLY_DAMAGE_MODIFIERS)
+ return
+
+/// Applies the damage mult
+/datum/status_effect/power/command_assault/proc/modify_incoming_damage(mob/living/source, list/damage_mods, damage_amount, damagetype, def_zone, sharpness, attack_direction, obj/item/attacking_item)
+ SIGNAL_HANDLER
+ damage_mods += (1 + (damage_increase_percent / 100))
+
+/atom/movable/screen/alert/status_effect/command_assault
+ name = "Command: Assault"
+ desc = "You take increased damage from all sources!"
+ icon = 'icons/hud/guardian.dmi'
+ icon_state = "assassin"
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm
new file mode 100644
index 00000000000000..bd5aa1f65ec7ac
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm
@@ -0,0 +1,56 @@
+/*
+ Gives pain negation as well as stam-damage immunity.
+*/
+/datum/power/warfighter/command_grit
+ name = "Command: Grit"
+ desc = "Whilst active, the target ignores pain for 15 seconds, as well as slowdown from damage and stamina loss. Increased effect lenghtens duration."
+ security_record_text = "Subject has an unusual charisma and can motivate others to grit through any pain or injury without slowing down."
+ security_threat = POWER_THREAT_MAJOR // you dont want this guy supporting your takedown target
+ value = 5
+ required_powers
+ action_path = /datum/action/cooldown/power/warfighter/command/grit
+ required_powers = list(/datum/power/warfighter/command_recover)
+
+/datum/action/cooldown/power/warfighter/command/grit
+ name = "Command: Grit"
+ desc = "Whilst active, the target ignores pain for 15 seconds, as well as slowdown from damage and stamina loss. Increased effect lenghtens duration."
+
+ cooldown_time = 600
+ button_icon = 'icons/hud/guardian.dmi'
+ button_icon_state = "protector"
+ action_symbol = "guard"
+
+/datum/action/cooldown/power/warfighter/command/grit/use_action(mob/living/user, mob/living/carbon/target)
+ target.apply_status_effect(/datum/status_effect/power/command_grit, commander_modifier)
+ return TRUE
+
+// Status effect that Burden Revered applies
+/datum/status_effect/power/command_grit
+ id = "command_grit"
+ show_duration = TRUE
+ duration = 15 SECONDS // baseline
+ tick_interval = -1
+ alert_type = /atom/movable/screen/alert/status_effect/command_grit
+
+/datum/status_effect/power/command_grit/on_creation(mob/living/new_owner, commander_modifier)
+ if(isnum(commander_modifier))
+ duration = 15 SECONDS * commander_modifier
+ . = ..()
+
+/datum/status_effect/power/command_grit/on_apply()
+ ADD_TRAIT(owner, TRAIT_ANALGESIA, type)
+ owner.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/damage_slowdown)
+ owner.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/basic_stamina_slowdown)
+ return TRUE
+
+/datum/status_effect/power/command_grit/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_ANALGESIA, type)
+ owner.remove_movespeed_mod_immunities(src, /datum/movespeed_modifier/damage_slowdown)
+ owner.remove_movespeed_mod_immunities(src, /datum/movespeed_modifier/basic_stamina_slowdown)
+ return
+
+/atom/movable/screen/alert/status_effect/command_grit
+ name = "Grit"
+ desc = "You ignore pain for a duration, including the slowdowns from damage and stamina!"
+ icon = 'icons/hud/guardian.dmi'
+ icon_state = "standard"
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm
new file mode 100644
index 00000000000000..ec1d149568d2b7
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm
@@ -0,0 +1,36 @@
+/*
+ Gets someone up as if shook a couple of times. Also contains the lore dump on how commander powers work and their overarching mechanics.
+ Gateway power for all the commander stuff
+*/
+/datum/power/warfighter/command_recover
+ name = "Commander"
+ desc = "There's many facets to a good leader, but being able to delegate and manage people under pressure is an art of it's own. \
+ You gain the 'Command: Recover' ability. Using it on someone will cause them to recover from stuns faster (as if shook on help intent). Has a moderate cooldown. \
+ For any and all command abilities in this category, the effect is increased if you are in the same department as the target, and even further if you are a head of staff (regardless of department). \
+ Command abilities can never be used on yourself, and require the target to be able to see or hear you."
+ security_record_text = "Subject has an unusual charisma and can motivate others to recover from incapacitating effects faster."
+ value = 4
+ action_path = /datum/action/cooldown/power/warfighter/command/recover
+
+/datum/action/cooldown/power/warfighter/command/recover
+ name = "Command: Recover"
+ desc = "Command a target to recover, with an effect similar to shaking them with help intent several times."
+
+ cooldown_time = 200
+ button_icon = 'icons/hud/guardian.dmi'
+ button_icon_state = "dextrous"
+ action_symbol = "move"
+
+ /// How much to reduce the afflictions with. Sleeping is 150% of this.
+ var/seconds_to_reduce = 6 SECONDS
+
+/datum/action/cooldown/power/warfighter/command/recover/use_action(mob/living/user, mob/living/carbon/target)
+ // Basically the same amounts as shaking up twice multiplied by commander modifiers.
+ target.AdjustStun(-seconds_to_reduce * (commander_modifier + 1))
+ target.AdjustKnockdown(-seconds_to_reduce * (commander_modifier + 1))
+ target.AdjustUnconscious(-seconds_to_reduce * (commander_modifier + 1))
+ target.AdjustSleeping(-(seconds_to_reduce * 1.5) * (commander_modifier + 1))
+ target.AdjustParalyzed(-seconds_to_reduce * (commander_modifier + 1))
+ target.AdjustImmobilized(-seconds_to_reduce * (commander_modifier + 1))
+ target.shake_up_animation() // visual
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm
new file mode 100644
index 00000000000000..9382d36bddd47a
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm
@@ -0,0 +1,329 @@
+/**
+ * Shows a live detonation countdown on the grenade's hand HUD icon.
+ * Visible to explosives specialists and to observers watching the holder.
+ * Spread into two parts: grenade_timer_hud is the inhand timer, grenade_timer_ground is the ground timer.
+ * The global manager centralizes countdown math; components handle their own visuals.
+ */
+/datum/component/grenade_timer_hud
+ /// The grenade the component is attached to
+ var/obj/item/grenade/parent_grenade
+ /// The mob currently holding the grenade
+ var/mob/holder
+ /// The visible timer element on the grenade within the hud
+ var/atom/movable/screen/timer_hud
+ /// Stored time for when the grenade should explode (roundtime)
+ var/explodes_at = 0
+ /// reference ID to the timer instance
+ var/timer_id
+ /// Everyone that can currently see the grenade.
+ var/list/current_viewers = list()
+
+/datum/component/grenade_timer_hud/Initialize()
+ if(!istype(parent, /obj/item/grenade))
+ return COMPONENT_INCOMPATIBLE
+ parent_grenade = parent
+
+/datum/component/grenade_timer_hud/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_GRENADE_ARMED, PROC_REF(on_armed))
+ RegisterSignal(parent, COMSIG_GRENADE_DETONATE, PROC_REF(on_detonate))
+ RegisterSignal(parent, COMSIG_ITEM_PICKUP, PROC_REF(on_pickup))
+ RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equipped))
+ RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_drop))
+
+/datum/component/grenade_timer_hud/UnregisterFromParent()
+ UnregisterSignal(parent, list(
+ COMSIG_GRENADE_ARMED,
+ COMSIG_GRENADE_DETONATE,
+ COMSIG_ITEM_PICKUP,
+ COMSIG_ITEM_EQUIPPED,
+ COMSIG_ITEM_DROPPED,
+ ))
+ stop_timer()
+ remove_hud()
+
+/// Listener for when the grenade is armed
+/datum/component/grenade_timer_hud/proc/on_armed(datum/source, det_time, delayoverride)
+ SIGNAL_HANDLER
+ var/delay = isnull(delayoverride) ? det_time : delayoverride
+ explodes_at = world.time + delay
+ if(!holder && ismob(parent_grenade.loc))
+ holder = parent_grenade.loc
+ update_viewers()
+ start_timer()
+
+/// Listener for when the grenade explodes
+/datum/component/grenade_timer_hud/proc/on_detonate(datum/source, lanced_by)
+ SIGNAL_HANDLER
+ stop_timer()
+ remove_hud()
+
+/// Listener for when the grenade is picked up
+/datum/component/grenade_timer_hud/proc/on_pickup(datum/source, mob/living/user)
+ SIGNAL_HANDLER
+ holder = user
+ update_viewers()
+
+/// Listener for when the grenade is equipped on our person
+/datum/component/grenade_timer_hud/proc/on_equipped(datum/source, mob/living/user, slot)
+ SIGNAL_HANDLER
+ holder = user
+ update_viewers()
+
+/// Listener for when the item is dropped
+/datum/component/grenade_timer_hud/proc/on_drop(datum/source, mob/living/user)
+ SIGNAL_HANDLER
+ if(holder == user)
+ holder = null
+ remove_hud()
+
+/// Starts the tick timer
+/datum/component/grenade_timer_hud/proc/start_timer()
+ if(timer_id)
+ return
+ timer_id = addtimer(CALLBACK(src, PROC_REF(tick)), 1 DECISECONDS, TIMER_LOOP | TIMER_STOPPABLE)
+
+/// Ends the tick timer
+/datum/component/grenade_timer_hud/proc/stop_timer()
+ if(timer_id)
+ deltimer(timer_id)
+ timer_id = null
+
+/// We use addtimer to psuedo process every deci-second and update the timer as needed.
+/datum/component/grenade_timer_hud/proc/tick()
+ if(!parent_grenade?.active)
+ stop_timer()
+ remove_hud()
+ return
+
+ update_viewers()
+ if(!timer_hud)
+ return
+
+ var/remaining = max(explodes_at - world.time, 0)
+ var/remaining_seconds = max(CEILING(remaining / 10, 1), 0)
+ timer_hud.maptext = "[remaining_seconds]"
+ timer_hud.screen_loc = parent_grenade.screen_loc
+
+/// Gets everoyne that can see the grenade
+/datum/component/grenade_timer_hud/proc/get_viewers()
+ var/list/viewers = list()
+ if(holder?.client && HAS_TRAIT(holder, TRAIT_POWER_EXPLOSIVES_SPECIALIST))
+ viewers += holder
+ if(holder?.observers?.len)
+ for(var/mob/dead/observer/O in holder.observers)
+ if(O?.client && O.client.eye == holder)
+ viewers += O
+ return viewers
+
+/// Updates the list of mobs that can view the grenade.
+/datum/component/grenade_timer_hud/proc/update_viewers()
+ if(!holder || !parent_grenade.active || parent_grenade.loc != holder)
+ remove_hud()
+ return
+
+ var/list/new_viewers = get_viewers()
+ if(!new_viewers.len)
+ remove_hud()
+ return
+
+ show_hud()
+
+ for(var/mob/M in current_viewers)
+ if(!(M in new_viewers))
+ M.client?.screen -= timer_hud
+
+ for(var/mob/M in new_viewers)
+ if(!(M in current_viewers))
+ M.client?.screen += timer_hud
+
+ current_viewers = new_viewers
+
+/// Shows the timer maptext element on the target's HUD.
+/datum/component/grenade_timer_hud/proc/show_hud()
+ if(timer_hud)
+ return
+ timer_hud = new /atom/movable/screen
+ timer_hud.layer = ABOVE_HUD_PLANE
+ timer_hud.plane = HUD_PLANE
+ timer_hud.maptext_width = 32
+ timer_hud.maptext_height = 16
+ timer_hud.maptext = "?"
+
+/// Removes the timer maptext hud element.
+/datum/component/grenade_timer_hud/proc/remove_hud()
+ if(timer_hud)
+ for(var/mob/M in current_viewers)
+ M?.client?.screen -= timer_hud
+ current_viewers = list()
+ QDEL_NULL(timer_hud)
+
+/**
+ * Registers armed grenades with the global timer manager.
+ */
+/datum/component/grenade_timer_ground
+
+/datum/component/grenade_timer_ground/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_GRENADE_ARMED, PROC_REF(on_armed))
+
+/datum/component/grenade_timer_ground/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_GRENADE_ARMED)
+
+/// Listener for when the grenade is armed.
+/datum/component/grenade_timer_ground/proc/on_armed(datum/source, det_time, delayoverride)
+ SIGNAL_HANDLER
+ GLOB.grenade_timer_manager.register_grenade(source, det_time, delayoverride)
+
+
+/**
+ * The part 2 that's respnsible for on the ground timers.
+ * Because showing text overlays to select characters isn't easy, and ghosts get the easy pass with invisibility flags.
+ */
+
+/// Global countdowns for specialists/observers looking at any armed grenade.
+GLOBAL_DATUM_INIT(grenade_timer_manager, /datum/grenade_timer_manager, new)
+
+/datum/grenade_timer_manager
+ /// List of grenades that are currently armed
+ var/list/armed_grenades = list() // /obj/item/grenade -> explode_at (world.time)
+ /// List of maptexts that every mob can see
+ var/list/viewer_images = list() // mob -> (grenade -> image)
+ /// Reference id for the timer instance
+ var/timer_id
+
+/// Registers the grenade in the timer manager.
+/datum/grenade_timer_manager/proc/register_grenade(obj/item/grenade/G, det_time, delayoverride)
+ if(QDELETED(G))
+ return
+ if(armed_grenades[G])
+ return
+ var/delay = isnull(delayoverride) ? det_time : delayoverride
+ armed_grenades[G] = world.time + delay
+ RegisterSignal(G, COMSIG_GRENADE_DETONATE, PROC_REF(on_grenade_detonate))
+ RegisterSignal(G, COMSIG_QDELETING, PROC_REF(on_grenade_deleted))
+ ensure_timer()
+
+/// Removes a grenade from the timer manager, usually when qdel'd.
+/datum/grenade_timer_manager/proc/unregister_grenade(obj/item/grenade/G)
+ if(!armed_grenades[G])
+ return
+ armed_grenades -= G
+ UnregisterSignal(G, list(COMSIG_GRENADE_DETONATE, COMSIG_QDELETING))
+ remove_grenade_images(G)
+ if(!armed_grenades.len)
+ stop_timer()
+
+/// Listener for when the grenade goes boom.
+/datum/grenade_timer_manager/proc/on_grenade_detonate(datum/source, lanced_by)
+ SIGNAL_HANDLER
+ unregister_grenade(source)
+
+/// Listener for when the grenade is DELETED
+/datum/grenade_timer_manager/proc/on_grenade_deleted(datum/source)
+ SIGNAL_HANDLER
+ unregister_grenade(source)
+
+/// Adds an active timer to the grenade when the grenade is registered and armed.
+/datum/grenade_timer_manager/proc/ensure_timer()
+ if(timer_id)
+ return
+ timer_id = addtimer(CALLBACK(src, PROC_REF(tick)), 1 DECISECONDS, TIMER_LOOP | TIMER_STOPPABLE)
+
+/// Stops the timer (I didn't know timers could be stopped on live grenades)
+/datum/grenade_timer_manager/proc/stop_timer()
+ if(timer_id)
+ deltimer(timer_id)
+ timer_id = null
+
+/// Tick proc called every decisecond by the timer.
+/datum/grenade_timer_manager/proc/tick()
+ if(!armed_grenades.len)
+ stop_timer()
+ return
+
+ var/list/eligible_viewers = get_eligible_viewers()
+
+ // Remove viewers who are no longer eligible
+ for(var/mob/M in viewer_images)
+ if(!(M in eligible_viewers))
+ remove_all_images_from(M)
+ viewer_images -= M
+
+ // Update grenade images per eligible viewer
+ for(var/obj/item/grenade/G as anything in armed_grenades)
+ if(QDELETED(G) || !G.active)
+ unregister_grenade(G)
+ continue
+
+ var/remaining = max(armed_grenades[G] - world.time, 0)
+ var/remaining_seconds = max(CEILING(remaining / 10, 1), 0)
+
+ for(var/mob/M in eligible_viewers)
+ if(can_view_grenade(M, G))
+ update_image(M, G, remaining_seconds)
+ else
+ remove_image(M, G)
+
+/// Get all mobs that can see the grenade in range.
+/datum/grenade_timer_manager/proc/get_eligible_viewers()
+ var/list/viewers = list()
+ for(var/mob/M in GLOB.player_list)
+ if(!M?.client)
+ continue
+ if(HAS_TRAIT(M, TRAIT_POWER_EXPLOSIVES_SPECIALIST) || isobserver(M))
+ viewers += M
+ return viewers
+
+/// Checks ifa mob has Line of Sight on the grenade or otherwise can see it.
+/datum/grenade_timer_manager/proc/can_view_grenade(mob/M, obj/item/grenade/G)
+ var/atom/eye = M.client?.eye || M
+ if(!eye || eye.z != G.z)
+ return FALSE
+ var/list/view_range = getviewsize(M.client?.view)
+ if(!view_range || view_range.len < 2)
+ return FALSE
+ var/range = max(view_range[1], view_range[2])
+ return get_dist(eye, G) <= range
+
+/// Updates the maptext image on the grenade.
+/datum/grenade_timer_manager/proc/update_image(mob/M, obj/item/grenade/G, remaining_seconds)
+ if(!viewer_images[M])
+ viewer_images[M] = list()
+
+ var/image/I = viewer_images[M][G]
+ if(!I)
+ I = image('icons/blanks/32x32.dmi', loc = G, icon_state = "nothing")
+ I.plane = ABOVE_LIGHTING_PLANE
+ I.layer = FLOAT_LAYER
+ I.dir = SOUTH
+ I.maptext_width = 32
+ I.maptext_height = 16
+ I.appearance_flags |= RESET_TRANSFORM|RESET_COLOR|KEEP_APART
+ viewer_images[M][G] = I
+ M.client.images += I
+
+ I.maptext = "[remaining_seconds]"
+
+/// Removes the maptext image from the grenade
+/datum/grenade_timer_manager/proc/remove_image(mob/M, obj/item/grenade/G)
+ var/list/images = viewer_images[M]
+ if(!images)
+ return
+ var/image/I = images[G]
+ if(I)
+ M.client?.images -= I
+ images -= G
+
+/// Removes ALL maptext images that the mob can see.
+/datum/grenade_timer_manager/proc/remove_all_images_from(mob/M)
+ var/list/images = viewer_images[M]
+ if(!images)
+ return
+ for(var/image/I in images)
+ M.client?.images -= I
+ images.Cut()
+
+/// Removes ALL maptext images on the grenades
+/datum/grenade_timer_manager/proc/remove_grenade_images(obj/item/grenade/G)
+ for(var/mob/M in viewer_images)
+ remove_image(M, G)
+
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm
new file mode 100644
index 00000000000000..bc4f982287f242
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm
@@ -0,0 +1,121 @@
+/*
+ Allows toggling a dual-wield stance.
+ When active, a melee attack with one weapon immediately follows with an off-hand strike.
+ Both strikes have an independent flat miss chance.
+*/
+
+#define DUAL_WIELD_OFFHAND "dual_wield_offhand"
+#define DUAL_WIELD_ATTACK_ITEM "dual_wield_attack_item"
+#define DUAL_WIELD_HAS_FORCED_MISS "dual_wield_has_forced_miss"
+#define DUAL_WIELD_FORCED_MISS "dual_wield_forced_miss"
+
+/datum/power/warfighter/dual_wielder
+ name = "Dual Wielder"
+ desc = "You can toggle a dual-wield stance. While active, striking with a melee weapon immediately follows with an off-hand strike. Both strikes have a 30% chance to miss."
+ security_record_text = "Subject knows how to efficiently fight with two melee weapons at once."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+
+ required_powers = list(/datum/power/warfighter/quick_draw)
+ action_path = /datum/action/cooldown/power/warfighter/dual_wielder
+
+/datum/action/cooldown/power/warfighter/dual_wielder
+ name = "Dual Wield"
+ desc = "Toggle dual-wielding. While active, melee attacks immediately follow with an off-hand strike (each strike has a 30% miss chance)."
+ button_icon = 'modular_doppler/modular_powers/icons/powers/actions_icons.dmi'
+ button_icon_state = "dual_wield"
+
+ // starts on
+ active = TRUE
+ /// Chance that we miss a swing
+ var/dual_wield_miss_chance = 30
+ /// Overlay for mirrored icon when active.
+ var/mutable_appearance/dual_wield_overlay
+
+/datum/action/cooldown/power/warfighter/dual_wielder/use_action(mob/living/user, atom/target)
+ active = !active
+ user.balloon_alert(user, active ? "dual wield on" : "dual wield off")
+ button_icon_state = (active ? "dual_wield" : "dual_wield_off")
+ build_all_button_icons(UPDATE_BUTTON_ICON | UPDATE_BUTTON_STATUS) // need this so the icon state updates.
+ return TRUE
+
+/datum/action/cooldown/power/warfighter/dual_wielder/Grant(mob/granted_to)
+ . = ..()
+ RegisterSignal(granted_to, COMSIG_MOB_ITEM_ATTACK, PROC_REF(on_melee_attack))
+
+/datum/action/cooldown/power/warfighter/dual_wielder/Remove(mob/removed_from)
+ . = ..()
+ UnregisterSignal(removed_from, COMSIG_MOB_ITEM_ATTACK)
+
+/// Listener for when we ATTEMPT a strike on a mob; at which point we handle our melee attack logic.
+/datum/action/cooldown/power/warfighter/dual_wielder/proc/on_melee_attack(mob/living/source, atom/target, mob/living/user, list/modifiers, list/attack_modifiers)
+ SIGNAL_HANDLER
+
+ if(source != owner)
+ return
+ if(!active)
+ return
+
+ var/is_offhand = LAZYACCESS(attack_modifiers, DUAL_WIELD_OFFHAND)
+ var/obj/item/main_item = source.get_active_held_item()
+ var/obj/item/off_item = source.get_inactive_held_item()
+ // Only apply dual-wield logic if both hands are valid melee weapons (force > 0).
+ if(!is_valid_melee_item(main_item) || !is_valid_melee_item(off_item))
+ return
+ var/obj/item/attacking_item = LAZYACCESS(attack_modifiers, DUAL_WIELD_ATTACK_ITEM) || main_item
+
+ var/forced_miss = FALSE
+ var/has_forced_miss = LAZYACCESS(attack_modifiers, DUAL_WIELD_HAS_FORCED_MISS)
+ if(has_forced_miss)
+ forced_miss = LAZYACCESS(attack_modifiers, DUAL_WIELD_FORCED_MISS)
+ var/main_miss = has_forced_miss ? forced_miss : prob(dual_wield_miss_chance)
+
+ var/offhand_attempted = FALSE
+ var/offhand_miss = FALSE
+ if(!is_offhand)
+ offhand_miss = prob(dual_wield_miss_chance)
+ offhand_attempted = try_offhand_attack(source, target, modifiers, offhand_miss)
+
+ if(main_miss)
+ if(offhand_attempted && offhand_miss) // if you miss both
+ user.do_attack_animation(target, used_item = attacking_item)
+ user.visible_message(span_warning("[user] misses with both weapons!"), span_danger("You miss with both weapons!"))
+ playsound(owner, 'sound/items/weapons/etherealmiss.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+ if(!is_offhand && offhand_attempted && !offhand_miss) // if you hit both
+ target.visible_message(span_warning("[user] lands a hit with both weapons!"), span_userdanger("You were hit by both of [user]'s weapons!"))
+ playsound(owner, 'sound/items/weapons/etherealhit.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+
+/// Validation proc to check if an item is valid to use for melee attacks.
+/datum/action/cooldown/power/warfighter/dual_wielder/proc/is_valid_melee_item(obj/item/item)
+ if(!item)
+ return FALSE
+ if(istype(item, /obj/item/offhand))
+ return FALSE
+ if(item.item_flags & NOBLUDGEON)
+ return FALSE
+ if(istype(item, /obj/item/gun))
+ return FALSE
+ if(item.force <= 0)
+ return FALSE
+ return TRUE
+
+/// Attempts an off-hand attack if it passes the vlaidation pipeline.
+/datum/action/cooldown/power/warfighter/dual_wielder/proc/try_offhand_attack(mob/living/source, atom/target, list/modifiers, offhand_miss)
+ var/obj/item/offhand = source.get_inactive_held_item()
+ if(!is_valid_melee_item(offhand))
+ return FALSE
+ if(offhand == source.get_active_held_item())
+ return FALSE
+ if(!source.Adjacent(target))
+ return FALSE
+
+ INVOKE_ASYNC(offhand, TYPE_PROC_REF(/obj/item, melee_attack_chain), source, target, modifiers, list(DUAL_WIELD_OFFHAND = TRUE, DUAL_WIELD_ATTACK_ITEM = offhand, DUAL_WIELD_HAS_FORCED_MISS = TRUE, DUAL_WIELD_FORCED_MISS = offhand_miss))
+ return TRUE
+
+#undef DUAL_WIELD_OFFHAND
+#undef DUAL_WIELD_ATTACK_ITEM
+#undef DUAL_WIELD_HAS_FORCED_MISS
+#undef DUAL_WIELD_FORCED_MISS
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/explosives_specialist.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/explosives_specialist.dm
new file mode 100644
index 00000000000000..a30b47071389ed
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/explosives_specialist.dm
@@ -0,0 +1,10 @@
+/datum/power/warfighter/explosives_specialist
+ name = "Explosives Specialist"
+ desc = "Bombs and grenades are your forte. You can see the countdown on grenades (and bombs, but practically all bombs already come with a display for DRAMATIC FLAIR)."
+ security_record_text = "Subject is specialized in explosives, and can estimate the detonation time on grenades and explosives."
+ security_threat = POWER_THREAT_MAJOR
+ value = 4
+ required_powers = list(/datum/power/warfighter/quick_draw)
+ mob_trait = TRAIT_POWER_EXPLOSIVES_SPECIALIST
+
+// See modular_doppler\modular_powers\code\powers\mortal\warfighter\components\grenade_components.dm for how we add the timers
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm
new file mode 100644
index 00000000000000..15c79e4beb765d
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm
@@ -0,0 +1,98 @@
+/datum/power/warfighter/focused_block
+ name = "Focused Block"
+ desc = "Using what you have on you, you raise your block chance by 50 for 1.5 seconds, as long as you are holding a bulky-sized item or an item with a block chance. \
+ This stacks on-top of any existing block you may have, guaranteeing blocks with most shields. Has a short cooldown."
+ security_record_text = "Subject can block attacks with extreme efficiency while wielding a shield or large object."
+ security_threat = POWER_THREAT_MAJOR
+ value = 6
+
+ action_path = /datum/action/cooldown/power/warfighter/focused_block
+ required_powers = list(/datum/power/warfighter/quick_draw)
+
+/datum/action/cooldown/power/warfighter/focused_block
+ name = "Focused Block"
+ desc = "You raise your block chance by 50 for 1.5 seconds, as long as you are holding a bulky-sized item or an item with a block chance. This stacks on-top of any existing block you may have."
+ button_icon = 'icons/obj/weapons/shields.dmi'
+ button_icon_state = "kite"
+ cooldown_time = 120
+
+// Status effect handles most of the actual effects; we check for requirements here
+/datum/action/cooldown/power/warfighter/focused_block/use_action(mob/living/user, atom/target)
+ var/obj/item/active_item = user.get_active_held_item()
+ var/obj/item/inactive_item = user.get_inactive_held_item()
+ var/has_valid_item = FALSE
+
+ if(active_item && (active_item.w_class >= WEIGHT_CLASS_BULKY || active_item.block_chance > 0))
+ has_valid_item = TRUE
+ else if(inactive_item && (inactive_item.w_class >= WEIGHT_CLASS_BULKY || inactive_item.block_chance > 0))
+ has_valid_item = TRUE
+
+ if(!has_valid_item)
+ user.balloon_alert(user, "need bulky or blocking item")
+ return FALSE
+
+ // apply status effect
+ var/datum/status_effect/power/focused_block/applied = user.apply_status_effect(/datum/status_effect/power/focused_block)
+ return !!applied
+
+// 1.5 seconds of hieghtened block
+/datum/status_effect/power/focused_block
+ id = "focused_block"
+ duration = 1.5 SECONDS
+ tick_interval = STATUS_EFFECT_NO_TICK
+ alert_type = null
+ var/block_bonus = 50
+
+/datum/status_effect/power/focused_block/on_apply()
+ if(!owner)
+ return FALSE
+ var/image/flash_overlay = new('icons/effects/effects.dmi', owner, "shield-flash", dir = pick(GLOB.cardinals))
+ owner.flick_overlay_view(flash_overlay, 30)
+ RegisterSignal(owner, COMSIG_LIVING_CHECK_BLOCK, PROC_REF(check_block))
+ return TRUE
+
+/datum/status_effect/power/focused_block/on_remove()
+ if(owner)
+ UnregisterSignal(owner, COMSIG_LIVING_CHECK_BLOCK)
+
+/// We use the COMSIG_LIVING_CHECK_BLOCK signal to check artifically for block.
+/datum/status_effect/power/focused_block/proc/check_block(mob/living/blocking_user, atom/movable/hitby, damage, attack_text, attack_type, armour_penetration, damage_type)
+ SIGNAL_HANDLER
+
+ var/has_valid_item = FALSE
+ var/best_block = 0
+ for(var/obj/item/held_item in blocking_user.held_items)
+ if(!held_item)
+ continue
+ if(held_item.w_class >= WEIGHT_CLASS_BULKY || held_item.block_chance > 0)
+ has_valid_item = TRUE
+ if(held_item.block_chance > best_block)
+ best_block = held_item.block_chance
+
+ if(!has_valid_item)
+ return NONE
+
+ // guaranteed block chance
+ best_block = max(best_block, 0)
+ var/target_block = min(100, best_block + block_bonus)
+ if(target_block >= 100)
+ block_effect(blocking_user, attack_text)
+ return SUCCESSFUL_BLOCK
+
+ // random chance for block
+ var/bonus_chance = 100 - ((100 - target_block) * 100 / (100 - best_block))
+ bonus_chance = clamp(bonus_chance, 0, 100)
+ if(!prob(bonus_chance))
+ return NONE
+ block_effect(blocking_user, attack_text)
+ return SUCCESSFUL_BLOCK
+
+/// we have to mimmick the block effects cause they're not baked into COMSIG_LIVING_CHECK_BLOCK by default.
+/datum/status_effect/power/focused_block/proc/block_effect(mob/living/blocking_user, attack_text)
+ blocking_user.visible_message(
+ span_danger("[blocking_user] blocks [attack_text]!"),
+ span_userdanger("You block [attack_text]!"),
+ )
+ var/owner_turf = get_turf(blocking_user)
+ new /obj/effect/temp_visual/block(owner_turf, COLOR_YELLOW)
+ playsound(blocking_user, 'sound/items/weapons/parry.ogg', BLOCK_SOUND_VOLUME, vary = TRUE)
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/greater_tackler.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/greater_tackler.dm
new file mode 100644
index 00000000000000..736f46af8338b1
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/greater_tackler.dm
@@ -0,0 +1,33 @@
+/*
+ +3 to skill mod, +2 to range, 0.5s to knockdown duration.
+*/
+/datum/power/warfighter/tackler/greater_tackler
+ name = "Greater Tackler"
+ desc = "Your chances of landing a succesful tackle are greatly increased, as are your range and the duration you knockdown tackled foes."
+ security_record_text = "Subject is exceedingly good at landing tackles."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+
+ required_powers = list(/datum/power/warfighter/tackler)
+
+ /// bonuses to success chance
+ var/skill_mod_bonus = 3
+ /// bonuses to range
+ var/tackle_range_bonus = 2
+ /// bonuses to knockdown duration
+ var/knockdown_bonus = 0.5 SECONDS
+
+/datum/power/warfighter/tackler/greater_tackler/post_add()
+ . = ..()
+ var/datum/component/tackler/component = power_holder.GetComponent(/datum/component/tackler)
+ if(component)
+ component.skill_mod += skill_mod_bonus
+ component.range += tackle_range_bonus
+ component.base_knockdown += knockdown_bonus
+
+/datum/power/warfighter/tackler/greater_tackler/remove()
+ var/datum/component/tackler/component = power_holder.GetComponent(/datum/component/tackler)
+ if(component)
+ component.skill_mod -= skill_mod_bonus
+ component.range -= tackle_range_bonus
+ component.base_knockdown -= knockdown_bonus
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm
new file mode 100644
index 00000000000000..1038d948014c10
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm
@@ -0,0 +1,166 @@
+/*
+ Hits everything in a 2x3 melee aoe in the targeted direction. Damages creatures, and objects, if in combat mode.
+*/
+
+/datum/power/warfighter/heavy_slam
+ name = "Heavy Slam"
+ desc = "You perform a massive, arcing strike that hits a large area. You strike the 2x3 area adjacent to you in the target direction, hitting everyone in the area (and everything, if in combat mode). \
+ A creature can only be hit once by this power, but large creatures take double damage. Requires you to actively be wielding a two-handed weapon."
+ security_record_text = "Subject can swing two-handed weapons in an enormous area."
+ security_threat = POWER_THREAT_MAJOR
+ value = 4
+
+ action_path = /datum/action/cooldown/power/warfighter/heavy_slam
+ required_powers = list(/datum/power/warfighter/quick_draw)
+
+/datum/action/cooldown/power/warfighter/heavy_slam
+ name = "Heavy Slam"
+ desc = "You strike the 2x3 area adjacent to you in the target direction, hitting everyone in the area (and everything, if in combat mode). \
+ A creature can only be hit once by this power, but large creatures take double damage. Requires you to actively be wielding a two-handed weapon."
+ button_icon = 'icons/obj/weapons/hammer.dmi'
+ button_icon_state = "hammeron"
+ cooldown_time = 120
+ click_to_activate = TRUE
+ target_self = FALSE
+
+/datum/action/cooldown/power/warfighter/heavy_slam/use_action(mob/living/user, atom/target)
+ var/obj/item/active_item = user.get_active_held_item()
+ if(!active_item)
+ user.balloon_alert(user, "requires a two-handed weapon!")
+ return FALSE
+
+ var/datum/component/two_handed/twohanded = active_item.GetComponent(/datum/component/two_handed)
+ if(!twohanded || !HAS_TRAIT(active_item, TRAIT_WIELDED))
+ user.balloon_alert(user, "requires a two-handed weapon!")
+ return FALSE
+
+ // gets what way we should swing.
+ var/dir_to_use = get_cardinal_dir(user, target)
+ if(!dir_to_use)
+ return FALSE
+ // we turn towards where we swinging.
+ user.dir = dir_to_use
+
+ //gets the area that we are going to be slamming
+ var/turf/origin = get_turf(user)
+ if(!origin)
+ return FALSE
+
+ var/list/strike_turfs = list()
+ var/dir_left = turn(dir_to_use, 90)
+ var/dir_right = turn(dir_to_use, -90)
+
+ var/turf/row1 = get_step(origin, dir_to_use)
+ var/turf/row2 = null
+
+ add_slam_row(strike_turfs, row1, dir_left, dir_right)
+ if(row1 && !is_blocked_turf(row1)) // check if we are allowed to smash through the first row.
+ row2 = get_step(row1, dir_to_use)
+ add_slam_row(strike_turfs, row2, dir_left, dir_right)
+
+ // applies the slam vfx
+ for(var/turf/strike_turf in strike_turfs)
+ new /obj/effect/temp_visual/dir_setting/warfighter_heavy_slam(strike_turf, dir_to_use)
+
+ var/turf/impact_turf = row1 ? row1 : origin
+ playsound(impact_turf, 'sound/effects/meteorimpact.ogg', 80, TRUE)
+
+ var/list/shaken_mobs = list()
+ for(var/turf/strike_turf in strike_turfs)
+ for(var/mob/living/shake_mob in view(2, strike_turf))
+ if(shake_mob in shaken_mobs)
+ continue
+ shaken_mobs += shake_mob
+ shake_camera(shake_mob, 2, 1)
+
+ RegisterSignal(active_item, COMSIG_ITEM_ATTACK_ANIMATION, PROC_REF(suppress_attack_animation))
+
+ // handles hitting mobs
+ var/list/hit_mobs = list()
+ for(var/turf/strike_turf in strike_turfs)
+ for(var/mob/living/hit_mob in strike_turf)
+ if(hit_mob == user)
+ continue
+ if(hit_mob in hit_mobs)
+ continue
+ hit_mobs += hit_mob
+
+ var/list/attack_modifiers = list()
+ if(is_multi_tile_object(hit_mob) || hit_mob.mob_size >= MOB_SIZE_LARGE)
+ attack_modifiers[FORCE_MULTIPLIER] = 2
+
+ active_item.melee_attack_chain(user, hit_mob, null, attack_modifiers)
+
+ // handles hitting objects
+ if(user.combat_mode)
+ var/list/hit_objs = list()
+ for(var/turf/strike_turf in strike_turfs)
+ for(var/obj/target_obj in strike_turf)
+ if(target_obj == active_item)
+ continue
+ if(!(isstructure(target_obj) || ismachinery(target_obj)))
+ continue
+ if(target_obj in hit_objs)
+ continue
+ hit_objs += target_obj
+ target_obj.attackby(active_item, user, null, null)
+
+ UnregisterSignal(active_item, COMSIG_ITEM_ATTACK_ANIMATION)
+
+ // short cd on hit
+ melee_cooldown_time = active_item.attack_speed
+ return TRUE
+
+/// Handles the aoe row by row. Basically gets the left and right turfs of the direction.
+/datum/action/cooldown/power/warfighter/heavy_slam/proc/add_slam_row(list/strike_turfs, turf/row_turf, dir_left, dir_right)
+ if(!row_turf)
+ return
+ if(!(row_turf in strike_turfs))
+ strike_turfs += row_turf
+
+ var/turf/left_turf = get_step(row_turf, dir_left)
+ if(left_turf && !(left_turf in strike_turfs))
+ strike_turfs += left_turf
+
+ var/turf/right_turf = get_step(row_turf, dir_right)
+ if(right_turf && !(right_turf in strike_turfs))
+ strike_turfs += right_turf
+
+/// We check if we can smash through the first row.
+/datum/action/cooldown/power/warfighter/heavy_slam/proc/is_blocked_turf(turf/row_turf)
+ if(!row_turf)
+ return TRUE
+ if(row_turf.density)
+ return TRUE
+ for(var/obj/blocked_obj in row_turf)
+ if(blocked_obj.density)
+ return TRUE
+ return FALSE
+
+/// Okay look it sounds menacing but this basically just gets the direction that we're swinging towards based on where we click.
+/datum/action/cooldown/power/warfighter/heavy_slam/proc/get_cardinal_dir(mob/living/user, atom/target)
+ if(!user)
+ return 0
+ var/dir_to_use = target ? get_dir(user, target) : user.dir
+ if(!dir_to_use)
+ return 0
+ if((dir_to_use & (NORTH | SOUTH)) && (dir_to_use & (EAST | WEST)))
+ var/dx = target ? (target.x - user.x) : 0
+ var/dy = target ? (target.y - user.y) : 0
+ if(abs(dx) >= abs(dy))
+ dir_to_use = (dx >= 0) ? EAST : WEST
+ else
+ dir_to_use = (dy >= 0) ? NORTH : SOUTH
+ return dir_to_use
+
+/// This is a pretty cringe way of doing it but uhh I am out of ideas on how to do this better.
+/// Prevents everyone in the area from being visible struck by the weapon.
+/datum/action/cooldown/power/warfighter/heavy_slam/proc/suppress_attack_animation(obj/item/source, atom/movable/attacker, atom/attacked_atom, animation_type, list/image_override, list/animation_override, list/angle_override)
+ SIGNAL_HANDLER
+ image_override += image(icon = 'icons/effects/effects.dmi', icon_state = "nothing")
+
+// Effect of the slam
+/obj/effect/temp_visual/dir_setting/warfighter_heavy_slam
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "smash"
+ duration = 3
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm
new file mode 100644
index 00000000000000..992ed0cb6ecfce
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm
@@ -0,0 +1,24 @@
+/*
+ Lets you do KRAV MAGA.
+ I am aware of the controversy with the name; but as long as it is called krav maga in the code I am going to refer to it as such in the code.
+ You are free to edit it once we get an in-setting name.
+*/
+/datum/power/warfighter/krav_maga
+ name = "Krav Maga"
+ desc = "Trained in various disarming moves, you can wield the martial arts of Krav Maga without any external assistance."
+ security_record_text = "Subject can wield Krav Maga in unarmed combat."
+ security_threat = POWER_THREAT_MAJOR
+ value = 10
+ required_powers = list(/datum/power/warfighter/martial_artist)
+ /// Uniquely, martial arts components are stored in the minds. Most powers are stored per mob, so this is a bit of an odd case.
+ var/datum/component/mindbound_martial_arts/krav_component
+
+/datum/power/warfighter/krav_maga/add()
+ if(!power_holder?.mind)
+ return
+ krav_component = power_holder.mind.AddComponent(/datum/component/mindbound_martial_arts, /datum/martial_art/krav_maga)
+
+/datum/power/warfighter/krav_maga/remove()
+ if(krav_component)
+ qdel(krav_component)
+ krav_component = null
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/martial_artist.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/martial_artist.dm
new file mode 100644
index 00000000000000..5c66ea5c034fca
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/martial_artist.dm
@@ -0,0 +1,26 @@
+/*
+ +5 to punch. Gateway to most of the martial arts stuff, just not a hard-root due to Mortal's design philosophy.
+*/
+/datum/power/warfighter/martial_artist
+ name = "Martial Artist"
+ desc = "Trained in specialized combat maneuvers, you know where to best strike your opponents. Your punches deal extra damage."
+ security_record_text = "Subject is trained in hand-to-hand combat and throws stronger punches."
+ security_threat = POWER_THREAT_MAJOR
+ value = 2
+
+ power_flags = POWER_HUMAN_ONLY
+ /// how much EEEEXTRA DEEEAAMEEEEG we do with our punches.
+ var/bonus_damage = 5
+
+/datum/power/warfighter/martial_artist/add()
+ RegisterSignal(power_holder, COMSIG_HUMAN_UNARMED_HIT, PROC_REF(on_unarmed_hit))
+
+/datum/power/warfighter/martial_artist/remove()
+ UnregisterSignal(power_holder, COMSIG_HUMAN_UNARMED_HIT)
+
+/// Sends a signal to the new signaler for unarmed punches.
+/datum/power/warfighter/martial_artist/proc/on_unarmed_hit(mob/living/user, mob/living/target, obj/item/bodypart/affecting, damage, armor_block, limb_sharpness)
+ SIGNAL_HANDLER
+ if(!target || bonus_damage <= 0)
+ return
+ target.apply_damage(bonus_damage, BRUTE, affecting, armor_block, sharpness = limb_sharpness)
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm
new file mode 100644
index 00000000000000..933e68e18becc4
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm
@@ -0,0 +1,229 @@
+/*
+ Allows you to bind with a specific item and draw any of its type on demand. Keyword type; so if you like consumable items a la flashbangs & bolas, you'll love this one.
+*/
+/datum/power/warfighter/quick_draw
+ name = "Equipment Specialist"
+ desc = "Some folks have studied warfare in their own specialized way for years, putting them on an equal ground with many others. This category includes things such as swords, shields and more. \
+ The power itself grants you the 'Quick Draw' ability, letting you 'acclimate' with an item of your choice. \
+ Whilst acclimated, you can use the power to instantly draw that type of item to your hand, as long as it is anywhere on your person, or within melee range of you. \
+ You can even use this to snag it back from your enemies."
+ security_record_text = "Subject has a high amount of manual dexterity and is hard to disarm."
+ security_threat = POWER_THREAT_MAJOR
+ value = 3
+ action_path = /datum/action/cooldown/power/warfighter/quick_draw
+
+/datum/action/cooldown/power/warfighter/quick_draw
+ name = "Quick Draw"
+ desc = "Acclimate to a held item type, then draw that item from sensible storage or nearby hands automatically into your active hand."
+ button_icon = 'icons/mob/actions/actions_slime.dmi' // placeholders out of the wazoo
+ button_icon_state = "slimeeject"
+
+ /// Cached overlay so we can cleanly update it.
+ var/mutable_appearance/bonded_overlay
+ /// Type path of the bonded item.
+ var/bonded_type
+ /// Display name for user feedback.
+ var/bonded_name
+ /// Icon file for bonded item overlay.
+ var/bonded_icon
+ /// Icon state for bonded item overlay.
+ var/bonded_icon_state
+
+/datum/action/cooldown/power/warfighter/quick_draw/use_action(mob/living/user, atom/target)
+ var/obj/item/held_item = user.get_active_held_item()
+
+ // Bind if we don't have a bonded type yet.
+ if(!bonded_type)
+ if(!held_item)
+ user.balloon_alert(user, "hold an item to acclimate")
+ return FALSE
+ if(!can_bond_item(held_item, user))
+ user.balloon_alert(user, "can't acclimate to that")
+ return FALSE
+ bonded_type = held_item.type
+ bonded_name = held_item.name
+ bonded_icon = held_item.icon
+ bonded_icon_state = held_item.icon_state
+ user.balloon_alert(user, "acclimated to [held_item]")
+ build_all_button_icons(UPDATE_BUTTON_OVERLAY)
+ return TRUE
+
+ // Rebind if holding a different item type.
+ if(held_item && !istype(held_item, bonded_type))
+ if(!can_bond_item(held_item, user))
+ user.balloon_alert(user, "can't acclimate to that")
+ return FALSE
+ bonded_type = held_item.type
+ bonded_name = held_item.name
+ bonded_icon = held_item.icon
+ bonded_icon_state = held_item.icon_state
+ user.balloon_alert(user, "reacclimated to [held_item]")
+ build_all_button_icons(UPDATE_BUTTON_OVERLAY)
+ return TRUE
+
+ if(user.get_active_held_item() && user.get_inactive_held_item())
+ user.balloon_alert(user, "hands full")
+ return FALSE
+
+ var/obj/item/target_item = find_drawable_item(user)
+ if(!target_item)
+ var/label_name = bonded_name ? bonded_name : "item"
+ user.balloon_alert(user, "no [label_name]")
+ return FALSE
+
+ if(!draw_item_to_hand(user, target_item))
+ user.balloon_alert(user, "can't draw it")
+ return FALSE
+
+ user.visible_message(
+ span_notice("[user] draws [target_item]!"),
+ span_notice("You draw [target_item]."),
+ )
+ return TRUE
+
+// Adds an overlay to the power button so that the user knows what their bonded item is.
+/datum/action/cooldown/power/warfighter/quick_draw/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force = FALSE)
+ ..()
+
+ if(!bonded_icon || !bonded_icon_state)
+ if(bonded_overlay)
+ current_button.cut_overlay(bonded_overlay)
+ bonded_overlay = null
+ return
+
+ if(bonded_overlay)
+ current_button.cut_overlay(bonded_overlay)
+
+ bonded_overlay = mutable_appearance(icon = bonded_icon, icon_state = bonded_icon_state)
+ current_button.add_overlay(bonded_overlay)
+
+/// Checks if an item can be bonded to.
+/datum/action/cooldown/power/warfighter/quick_draw/proc/can_bond_item(obj/item/held_item, mob/living/user)
+ if(!held_item || !user)
+ return FALSE
+ if(held_item.item_flags & ABSTRACT)
+ return FALSE
+ return TRUE
+
+/// Checks if an item is eligible to be quick-drawn from the user's gear.
+/datum/action/cooldown/power/warfighter/quick_draw/proc/can_quickdraw_item(obj/item/candidate_item, mob/living/user, list/equipped_items)
+ if(!candidate_item || !user)
+ return FALSE
+ if(candidate_item.item_flags & ABSTRACT)
+ return FALSE
+ // Normally you can't draw equipped items unless they're in a container (anti-cheese), but it works if its either in the pockets, the belts, the suit slot or the id.
+ var/allow_equipped_slot = FALSE
+ if(ishuman(user))
+ var/mob/living/carbon/human/human_user = user
+ var/slot_id = human_user.get_slot_by_item(candidate_item)
+ if(slot_id == ITEM_SLOT_LPOCKET || slot_id == ITEM_SLOT_RPOCKET || slot_id == ITEM_SLOT_BELT || slot_id == ITEM_SLOT_BACK || slot_id == ITEM_SLOT_SUITSTORE || slot_id == ITEM_SLOT_ID)
+ allow_equipped_slot = TRUE
+
+ if(candidate_item in equipped_items)
+ if(!allow_equipped_slot)
+ return FALSE
+ if(candidate_item.loc == user && !allow_equipped_slot)
+ return FALSE
+
+ // Reject implants/organs and abstract containers in the loc chain.
+ var/atom/current_container = candidate_item.loc
+ while(current_container && !ismob(current_container))
+ if(istype(current_container, /obj/item/implant) || istype(current_container, /obj/item/organ))
+ return FALSE
+ if(isobj(current_container))
+ var/obj/item/container_item = current_container
+ if(container_item.item_flags & ABSTRACT)
+ return FALSE
+ if(container_item.atom_storage?.locked)
+ return FALSE
+ current_container = current_container.loc
+
+ // Must be inside a storage container to count as "on your person".
+ if(!allow_equipped_slot && !isobj(candidate_item.loc))
+ return FALSE
+ var/obj/item/candidate_container = candidate_item.loc
+ if(!allow_equipped_slot && !candidate_container.atom_storage)
+ return FALSE
+
+ return TRUE
+
+/// Finds a suitable bonded item to draw.
+/datum/action/cooldown/power/warfighter/quick_draw/proc/find_drawable_item(mob/living/user)
+ if(!bonded_type || !user)
+ return null
+
+ var/list/equipped_items = user.get_equipped_items(INCLUDE_POCKETS | INCLUDE_HELD | INCLUDE_ACCESSORIES)
+ var/list/gear_items = user.get_all_gear(recursive = TRUE)
+
+ for(var/obj/item/candidate_item in gear_items)
+ if(!istype(candidate_item, bonded_type))
+ continue
+ if(!can_quickdraw_item(candidate_item, user, equipped_items))
+ continue
+ return candidate_item
+
+ // Check adjacent ground items (melee range).
+ for(var/obj/item/ground_item in view(1, user))
+ if(!istype(ground_item, bonded_type))
+ continue
+ if(ground_item.item_flags & ABSTRACT)
+ continue
+ if(!isturf(ground_item.loc))
+ continue
+ return ground_item
+
+ // Check adjacent enemies' hands only.
+ for(var/mob/living/nearby_mob in view(1, user))
+ if(nearby_mob == user)
+ continue
+ var/obj/item/active_item = nearby_mob.get_active_held_item()
+ if(istype(active_item, bonded_type) && can_take_from_other(nearby_mob, active_item))
+ return active_item
+ var/obj/item/inactive_item = nearby_mob.get_inactive_held_item()
+ if(istype(inactive_item, bonded_type) && can_take_from_other(nearby_mob, inactive_item))
+ return inactive_item
+
+ return null
+
+/// Checks if we can take an item from another mob's hand.
+/datum/action/cooldown/power/warfighter/quick_draw/proc/can_take_from_other(mob/living/other_mob, obj/item/held_item)
+ if(!other_mob || !held_item)
+ return FALSE
+ if(held_item.item_flags & ABSTRACT)
+ return FALSE
+ if(!other_mob.canUnEquip(held_item, FALSE))
+ return FALSE
+ return TRUE
+
+/// Moves the target item into the user's hands, if possible.
+/datum/action/cooldown/power/warfighter/quick_draw/proc/draw_item_to_hand(mob/living/user, obj/item/target_item)
+ if(!user || !target_item)
+ return FALSE
+
+ // taken from a mob
+ if(ismob(target_item.loc))
+ var/mob/living/holder_mob = target_item.loc
+ if(!holder_mob.canUnEquip(target_item, FALSE))
+ return FALSE
+ if(!holder_mob.transferItemToLoc(target_item, user, force = FALSE))
+ return FALSE
+ if(!holder_mob == user) // tell the person we're stealing from that we stole from them.
+ user.balloon_alert(user, "snagged")
+ holder_mob.balloon_alert(holder_mob, "[target_item.name] was snagged!")
+ return user.put_in_hands(target_item)
+
+ // took it from our person
+ if(isobj(target_item.loc))
+ var/obj/item/storage_container = target_item.loc
+ if(storage_container.atom_storage)
+ if(storage_container.atom_storage.locked)
+ return FALSE
+ if(!storage_container.atom_storage.remove_single(user, target_item, user))
+ return FALSE
+ return user.put_in_hands(target_item)
+
+ // took it from the ground
+ if(isturf(target_item.loc))
+ return user.put_in_hands(target_item)
+
+ return FALSE
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm
new file mode 100644
index 00000000000000..153b71df0e74d5
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm
@@ -0,0 +1,36 @@
+/*
+ Grants the tackle subsystem and makes you better at tackling.
+ Stats wise this is about the same strenght as the tackler gloves just with a +1 to the skill_mod
+*/
+/datum/power/warfighter/tackler
+ name = "Tackler"
+ desc = "You know how to throw a well-trained tackle. Allows you to perform tackles without assistive items and allows you to perform them better."
+ security_record_text = "Subject is trained in using tackles for takedowns."
+ security_threat = POWER_THREAT_MAJOR
+ value = 4
+
+ required_powers = list(/datum/power/warfighter/martial_artist)
+
+ /// the datum that the tackle system is in
+ var/datum/component/tackler
+
+/datum/power/warfighter/tackler/add()
+ // Taking these over from tackle gloves just for clarity. They're in here becuase I don't want to clog the upgrade vars with these + the component inherits these values so having them tweakable in vv doesnt make sense.
+ /// See: [/datum/component/tackler/var/stamina_cost]
+ var/tackle_stam_cost = 25
+ /// See: [/datum/component/tackler/var/base_knockdown]
+ var/base_knockdown = 1 SECONDS
+ /// See: [/datum/component/tackler/var/range]
+ var/tackle_range = 4
+ /// See: [/datum/component/tackler/var/min_distance]
+ var/min_distance = 0
+ /// See: [/datum/component/tackler/var/speed]
+ var/tackle_speed = 1
+ /// See: [/datum/component/tackler/var/skill_mod]
+ var/skill_mod = 2
+
+ tackler = power_holder.AddComponent(/datum/component/tackler, stamina_cost=tackle_stam_cost, base_knockdown = base_knockdown, range = tackle_range, speed = tackle_speed, skill_mod = skill_mod, min_distance = min_distance)
+
+/datum/power/warfighter/tackler/remove()
+ power_holder.RemoveComponentSource(src, /datum/component/tackler)
+
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_action.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_action.dm
new file mode 100644
index 00000000000000..96374511dc4767
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_action.dm
@@ -0,0 +1,4 @@
+/datum/action/cooldown/power/aberrant
+ name = "abstract aberrant power action - ahelp this"
+ background_icon_state = "bg_alien"
+ overlay_icon_state = "bg_alien_border"
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_power.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_power.dm
new file mode 100644
index 00000000000000..af7a0f008faa14
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_power.dm
@@ -0,0 +1,8 @@
+/datum/power/aberrant
+ name = "Aberrant Power"
+ desc = "Oh my god how horrifying; an abstract parent type! Such an abomination. Not meant for your mortal eyes."
+
+ archetype = POWER_ARCHETYPE_RESONANT
+ path = POWER_PATH_ABERRANT
+ priority = POWER_PRIORITY_BASIC
+ abstract_parent_type = /datum/power/aberrant
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root.dm
new file mode 100644
index 00000000000000..a015cca0992011
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root.dm
@@ -0,0 +1,8 @@
+/datum/power/aberrant_root
+ name = "Abstract aberrant root"
+ desc = "You've pierced beyond the veil. Gaining dark powers you were not meant to have. Repent, sinner, by telling the developers how you got this, or be faced with the developer inquisition."
+ abstract_parent_type = /datum/power/aberrant_root
+
+ archetype = POWER_ARCHETYPE_RESONANT
+ path = POWER_PATH_ABERRANT
+ priority = POWER_PRIORITY_BASIC // removing roots after the fact
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm
new file mode 100644
index 00000000000000..af2bba9bcdea6b
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm
@@ -0,0 +1,31 @@
+/*
+ Anomalous root. The anomaly root is largely a ribbon style power, but can be neat at times.
+*/
+
+/datum/power/aberrant_root/anomalous
+ name = "Anomalous Origin"
+ desc = "Things just don't add up with you. You can interact with anomalies to close them, as if you were using an anomaly neutralizer."
+ security_record_text = "Subject has unusual properties when interacting with anomalies."
+ value = 1
+
+/datum/power/aberrant_root/anomalous/add(client/client_source)
+ RegisterSignal(power_holder, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_unarmed_attack))
+
+/datum/power/aberrant_root/anomalous/remove()
+ UnregisterSignal(power_holder, COMSIG_LIVING_UNARMED_ATTACK)
+
+/// Listener for hitting anomalies.
+/datum/power/aberrant_root/anomalous/proc/on_unarmed_attack(mob/living/source, atom/target, proximity, modifiers)
+ SIGNAL_HANDLER
+
+ if(!proximity || !istype(target, /obj/effect/anomaly))
+ return NONE
+
+ if(HAS_TRAIT(target, TRAIT_ILLUSORY_EFFECT))
+ to_chat(source, span_notice("You pass your hand through [target], but nothing seems to happen. Is it really even there?"))
+ return COMPONENT_CANCEL_ATTACK_CHAIN
+
+ var/obj/effect/anomaly/anomaly_target = target
+ to_chat(source, span_notice("You reach out and touch [anomaly_target], disrupting the anomaly!"))
+ anomaly_target.anomalyNeutralize()
+ return COMPONENT_CANCEL_ATTACK_CHAIN
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_beastial.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_beastial.dm
new file mode 100644
index 00000000000000..fac207a47e42c0
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_beastial.dm
@@ -0,0 +1,66 @@
+/datum/power/aberrant_root/beastial
+ name = "Beastial Body"
+ desc = "You have the traits of an animal; and with it, the apetite of one. In addition to your species normal preferences, you now like the following food based on your choice of Herbivore or Carnivore (including making it non-toxic)\
+ \nHerbivore: Vegetables, Fruit & Nuts. \
+ \nCarnivore: Raw, Gore, Meat, Bugs & Seafood."
+ value = 2
+ /// Saved preference value used for security records snapshotting.
+ var/chosen_diet = "None"
+
+/datum/power/aberrant_root/beastial/get_security_record_text()
+ switch(chosen_diet)
+ if("Herbivore", "Carnivore")
+ return "Subject has a [LOWER_TEXT(chosen_diet)] dietary adaptation."
+ return ""
+
+/datum/power/aberrant_root/beastial/add(client/client_source)
+ var/obj/item/organ/tongue/tongue = power_holder.get_organ_slot(ORGAN_SLOT_TONGUE)
+ if(!tongue)
+ return
+ var/diet_choice = client_source?.prefs?.read_preference(/datum/preference/choiced/beastial_diet)
+ if(isnull(diet_choice))
+ diet_choice = "None"
+ chosen_diet = diet_choice
+
+ switch(diet_choice)
+ if("Herbivore")
+ tongue.liked_foodtypes |= VEGETABLES | FRUIT | NUTS
+ tongue.disliked_foodtypes &= ~(VEGETABLES | FRUIT | NUTS)
+ tongue.toxic_foodtypes &= ~(VEGETABLES | FRUIT | NUTS)
+ if("Carnivore")
+ tongue.liked_foodtypes |= RAW | GORE | MEAT | BUGS | SEAFOOD
+ tongue.disliked_foodtypes &= ~(RAW | GORE | MEAT | BUGS | SEAFOOD)
+ tongue.toxic_foodtypes &= ~(RAW | GORE | MEAT | BUGS | SEAFOOD)
+
+/datum/power/aberrant_root/beastial/remove()
+ var/obj/item/organ/tongue/tongue = power_holder.get_organ_slot(ORGAN_SLOT_TONGUE)
+ if(!tongue)
+ return
+ tongue.liked_foodtypes = initial(tongue.liked_foodtypes)
+ tongue.disliked_foodtypes = initial(tongue.disliked_foodtypes)
+ tongue.toxic_foodtypes = initial(tongue.toxic_foodtypes)
+
+// Preference choice for Beastkindred diet selection.
+/datum/preference/choiced/beastial_diet
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "beastial_diet"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/beastial_diet/create_default_value()
+ return "None"
+
+/datum/preference/choiced/beastial_diet/init_possible_values()
+ return list("None", "Herbivore", "Carnivore")
+
+/datum/preference/choiced/beastial_diet/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return TRUE
+
+/datum/preference/choiced/beastial_diet/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/datum/power_constant_data/beastial
+ associated_typepath = /datum/power/aberrant_root/beastial
+ customization_options = list(/datum/preference/choiced/beastial_diet)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_monstrous.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_monstrous.dm
new file mode 100644
index 00000000000000..7b581e6d09e28f
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_monstrous.dm
@@ -0,0 +1,63 @@
+// MORE BLOOD
+/datum/power/aberrant_root/monstrous
+ name = "Monstrous Body"
+ desc = "If it bleeds, you can kill it. Just with you, blood doesn't really matter. You have 125% the normal blood capacity of your species, and regenerate blood that much faster as well.\
+ \n The thresholds for being low on blood are unchanged, meaning you are extra resistent to bloodloss."
+ security_record_text = "Subject's body contains and regenerates more blood."
+ value = 3
+
+ /// Target blood level while this power is active.
+ var/target_blood_volume
+ /// Tracks if we applied our regen multiplier so we can undo safely.
+ var/regen_multiplier_applied
+ /// How much extra blood capacity we have.
+ var/extra_blood_mult = 1.25
+ /// How much faster our blood regenerates.
+ var/extra_blood_regen_mult = 1.25
+
+/datum/power/aberrant_root/monstrous/add()
+ var/mob/living/carbon/human/human_holder = power_holder
+ if(!istype(human_holder) || HAS_TRAIT(human_holder, TRAIT_NOBLOOD))
+ return
+
+ target_blood_volume = BLOOD_VOLUME_NORMAL * extra_blood_mult
+ human_holder.blood_volume = min(target_blood_volume, BLOOD_VOLUME_MAXIMUM)
+
+ human_holder.physiology.blood_regen_mod *= extra_blood_regen_mult
+ regen_multiplier_applied = TRUE
+
+ RegisterSignal(human_holder, COMSIG_HUMAN_ON_HANDLE_BLOOD, PROC_REF(handle_extra_blood_regen))
+
+
+/datum/power/aberrant_root/monstrous/remove()
+ var/mob/living/carbon/human/human_holder = power_holder
+ if(!istype(human_holder))
+ return
+
+ UnregisterSignal(human_holder, COMSIG_HUMAN_ON_HANDLE_BLOOD)
+
+ if(regen_multiplier_applied)
+ human_holder.physiology.blood_regen_mod /= extra_blood_regen_mult
+ regen_multiplier_applied = FALSE
+
+ if(human_holder.blood_volume > BLOOD_VOLUME_NORMAL)
+ human_holder.blood_volume = BLOOD_VOLUME_NORMAL
+
+ target_blood_volume = 0
+
+/// So its hardcoded that blood caps out at BLOOD_VOLUME_NORMAL so we have to handle blood regen in our own way here.
+/datum/power/aberrant_root/monstrous/proc/handle_extra_blood_regen(datum/source, seconds_per_tick, times_fired)
+ SIGNAL_HANDLER
+
+ if(!target_blood_volume)
+ return
+
+ var/mob/living/carbon/human/human_holder = power_holder
+ if(!istype(human_holder) || HAS_TRAIT(human_holder, TRAIT_NOBLOOD))
+ return
+
+ if(human_holder.blood_volume < BLOOD_VOLUME_NORMAL || human_holder.blood_volume >= target_blood_volume)
+ return
+
+ var/blood_regen_amount = BLOOD_REGEN_FACTOR * human_holder.physiology.blood_regen_mod * seconds_per_tick
+ human_holder.blood_volume = min(human_holder.blood_volume + blood_regen_amount, target_blood_volume)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm
new file mode 100644
index 00000000000000..2a6aa0aa8dc5a1
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm
@@ -0,0 +1,91 @@
+// Its like a weaker version of the changeling armblade.
+/datum/power/aberrant/armblade
+ name = "Armblade"
+ desc = "Allows you to transform your arm into a deadly blade. The weapon itself has high damage, pierces armor and can destroy tables that block your way.\
+ \n Requires an empty hand to use."
+ security_record_text = "Subject can manifest a sharp-edged blade from their arm."
+ security_threat = POWER_THREAT_MAJOR
+ value = 4
+ required_powers = list(/datum/power/aberrant_root/monstrous)
+ action_path = /datum/action/cooldown/power/aberrant/armblade
+
+/datum/action/cooldown/power/aberrant/armblade
+ name = "Armblade"
+ desc = "Reform your arm into a deadly blade. Using the power again retracts it."
+ button_icon = 'icons/mob/actions/actions_changeling.dmi'
+ button_icon_state = "armblade"
+ active = FALSE
+
+ cooldown_time = 50
+
+/datum/action/cooldown/power/aberrant/armblade/Grant(mob/granted_to)
+ . = ..()
+ RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+/datum/action/cooldown/power/aberrant/armblade/Remove(mob/removed_from)
+ . = ..()
+ UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
+
+/datum/action/cooldown/power/aberrant/armblade/use_action(mob/living/user, atom/target)
+ if(active)
+ for(var/obj/item/melee/arm_blade/aberrant/blade in user.held_items)
+ user.temporarilyRemoveItemFromInventory(blade, TRUE)
+ playsound(user, 'sound/effects/blob/blobattack.ogg', 30, TRUE)
+ user.visible_message(
+ span_warning("With a sickening crunch, [user] reforms [user.p_their()] blade into an arm!"),
+ span_notice("You assimilate the blade back into your body."))
+ user.update_held_items()
+ active = FALSE
+ return TRUE
+
+ if(user.get_active_held_item())
+ user.balloon_alert(user, "hand occupied!")
+ return FALSE
+
+ var/obj/item/melee/arm_blade/aberrant/new_blade = new(user, FALSE)
+ if(!user.put_in_active_hand(new_blade))
+ qdel(new_blade)
+ return FALSE
+
+ playsound(user, 'sound/effects/blob/blobattack.ogg', 30, TRUE)
+ active = TRUE
+ return TRUE
+
+/// When dispelled, arm pops back in.
+/datum/action/cooldown/power/aberrant/armblade/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+ if(!active)
+ return NONE
+
+ for(var/obj/item/melee/arm_blade/aberrant/blade in owner.held_items)
+ owner.temporarilyRemoveItemFromInventory(blade, TRUE)
+ owner.visible_message(
+ span_warning("With a sickening crunch, [owner] reforms [owner.p_their()] blade into an arm!"),
+ span_boldwarning("Your arm twists back to normal against your own volition!"))
+ owner.update_held_items()
+ break
+
+ active = FALSE
+ StartCooldownSelf(150)
+ return DISPEL_RESULT_DISPELLED
+
+// Weaker version
+/obj/item/melee/arm_blade/aberrant
+ force = 20
+ armour_penetration = 25
+
+// No door forcing.
+/obj/item/melee/arm_blade/aberrant/afterattack(atom/target, mob/user, list/modifiers, list/attack_modifiers)
+ if(istype(target, /obj/structure/table))
+ var/obj/smash = target
+ smash.deconstruct(FALSE)
+
+ else if(istype(target, /obj/machinery/computer))
+ target.attack_alien(user)
+
+// Override the init as to rephrase the spawn message, preventing changeling nouns of 'our'
+/obj/item/melee/arm_blade/aberrant/Initialize(mapload, silent, synthetic)
+ . = ..(mapload, TRUE, synthetic) // suppress parent message
+ if(ismob(loc))
+ loc.visible_message(span_warning("A grotesque blade forms around [loc.name]\'s arm!"), span_notice("Your arm twists and mutates, transforming it into a deadly blade."))
+ return .
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm
new file mode 100644
index 00000000000000..ade3ce9f683afd
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm
@@ -0,0 +1,176 @@
+// he be glowin. can be toggled on or off.
+/datum/power/aberrant/bioluminescence
+ name = "Bioluminescence"
+ desc = "You can glow! You passively emit the chosen light color; which can be toggled on or off at will. Very slightly increases passive hunger when enabling or disabling the light."
+ value = 1
+ security_record_text = "Subject has been observed to glow through bioluminescence."
+
+ required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous)
+ required_allow_any = TRUE
+ action_path = /datum/action/cooldown/power/aberrant/bioluminescence
+
+/datum/action/cooldown/power/aberrant/bioluminescence
+ name = "Bioluminescence"
+ desc = "Toggle on or off your natural light!"
+ button_icon = 'icons/obj/lighting.dmi'
+ button_icon_state = "lantern-blue-on"
+
+ cooldown_time = 5
+ // start with da pretty lights on
+ active = TRUE
+
+ var/obj/effect/dummy/lighting_obj/moblight/biolum_light
+ /// Range of the light
+ var/biolum_range = 3
+ /// Strength of the light
+ var/biolum_power = 1
+ /// Color of the light
+ var/biolum_color = "#66c5dd"
+ /// Extra range of the light caused by being shaked
+ var/biolum_bonus_range = 0
+ /// Choiced option for the size of the light (from prefs)
+ var/biolum_size_choice
+
+/datum/action/cooldown/power/aberrant/bioluminescence/Grant(mob/granted_to)
+ . = ..()
+ RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+ RegisterSignal(granted_to, COMSIG_CARBON_HELP_ACT, PROC_REF(on_help_act))
+ init_biolum_settings_from_prefs()
+ if(active)
+ enable_bioluminescence()
+
+/datum/action/cooldown/power/aberrant/bioluminescence/Remove(mob/removed_from)
+ . = ..()
+ UnregisterSignal(removed_from, list(COMSIG_ATOM_DISPEL, COMSIG_CARBON_HELP_ACT))
+ disable_bioluminescence()
+
+/datum/action/cooldown/power/aberrant/bioluminescence/use_action(mob/living/user, atom/target)
+ active = !active
+ if(active)
+ enable_bioluminescence()
+ else
+ disable_bioluminescence()
+ user.adjust_nutrition(-2)
+ owner.balloon_alert(owner, active ? "bioluminescence on" : "bioluminescence off")
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+ return TRUE
+
+/// Applies the appropriate size from the choiced component.
+/datum/action/cooldown/power/aberrant/bioluminescence/proc/apply_biolum_size_settings()
+ if(isnull(biolum_size_choice))
+ biolum_size_choice = "Medium"
+ var/size_range = GLOB.bioluminescence_sizes[biolum_size_choice]
+ if(isnum(size_range))
+ biolum_range = size_range
+ else
+ biolum_range = GLOB.bioluminescence_sizes["Medium"]
+
+/// Gets the size and color and applies it to the mob.
+/datum/action/cooldown/power/aberrant/bioluminescence/proc/init_biolum_settings_from_prefs()
+ if(!owner)
+ return
+ var/color_choice = owner?.client?.prefs?.read_preference(/datum/preference/color/bioluminescence_color)
+ var/size_choice = owner?.client?.prefs?.read_preference(/datum/preference/choiced/bioluminescence_size)
+ if(isnull(color_choice))
+ color_choice = "66c5dd"
+ if(isnull(size_choice))
+ size_choice = "Medium"
+ biolum_size_choice = size_choice
+ biolum_color = color_choice
+ if(!isnull(biolum_color) && !findtext(biolum_color, "#", 1, 2))
+ biolum_color = "#[biolum_color]"
+ apply_biolum_size_settings()
+
+/// We turn the light on.
+/datum/action/cooldown/power/aberrant/bioluminescence/proc/enable_bioluminescence()
+ if(!owner || !isliving(owner))
+ return
+ var/mob/living/glowstick_person = owner
+ QDEL_NULL(biolum_light)
+ biolum_light = glowstick_person.mob_light(
+ range = biolum_range + biolum_bonus_range,
+ power = biolum_power,
+ color = biolum_color
+ )
+
+/// We turn the light off.
+/datum/action/cooldown/power/aberrant/bioluminescence/proc/disable_bioluminescence()
+ QDEL_NULL(biolum_light)
+
+/// On dispel, turn the lights off.
+/datum/action/cooldown/power/aberrant/bioluminescence/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+ if(!active)
+ return DISPEL_RESULT_DISPELLED
+ active = FALSE
+ disable_bioluminescence()
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+ return DISPEL_RESULT_DISPELLED
+
+/// You can shake em like glowsticks to make em glow MORE.
+/datum/action/cooldown/power/aberrant/bioluminescence/proc/on_help_act(mob/living/carbon/source, mob/living/carbon/helper)
+ SIGNAL_HANDLER
+ if(!active || !owner || source != owner)
+ return
+ if(biolum_bonus_range >= 2)
+ return
+ biolum_bonus_range++
+ enable_bioluminescence()
+ addtimer(CALLBACK(src, PROC_REF(decay_biolum_bonus)), 60 SECONDS)
+
+/// Undoes the bonus light from being shaked.
+/datum/action/cooldown/power/aberrant/bioluminescence/proc/decay_biolum_bonus()
+ if(biolum_bonus_range <= 0)
+ return
+ biolum_bonus_range--
+ if(active)
+ enable_bioluminescence()
+
+// Preference choice for Bioluminescence color selection.
+/datum/preference/color/bioluminescence_color
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "bioluminescence_color"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/color/bioluminescence_color/create_default_value()
+ return "66c5dd"
+
+/datum/preference/color/bioluminescence_color/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return TRUE
+
+/datum/preference/color/bioluminescence_color/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+// Preference choice for Bioluminescence size selection.
+/datum/preference/choiced/bioluminescence_size
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "bioluminescence_size"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/bioluminescence_size/create_default_value()
+ return "Medium"
+
+/datum/preference/choiced/bioluminescence_size/init_possible_values()
+ var/list/values = list()
+ for(var/choice in GLOB.bioluminescence_sizes)
+ values += choice
+ return values
+
+/datum/preference/choiced/bioluminescence_size/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return TRUE
+
+/datum/preference/choiced/bioluminescence_size/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/datum/power_constant_data/bioluminescence
+ associated_typepath = /datum/power/aberrant/bioluminescence
+ customization_options = list(
+ /datum/preference/color/bioluminescence_color,
+ /datum/preference/choiced/bioluminescence_size
+ )
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm
new file mode 100644
index 00000000000000..7876cbba008d84
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm
@@ -0,0 +1,187 @@
+// Lets you sniff someone out based on their blood. Sorta similar toa pinpointer.
+/datum/power/aberrant/bloodhound
+ name = "Bloodhound"
+ desc = "A whiff of someone's blood, and you're right on their tail. Select a source of blood and it will be your currently active scent. You can only have one active source of scent, and it only lasts for a few minutes.\
+ \n Whilst you have someone's blood, you have an indicator of your quarry's direction. Does not work on scrying immune creatures."
+ security_record_text = "Subject can track down a creature's direction using blood samples."
+ value = 10
+
+ required_powers = list(/datum/power/aberrant_root/beastial)
+ action_path = /datum/action/cooldown/power/aberrant/bloodhound
+
+/datum/action/cooldown/power/aberrant/bloodhound
+ name = "Bloodhound"
+ desc = "Track someone using a sample their blood. By targeting a source of blood, you acquire your quarry, allowing you to track their direction for a limited time."
+ button_icon = 'icons/mob/actions/actions_ecult.dmi'
+ button_icon_state = "cleave"
+ click_to_activate = TRUE
+ target_range = 1
+
+ /// How much hunger does tracking someone take?
+ var/hunger_cost = 20
+ /// How long you can keep a mob's scent.
+ var/scent_duration = 2 MINUTES
+
+/datum/action/cooldown/power/aberrant/bloodhound/use_action(mob/living/user, atom/target)
+ var/list/dna_samples = get_blood_dna_list_from_target(target)
+ if(!length(dna_samples))
+ user.balloon_alert(user, "You need blood to focus your scent.")
+ return FALSE
+
+ // If your list of dna samples has multiples then my man you gotta clean your samples. Chooses a random one.
+ var/selected_dna = pick(dna_samples)
+ var/mob/living/chosen_target = find_target_from_dna(selected_dna)
+ if(!chosen_target)
+ user.balloon_alert(user, "No scent to follow.")
+ return FALSE
+
+ if(!can_affect_bloodhound(chosen_target))
+ user.balloon_alert(user, "No scent to follow.")
+ return FALSE
+
+ var/datum/status_effect/power/bloodhound_scent/applied = user.apply_status_effect(/datum/status_effect/power/bloodhound_scent, scent_duration, chosen_target)
+ if(!applied)
+ return FALSE
+
+ user.emote("sniff")
+ to_chat(user, span_notice("You catch someone's scent!"))
+ user.adjust_nutrition(hunger_cost)
+ return TRUE
+
+/// Checks if the target can be affected by bloodhound tracking. Basically magic resistance + scrying immunity.
+/datum/action/cooldown/power/aberrant/bloodhound/proc/can_affect_bloodhound(mob/living/target)
+ if(target.can_block_resonance())
+ return FALSE
+ if(target.can_block_magic(MAGIC_RESISTANCE))
+ return FALSE
+ if(HAS_TRAIT(target, TRAIT_ANTIRESONANCE_SCRYING))
+ return FALSE
+ return TRUE
+
+/// Gets DNA from blood
+/datum/action/cooldown/power/aberrant/bloodhound/proc/get_blood_dna_list_from_target(atom/target)
+ if(isnull(target))
+ return null
+
+ var/list/dna_list = list()
+
+ if(ismob(target))
+ return dna_list
+
+ // Gets dna from a blood decal.
+ if(istype(target, /obj/effect/decal/cleanable/blood))
+ var/obj/effect/decal/cleanable/blood/blood_decal = target
+ if(blood_decal.dried || blood_decal.bloodiness <= 0) // we don't count dry blood. The trail has gone cold.
+ return dna_list
+ var/list/blood = GET_ATOM_BLOOD_DNA(target)
+ for(var/dna in blood)
+ dna_list += dna
+ return dna_list
+
+ // Gets dna from blood from reagent containers. Note: There's a bug with scraping blood not saving DNA; so if it acts weirds its likely that (as of 20/02/26)
+ if(istype(target, /obj/item/reagent_containers))
+ for(var/datum/reagent/present_reagent as anything in target.reagents?.reagent_list)
+ if(!istype(present_reagent, /datum/reagent/blood))
+ continue
+ var/blood_dna = present_reagent.data?["blood_DNA"]
+ if(isnull(blood_dna))
+ continue
+ if(islist(blood_dna))
+ for(var/dna in blood_dna)
+ dna_list += dna
+ else
+ dna_list += blood_dna
+
+ // Any non-mob atom with forensics blood on it (e.g. clothes, tools)
+ var/list/blood = GET_ATOM_BLOOD_DNA(target)
+ if(length(blood))
+ for(var/dna in blood)
+ dna_list += dna
+
+ return dna_list
+
+/// Checks the blood for a dna match.
+/datum/action/cooldown/power/aberrant/bloodhound/proc/find_target_from_dna(selected_dna)
+ if(!selected_dna)
+ return null
+
+ for(var/mob/living/target in GLOB.mob_list)
+ if(isobserver(target))
+ continue
+ var/list/blood_dna = target.get_blood_dna_list()
+ if(blood_dna && blood_dna[selected_dna])
+ return target
+ return null
+
+// Status effect meant for bloodhound
+/datum/status_effect/power/bloodhound_scent
+ id = "bloodhound_scent"
+ status_type = STATUS_EFFECT_REPLACE
+ show_duration = TRUE
+ tick_interval = STATUS_EFFECT_AUTO_TICK
+ alert_type = /atom/movable/screen/alert/status_effect/bloodhound_scent
+
+ /// Weakref to the target mob
+ var/datum/weakref/target_ref
+
+/datum/status_effect/power/bloodhound_scent/on_creation(mob/living/new_owner, passed_duration, mob/living/target)
+ if(isnum(passed_duration))
+ duration = passed_duration
+ else // we should always pass a duration so something went wrong. We fall back on this.
+ duration = 1 MINUTES
+
+ if(ismob(target))
+ target_ref = WEAKREF(target)
+ . = ..()
+ update_direction_indicator()
+
+// If we have no target we nuke the power.
+/datum/status_effect/power/bloodhound_scent/on_apply()
+ var/mob/living/target = target_ref?.resolve()
+ if(!target)
+ return FALSE
+ return TRUE
+
+/datum/status_effect/power/bloodhound_scent/tick(seconds_between_ticks)
+ if(prob(1))
+ owner.emote("sniff")
+ update_direction_indicator()
+
+/// Updates the direction indicator on the status effect (what we use to convey direction)
+/datum/status_effect/power/bloodhound_scent/proc/update_direction_indicator()
+ if(!owner || QDELETED(owner))
+ qdel(src)
+ return
+
+ var/mob/living/target = target_ref?.resolve()
+ if(!target || QDELETED(target))
+ qdel(src)
+ return
+
+ var/turf/here = get_turf(owner)
+ var/turf/there = get_turf(target)
+ if(!here || !there || here.z != there.z)
+ if(linked_alert)
+ linked_alert.icon = 'icons/effects/landmarks_static.dmi'
+ linked_alert.icon_state = "x"
+ linked_alert.dir = SOUTH
+ return
+
+ var/dir_to_target = get_dir(here, there)
+ if(!dir_to_target)
+ return
+
+ var/dir_text = uppertext(dir2text(dir_to_target))
+ var/image/dir_image = GLOB.all_radial_directions[dir_text] // this is literally the best list of direction indicators I could find lmao
+ if(!dir_image || !linked_alert)
+ return
+
+ linked_alert.icon = dir_image.icon
+ linked_alert.icon_state = dir_image.icon_state
+ linked_alert.dir = dir_image.dir
+
+/atom/movable/screen/alert/status_effect/bloodhound_scent
+ name = "Bloodhound"
+ desc = "Your senses point the way to your quarry."
+ icon = 'icons/testing/turf_analysis.dmi'
+ icon_state = "red_arrow"
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm
new file mode 100644
index 00000000000000..97ec46adbb3658
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm
@@ -0,0 +1,326 @@
+// Put things into a bundle of webs. Mostly on-demand storage and sorting; or dealing with people you don't want to escape.
+/datum/power/aberrant/cocoon
+ name = "Cocoon"
+ desc = "Allows you to cocoon objects and people after a delay. You can destroy cocoons by interacting with them.\
+ \n Targeting a space without a creature bundles all items on that space up in a container; this has the size and storage capacity of about two backpacks, and can only be opened by destroying it.\
+ \n Targeting a prone creature that you have aggressively grabbed bundles them up. The creature is buckled inside the cocoon and can't interact with the world or escape without struggling. \
+ Creature cocoons can be dragged around with less slow down commpared to normal.\
+ \n Costs hunger to use, and cannot be used while starving."
+ security_record_text = "Subject can produce enough silk to fully cocoon creatures and objects in webs."
+ security_threat = POWER_THREAT_MAJOR
+ value = 3
+
+ required_powers = list(/datum/power/aberrant/web_crafter)
+ action_path = /datum/action/cooldown/power/aberrant/cocoon
+
+/datum/action/cooldown/power/aberrant/cocoon
+ name = "Cocoon"
+ desc = "Wraps up a person or object for convenient storage. Object cocoons can be carried around and allow you to carry greater amount of items with relative ease. People cocoons can be used to keep people from escaping."
+ button_icon = 'icons/effects/web.dmi'
+ button_icon_state = "cocoon_large1"
+ cooldown_time = 2
+
+ target_range = 1
+ target_self = FALSE // why would you
+ click_to_activate = TRUE
+ use_time = 5 SECONDS
+ // Used to determine the cost
+ var/last_cocoon_was_mob = FALSE
+
+/datum/action/cooldown/power/aberrant/cocoon/InterceptClickOn(mob/living/clicker, params, atom/target)
+ ..()
+ // Always consume the click to avoid normal click interactions.
+ return TRUE
+
+// Block use while starving.
+/datum/action/cooldown/power/aberrant/cocoon/can_use(mob/living/user, atom/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(user.nutrition <= NUTRITION_LEVEL_STARVING)
+ owner.balloon_alert(user, "too hungry!")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/power/aberrant/cocoon/use_action(mob/living/user, atom/target)
+ // Living targets get wrapped.
+ if(isliving(target))
+ if(!can_cocoon_mob(user, target))
+ return FALSE
+ if(cocoon_mob(user, target))
+ last_cocoon_was_mob = TRUE
+ return TRUE
+ return FALSE
+ // Cocoon objects in the space if we don't have other targets.
+ if(cocoon_object(user, target))
+ last_cocoon_was_mob = FALSE
+ return TRUE
+ return FALSE
+
+/datum/action/cooldown/power/aberrant/cocoon/on_action_success(mob/living/user, atom/target)
+ if(!user)
+ return
+ user.adjust_nutrition(last_cocoon_was_mob ? -40 : -15)
+ return
+
+// Both cast time and visual effects are resolved in this.
+/datum/action/cooldown/power/aberrant/cocoon/do_use_time(mob/living/user, atom/target)
+ if(use_time <= 0)// Woooow I worked hard on this and you just var-edit it away BAKA.
+ return TRUE
+ if(!target)
+ return FALSE
+ if(isliving(target) && !can_cocoon_mob(user, target)) // I'd put this in can_use but can_cooon_mob also checks can_use so it will create a recursive loop.
+ return FALSE
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ return do_after(user, use_time, target = target, timed_action_flags = use_time_flags)
+ playsound(user, 'sound/items/handling/surgery/organ1.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ // Applies a visual effect similar to chiseling away at stone
+ var/obj/effect/temp_visual/cocoon_progress/progress_visual = new /obj/effect/temp_visual/cocoon_progress(target_turf)
+ progress_visual.apply_cocoon_appearance(target)
+ progress_visual.pixel_x = target.pixel_x
+ progress_visual.pixel_y = target.pixel_y
+ // If we having a living target, we assing it here.
+ var/mob/living/target_mob
+ if(isliving(target))
+ target_mob = target
+ // Align the sprite with the animation
+ target_mob.set_lying_angle(LYING_ANGLE_EAST)
+ progress_visual.setDir(EAST)
+ else
+ progress_visual.setDir(target.dir)
+ progress_visual.set_completion(0)
+
+ // spin!
+ if(target_mob)
+ target_mob.spin(spintime = use_time, speed = 2)
+
+ // Do_after loop and a progress bar for the user.
+ var/datum/progressbar/total_progress_bar = new(user, use_time, target)
+ var/use_time_period = max(1, round(use_time / ICON_SIZE_Y))
+ var/remaining_time = use_time
+ var/interrupted = FALSE
+ if(target_mob)
+ target_mob.remove_filter("cocoon_hide") // removes existing filter if its there.
+ while(remaining_time > 0 && !interrupted)
+ if(target_mob && !can_cocoon_mob(user, target))
+ interrupted = TRUE
+ break
+ // We update the progress bar as well as the visual effects for the cocoon.
+ if(do_after(user, use_time_period, target = target, timed_action_flags = use_time_flags, progress = FALSE))
+ remaining_time -= use_time_period
+ total_progress_bar.update(use_time - remaining_time)
+ var/progress = (use_time - remaining_time) / use_time
+ progress_visual.set_completion(progress) // this line's responsible for the cocoon effect
+ // this filter keeps pace with the cocoon and hides the mob so it doesn't have 'bits' poking out.
+ if(target_mob)
+ var/mask_offset = min(ICON_SIZE_Y, round(progress * ICON_SIZE_Y))
+ target_mob.add_filter("cocoon_hide", 1, alpha_mask_filter(icon = icon('icons/effects/alphacolors.dmi', "white"), y = mask_offset))
+ else
+ interrupted = TRUE
+ total_progress_bar.end_progress()
+
+ if(!QDELETED(progress_visual))
+ qdel(progress_visual)
+ if(target_mob)
+ target_mob.remove_filter("cocoon_hide")
+ return !interrupted
+
+/// Physically stuffs the mob in the cocoon.
+/datum/action/cooldown/power/aberrant/cocoon/proc/cocoon_mob(mob/living/user, mob/living/target)
+ if(!target || QDELETED(target))
+ return FALSE
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ return FALSE
+
+ var/obj/structure/closet/body_bag/cocoon/new_cocoon = new /obj/structure/closet/body_bag/cocoon(target_turf)
+ if(!new_cocoon)
+ return FALSE
+ if(!new_cocoon.insert(target))
+ qdel(new_cocoon)
+ return FALSE
+ return TRUE
+
+/// Checks if a mob is cocoonable.
+/datum/action/cooldown/power/aberrant/cocoon/proc/can_cocoon_mob(mob/living/user, mob/living/target)
+ if(!user || !target)
+ user.balloon_alert(user, "No target!")
+ return FALSE
+ if(!can_use(user, target))
+ return FALSE
+ if(QDELETED(user) || QDELETED(target))
+ user.balloon_alert(user, "No target!")
+ return FALSE
+ if(user.pulling != target || user.grab_state < GRAB_AGGRESSIVE)
+ user.balloon_alert(user, "You must aggressively grab the target!")
+ return FALSE
+ if(target.body_position != LYING_DOWN || !HAS_TRAIT(target, TRAIT_FLOORED))
+ user.balloon_alert(user, "Target must be prone!")
+ return FALSE
+ return TRUE
+
+/// We get the space we're on and bundle up all the items on the space; as much as possible.
+/datum/action/cooldown/power/aberrant/cocoon/proc/cocoon_object(mob/living/user, atom/target)
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ return FALSE
+
+ var/obj/item/storage/cocoon_item/new_cocoon = new /obj/item/storage/cocoon_item(target_turf)
+ if(!new_cocoon?.atom_storage)
+ qdel(new_cocoon)
+ return FALSE
+
+ // Stuffs everything inside of the container
+ var/inserted_any = FALSE
+ var/previous_lock_state = new_cocoon.atom_storage.locked
+ new_cocoon.atom_storage.set_locked(STORAGE_NOT_LOCKED)
+ for(var/obj/item/thing in target_turf)
+ if(thing == new_cocoon || thing.anchored)
+ continue
+ if(new_cocoon.atom_storage.attempt_insert(thing, null, messages = FALSE))
+ inserted_any = TRUE
+ new_cocoon.atom_storage.set_locked(previous_lock_state)
+
+ // can't make empty ones
+ if(!inserted_any)
+ user.balloon_alert(user, "Nothing to wrap!")
+ qdel(new_cocoon)
+ return FALSE
+ return TRUE
+
+// Cocoon for items
+/obj/item/storage/cocoon_item
+ name = "cocoon"
+ desc = "A tight bundle of webbing packed with stored goods. You will have to tear it open to get anything out."
+ icon = 'icons/effects/web.dmi'
+ icon_state = "cocoon1"
+ w_class = WEIGHT_CLASS_BULKY
+ var/unwrap_delay = 4 SECONDS
+
+/obj/item/storage/cocoon_item/Initialize(mapload)
+ . = ..()
+ if(atom_storage)
+ atom_storage.max_slots = 30
+ atom_storage.max_total_storage = 35
+ atom_storage.attack_hand_interact = FALSE
+ atom_storage.click_alt_open = FALSE
+ atom_storage.display_contents = FALSE
+ atom_storage.insert_on_attack = FALSE
+ atom_storage.set_locked(STORAGE_FULLY_LOCKED)
+
+/obj/item/storage/cocoon_item/attack_self(mob/user, modifiers)
+ return attempt_unwrap(user)
+
+/// Attempts to tear open and destroy the cocoon.
+/obj/item/storage/cocoon_item/proc/attempt_unwrap(mob/living/user)
+ if(!user)
+ return FALSE
+ to_chat(user, span_notice("You start tearing open [src]..."))
+ if(!do_after(user, unwrap_delay, target = src))
+ return FALSE
+ if(QDELETED(src))
+ return FALSE
+ var/turf/drop_turf = get_turf(src)
+ if(atom_storage && drop_turf)
+ atom_storage.remove_all(drop_turf)
+ visible_message(span_notice("[src] is torn open, spilling its contents!"))
+ qdel(src)
+ return TRUE
+
+// Cocoon for people
+/obj/structure/closet/body_bag/cocoon
+ name = "cocoon"
+ desc = "A person-sized cocoon; rows upon rows of silk keeping something quite secure."
+ icon = 'icons/effects/web.dmi'
+ icon_state = "cocoon_large1"
+ max_integrity = 40
+ material_drop = null
+ material_drop_amount = 0
+ obj_flags = CAN_BE_HIT
+ breakout_time = 2 MINUTES
+ mob_storage_capacity = 1
+ drag_slowdown = 0.5
+
+ /// How long it takes to tear open the cocoon
+ var/open_time = 5 SECONDS
+
+/obj/structure/closet/body_bag/cocoon/can_open(mob/living/user, force = FALSE)
+ return FALSE
+
+/obj/structure/closet/body_bag/cocoon/can_close(mob/living/user)
+ return FALSE
+
+/obj/structure/closet/body_bag/cocoon/toggle(mob/living/user)
+ return FALSE
+
+/obj/structure/closet/body_bag/cocoon/attack_hand(mob/living/user, list/modifiers)
+ tear_open(user)
+
+/obj/structure/closet/body_bag/cocoon/attack_hand_secondary(mob/user, list/modifiers)
+ return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
+
+/obj/structure/closet/body_bag/cocoon/attempt_fold(mob/living/carbon/human/the_folder)
+ return FALSE
+
+/obj/structure/closet/body_bag/cocoon/container_resist_act(mob/living/user, loc_required = TRUE)
+ var/breakout_time = 2 MINUTES
+ user.changeNext_move(CLICK_CD_BREAKOUT)
+ user.last_special = world.time + CLICK_CD_BREAKOUT
+ to_chat(user, span_notice("You struggle against the webs... (This will take about [DisplayTimeText(breakout_time)].)"))
+ visible_message(span_notice("You see something struggling and writhing in \the [src]!"))
+ if(do_after(user, breakout_time, target = src))
+ if(user.stat != CONSCIOUS || user.loc != src)
+ return
+ qdel(src)
+
+/// Starts tearing the mob cocoon open
+/obj/structure/closet/body_bag/cocoon/proc/tear_open(mob/living/user)
+ if(open_time > 0)
+ to_chat(user, span_notice("You start tearing open [src]..."))
+ if(!do_after(user, open_time, target = src))
+ return
+ if(QDELETED(src))
+ return
+ var/turf/drop_turf = get_turf(src)
+ if(drop_turf)
+ for(var/atom/movable/thing as anything in contents)
+ thing.forceMove(drop_turf)
+ visible_message(span_notice("[src] is torn open, spilling its contents!"))
+ qdel(src)
+
+// Cocoon progress visual for use_time.
+/obj/effect/temp_visual/cocoon_progress
+ icon = 'icons/effects/web.dmi'
+ icon_state = "cocoon_large1"
+ randomdir = FALSE
+ layer = ABOVE_MOB_LAYER
+ appearance_flags = KEEP_TOGETHER | KEEP_APART
+ duration = 1 MINUTES
+ var/completion = 0
+
+/// Takes the icon and state from the associated cocoon
+/obj/effect/temp_visual/cocoon_progress/proc/apply_cocoon_appearance(atom/target)
+ if(isliving(target))
+ var/obj/structure/closet/body_bag/cocoon/reference = /obj/structure/closet/body_bag/cocoon
+ icon = initial(reference.icon)
+ icon_state = initial(reference.icon_state)
+ else
+ var/obj/item/storage/cocoon_item/reference = /obj/item/storage/cocoon_item
+ icon = initial(reference.icon)
+ icon_state = initial(reference.icon_state)
+
+/// Makes the mob whiter as the wrap goes on.
+/obj/effect/temp_visual/cocoon_progress/proc/set_completion(value)
+ completion = clamp(value, 0, 1)
+ var/static/icon/white = icon('icons/effects/alphacolors.dmi', "white")
+ switch(completion)
+ if(0)
+ alpha = 0
+ remove_filter("partial_uncover")
+ filters = null
+ else
+ alpha = 255
+ var/mask_offset = min(ICON_SIZE_X, round(completion * ICON_SIZE_X))
+ remove_filter("partial_uncover")
+ add_filter("partial_uncover", 1, alpha_mask_filter(icon = white, x = mask_offset, flags = MASK_INVERSE))
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm
new file mode 100644
index 00000000000000..6cc5d011d68e18
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm
@@ -0,0 +1,27 @@
+// Lets you see in the dark. Duh.
+/datum/power/aberrant/darkvision
+ name = "Darkvision"
+ desc = "Your eyes see perfectly in the dark; but your vision gains a blue-ish hue to it."
+ security_record_text = "Subject sees perfectly in the dark."
+ mob_trait = TRAIT_TRUE_NIGHT_VISION
+
+ value = 3
+ required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous)
+ required_allow_any = TRUE
+
+ /// Saves if we apply the cutoffs for darkvision.
+ var/eye_color_cutoffs_applied = FALSE
+
+/datum/power/aberrant/darkvision/add()
+ var/obj/item/organ/eyes/eyes = power_holder.get_organ_slot(ORGAN_SLOT_EYES)
+ if(eyes && isnull(eyes.color_cutoffs)) // we apply a vision tint but only if our current eyes dont apply it
+ eyes.color_cutoffs = list(25, 15, 35)
+ eye_color_cutoffs_applied = TRUE
+ power_holder.update_sight()
+
+/datum/power/aberrant/darkvision/remove()
+ var/obj/item/organ/eyes/eyes = power_holder.get_organ_slot(ORGAN_SLOT_EYES)
+ if(eyes && eye_color_cutoffs_applied)
+ eyes.color_cutoffs = null
+ eye_color_cutoffs_applied = FALSE
+ power_holder.update_sight()
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/healing_factor.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/healing_factor.dm
new file mode 100644
index 00000000000000..31797c06007e23
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/healing_factor.dm
@@ -0,0 +1,30 @@
+/*
+ You passively heal. Wow.
+*/
+/datum/power/aberrant/healing_factor
+ name = "Healing Factor"
+ desc = "Your physical injuries heal without assistance. You heal 0.2 damage per second, randomly split between brute and burn damage while not in critical condition. Wounds such as bleeding still require medical treatment."
+ security_record_text = "Subject passively regenerates any injuries they sustain."
+ value = 4
+ power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES
+
+ required_powers = list(/datum/power/aberrant_root/monstrous)
+
+ /// how much we heal per second
+ var/healing = 0.2
+
+/datum/power/aberrant/healing_factor/process(seconds_per_tick)
+ // Does not work if you're in crit
+ if(power_holder.stat >= SOFT_CRIT)
+ return
+
+ var/heal_amt = healing * seconds_per_tick
+ if(heal_amt <= 0)
+ return
+
+ // Heal the first damaged organic limb we find.
+ var/mob/living/carbon/mob = power_holder
+ for(var/obj/item/bodypart/bodypart in mob.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC))
+ if(bodypart.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC))
+ mob.update_damage_overlays()
+ break
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/miasmic_conversion.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/miasmic_conversion.dm
new file mode 100644
index 00000000000000..7362f5fa3239bb
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/miasmic_conversion.dm
@@ -0,0 +1,46 @@
+/*
+ You passively convert your brute and burn damage into toxins damage at a defined ratio.
+*/
+/datum/power/aberrant/miasmic_conversion
+ name = "Miasmic Conversion"
+ desc = "Your body mends itself disturbingly well, but creates toxic backlash in your system. You passively convert 1 brute or burn damage per second to toxins damage, at a 90% ratio. You also passively heal a tiny amount of toxins damage per second."
+ security_record_text = "Subject extremely rapidly regenerates, but experiences toxic backlash when they do."
+ value = 4
+ power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES
+
+ required_powers = list(/datum/power/aberrant_root/monstrous)
+
+ /// how much we passively heal tox
+ var/passive_tox_healing = 0.05
+ /// how much we heal/convert per second
+ var/healing = 1
+ /// the ratio at which we convert.
+ var/conversion_rate = 0.90
+
+/datum/power/aberrant/miasmic_conversion/process(seconds_per_tick)
+ var/heal_amt = healing * seconds_per_tick
+ if(heal_amt <= 0)
+ return
+
+ var/passive_heal_sum = passive_tox_healing * seconds_per_tick
+ // Inverts for tox-healing spcies
+ passive_heal_sum = HAS_TRAIT(power_holder, TRAIT_TOXINLOVER) ? -passive_heal_sum : passive_heal_sum
+ // Always heal a small amount of toxins.
+ power_holder.adjustToxLoss(-passive_heal_sum)
+
+ // Gets all limbs and picks a random one.
+ var/mob/living/carbon/mob = power_holder
+ var/list/parts = mob.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC)
+ if(!parts.len)
+ return
+ var/obj/item/bodypart/bodypart = pick(parts)
+
+ // Applies healing, then reapplies as damage.
+ var/damage_before = bodypart.get_damage()
+ if(bodypart.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC))
+ mob.update_damage_overlays()
+ var/healed = damage_before - bodypart.get_damage()
+ if(healed > 0) // Reapply the damage as tox.
+ // Inverts for tox-healing spcies
+ healed = HAS_TRAIT(power_holder, TRAIT_TOXINLOVER) ? -healed : healed
+ power_holder.adjustToxLoss(healed * conversion_rate)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm
new file mode 100644
index 00000000000000..414c3ebfddf7e3
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm
@@ -0,0 +1,41 @@
+/*
+ Sunbathe under the Supermatter for healing. Doctors hate this trick! Heals every damage type except oxyloss.
+*/
+/datum/power/aberrant/radiosyntehsis
+ name = "Radiosynthesis"
+ desc = "Rather than the molecular degredation you experience from radioactivity, your body instead uses it as an energy source to rapidly heal your body. Radioactivity heals you instead of damaging you. Because this healing is anomalous, it heals synthetic and biological bodyparts."
+ security_record_text = "Subject's body regenerates instead of degenerate from exposure to radiation."
+ value = 3
+ mob_trait = TRAIT_HALT_RADIATION_EFFECTS // we don't give radimmune cause we want to ENCOURAGE people to get irradiated.
+ power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES
+
+ required_powers = list(/datum/power/aberrant_root/anomalous)
+
+ /// how much we heal per second
+ var/healing = 1
+
+/datum/power/aberrant/radiosyntehsis/process(seconds_per_tick)
+ // Only heal if we're irradiated
+ if(!HAS_TRAIT(power_holder, TRAIT_IRRADIATED))
+ return
+
+ var/heal_amt = healing * seconds_per_tick
+ if(heal_amt <= 0)
+ return
+
+ // Get body parts, heal damage on them if there's any.
+ var/mob/living/carbon/mob = power_holder
+ var/list/parts = mob.get_damaged_bodyparts(1, 1)
+ if(parts.len)
+ for(var/obj/item/bodypart/bodypart in parts)
+ if(bodypart.heal_damage(heal_amt/parts.len, heal_amt/parts.len, required_bodytype = NONE)) // Because anomalous is weird and funky we allow it to heal synthetic parts. This is deliberate. Be a radation powered robot. Beep boop.
+ mob.update_damage_overlays()
+ return
+
+ // Heal toxins if we didn't heal any other damage, but never remove the last point (keeps irradiation).
+ var/tox_loss = power_holder.getToxLoss()
+ if(tox_loss > 1 && heal_amt < tox_loss) // We don't want to heal all of a person's radiation, just as to preserve their radioactiv
+ var/tox_heal = min(heal_amt, tox_loss - 1)
+ // Invert for toxins-healing sepcies
+ tox_heal = HAS_TRAIT(power_holder, TRAIT_TOXINLOVER) ? -tox_heal : tox_heal
+ power_holder.adjustToxLoss(-tox_heal)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm
new file mode 100644
index 00000000000000..707a3f8f2662da
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm
@@ -0,0 +1,20 @@
+/*
+ You're immune to resonant antics! But also you're permanently silenced.
+*/
+/datum/power/aberrant/counter_resonance
+ name = "Counter-Resonance Anomaly"
+ desc = "You have a counteractive effect on resonance-based phenomena. You are immune to resonance-based effects (but not the highly advanced magics wielded by some antagonistic forces), and you cannot use any resonance-based powers.\
+ \n (Silencing only affects active powers; passive powers, such as Radiosyntehsis, are unaffected.)"
+ security_record_text = "Subject is immune to resonance-based phenomena and is unable to wield them."
+ security_threat = POWER_THREAT_MAJOR
+ value = 9
+
+ required_powers = list(/datum/power/aberrant_root/anomalous)
+
+/datum/power/aberrant/counter_resonance/add()
+ ADD_TRAIT(power_holder, TRAIT_ANTIRESONANCE, src)
+ ADD_TRAIT(power_holder, TRAIT_RESONANCE_SILENCED, src)
+
+/datum/power/aberrant/counter_resonance/remove()
+ REMOVE_TRAIT(power_holder, TRAIT_ANTIRESONANCE, src)
+ REMOVE_TRAIT(power_holder, TRAIT_RESONANCE_SILENCED, src)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm
new file mode 100644
index 00000000000000..e07f83416f735d
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm
@@ -0,0 +1,248 @@
+/// Global tracker for Riftwalker rifts. Largely stylized after how Heretic influences work.
+GLOBAL_DATUM_INIT(riftwalker_network, /datum/riftwalker_network_tracker, new)
+
+#define RIFTWALKER_MIN_PAIRS 10
+#define RIFTWALKER_MAX_PAIRS 12
+
+/datum/riftwalker_network_tracker
+ /// List of all active rifts
+ var/list/obj/effect/riftwalker_rift/rifts = list()
+ /// Debug: counts attempts to find valid rift turfs during generation
+ var/debug_attempts = 0
+
+/datum/riftwalker_network_tracker/Destroy(force)
+ if(GLOB.riftwalker_network == src)
+ stack_trace("[type] was deleted. Riftwalkers may no longer access rifts. This is bad; call the coders!")
+ message_admins("The [type] was deleted. Riftwalkers may no longer access rifts. This is bad; call the coders!")
+ QDEL_LIST(rifts)
+ return ..()
+
+/// Generates the rifts.
+/datum/riftwalker_network_tracker/proc/generate_rifts()
+ if(length(rifts))
+ return
+ var/start_time = world.timeofday
+ debug_attempts = 0
+
+ var/pair_count = rand(RIFTWALKER_MIN_PAIRS, RIFTWALKER_MAX_PAIRS)
+ var/turf/beacon_turf = pick_valid_beacon_turf()
+ var/next_pair_id = 1
+
+ // Guarantee at least one pair originates from an active teleport beacon, if possible. Mostly for fluff to suggest the connection between teleportation and the rifts.
+ if(beacon_turf)
+ var/obj/effect/riftwalker_rift/beacon_rift = new(beacon_turf)
+ beacon_rift.pair_id = next_pair_id
+ var/turf/partner_turf
+ if(prob(25)) // 25% chance it is adjacent to a teleporter.
+ var/turf/teleporter_adjacent = pick_adjacent_teleporter_turf()
+ if(teleporter_adjacent)
+ partner_turf = teleporter_adjacent
+ else // normal turf location logic
+ partner_turf = find_random_rift_turf()
+ // spawn logic.
+ if(partner_turf)
+ var/obj/effect/riftwalker_rift/partner_rift = new(partner_turf)
+ partner_rift.pair_id = next_pair_id
+ next_pair_id++
+ else
+ QDEL_NULL(beacon_rift)
+
+
+ var/max_iterations = 0 // Just to prevent some form of infinite loop
+ // Tries creating rift pairs repeatedly up to the pair_count.
+ while(next_pair_id <= pair_count && max_iterations < 200)
+ if(!spawn_pair())
+ max_iterations++
+ continue
+ next_pair_id++
+
+ log_game("Riftwalker generate_rifts: [world.timeofday - start_time] ds, attempts=[debug_attempts], rifts=[length(rifts)], pairs=[pair_count], iterations=[max_iterations]")
+ return
+
+/// Generates a new pair of rifts.
+/datum/riftwalker_network_tracker/proc/spawn_pair()
+ var/turf/first_turf = find_random_rift_turf()
+ if(!first_turf)
+ return FALSE
+ var/turf/second_turf = find_random_rift_turf()
+ if(!second_turf)
+ return FALSE
+
+ var/next_pair_id = 1
+ for(var/obj/effect/riftwalker_rift/existing_rift as anything in rifts)
+ next_pair_id = max(next_pair_id, existing_rift.pair_id + 1)
+
+ var/obj/effect/riftwalker_rift/first_rift = new(first_turf)
+ first_rift.pair_id = next_pair_id
+ var/obj/effect/riftwalker_rift/second_rift = new(second_turf)
+ second_rift.pair_id = next_pair_id
+ return TRUE
+
+/// Main logic that gets the actual turf
+/datum/riftwalker_network_tracker/proc/find_random_rift_turf()
+ var/tries = 0
+ while(tries < 50)
+ debug_attempts++
+ var/turf/chosen_location = get_safe_random_station_turf_equal_weight()
+ if(is_valid_rift_location(chosen_location))
+ return chosen_location
+ tries++
+ return null
+
+/// Checks if a space is a valid space for a rift. Basically blocks space and prevents them from being ontop of eachother.
+/datum/riftwalker_network_tracker/proc/is_valid_rift_location(turf/target_turf)
+ if(!isturf(target_turf) || !is_station_level(target_turf.z) || isopenspaceturf(target_turf) || isgroundlessturf(target_turf))
+ return FALSE
+ for(var/obj/thing in target_turf) // don't spawn on dense objects
+ if(thing.density)
+ return FALSE
+
+ for(var/obj/effect/riftwalker_rift/existing_rift in range(1, target_turf))
+ return FALSE
+
+ return TRUE
+
+/// Specifically gets a turf next to a teleporter.
+/datum/riftwalker_network_tracker/proc/pick_adjacent_teleporter_turf()
+ var/list/turf/candidates = list()
+ for(var/obj/machinery/teleport/hub/tele as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/teleport/hub)) // I'm trying this instead of world and seeing how it goes.
+ if(!is_station_level(tele.z))
+ continue
+ var/turf/tele_turf = get_turf(tele)
+ if(!tele_turf)
+ continue
+ for(var/turf/adjacent_turf as anything in range(1, tele_turf))
+ if(adjacent_turf == tele_turf) // not ON the teleporter
+ continue
+ if(is_valid_rift_location(adjacent_turf))
+ candidates += adjacent_turf
+ if(!length(candidates))
+ return null
+ return pick(candidates)
+
+/// Specifically gets a turf next to a beacon
+/datum/riftwalker_network_tracker/proc/pick_valid_beacon_turf()
+ for(var/obj/item/beacon/beacon as anything in GLOB.teleportbeacons)
+ var/turf/beacon_turf = get_turf(beacon)
+ if(is_station_level(beacon_turf?.z) && is_valid_rift_location(beacon_turf))
+ return beacon_turf
+ return null
+
+/obj/effect/riftwalker_rift
+ name = "bluespace rift"
+ desc = "Bluespace energies connecting two places together; many Bluespace researchers would kill to understand why these rifts form. Some argue that these are left behind by heavy sums of teleportation; but these claims are unfounded."
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "bluestream"
+ anchored = TRUE
+ invisibility = INVISIBILITY_OBSERVER
+ /// Which pair this rift belongs to
+ var/pair_id = 0
+
+/obj/effect/riftwalker_rift/Initialize(mapload)
+ . = ..()
+ GLOB.riftwalker_network.rifts += src
+ RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+ apply_wibbly_filters(src)
+ if(!loc)
+ return
+ var/image/rift_image = image(icon = icon, loc = src, icon_state = icon_state, layer = OBJ_LAYER)
+ rift_image.layer = OBJ_LAYER
+ rift_image.override = TRUE
+ apply_wibbly_filters(rift_image)
+ add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/riftwalker, "riftwalker_rift", rift_image)
+
+/obj/effect/riftwalker_rift/Destroy()
+ GLOB.riftwalker_network.rifts -= src
+ UnregisterSignal(src, COMSIG_ATOM_DISPEL)
+ return ..()
+
+/obj/effect/riftwalker_rift/examine(mob/user)
+ . = ..()
+ . += span_notice("Only riftwalkers can traverse these rifts.")
+
+/// Checks if a mob can see the rifts
+/obj/effect/riftwalker_rift/proc/verify_user_can_see(mob/user)
+ return HAS_TRAIT(user, TRAIT_ABERRANT_RIFTWALKER)
+
+// Teleport logic.
+/obj/effect/riftwalker_rift/attack_hand(mob/living/user, list/modifiers)
+ . = ..()
+ if(!verify_user_can_see(user))
+ return TRUE
+ if(HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED))
+ user.balloon_alert(user, "silenced!")
+ return TRUE
+
+ var/obj/effect/riftwalker_rift/linked_rift = get_paired_rift()
+
+ var/slip_in_message = pick("slides sideways in an odd way, and disappears", "jumps into an unseen dimension",\
+ "sticks one leg straight out, wiggles [user.p_their()] foot, and is suddenly gone", "stops, then blinks out of reality", \
+ "is pulled into an invisible vortex, vanishing from sight")
+ var/slip_out_message = pick("silently fades in", "leaps out of thin air","appears", "walks out of an invisible doorway",\
+ "slides out of a fold in spacetime")
+
+ to_chat(user, span_notice("You try to align with the bluespace stream..."))
+ if(!do_after(user, 2 SECONDS, target = src))
+ return TRUE
+
+ var/turf/source_turf = get_turf(src)
+ var/turf/destination_turf = get_turf(linked_rift) || source_turf // you tp to the same space if there's no linked rift.
+
+ new /obj/effect/temp_visual/bluespace_fissure(source_turf)
+ new /obj/effect/temp_visual/bluespace_fissure(destination_turf)
+
+ user.visible_message(span_warning("[user] [slip_in_message]."), ignored_mobs = user)
+
+ var/atom/movable/pulled = null
+ if(ismovable(user.pulling))
+ pulled = user.pulling
+ if(ismob(pulled))
+ to_chat(pulled, span_notice("You suddenly find yourself in a different location!"))
+ do_teleport(pulled, destination_turf, no_effects = TRUE)
+
+ if(do_teleport(user, destination_turf, no_effects = TRUE))
+ playsound(destination_turf, SFX_PORTAL_ENTER, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ user.visible_message(span_warning("[user] [slip_out_message]."), span_notice("...and find your way to the other side."))
+ if(pulled)
+ user.start_pulling(pulled)
+ else
+ user.visible_message(span_warning("[user] [slip_out_message], ending up exactly where they left."), span_notice("...and find yourself where you started?"))
+
+ return TRUE
+
+/obj/effect/riftwalker_rift/attack_ghost(mob/user)
+ var/obj/effect/riftwalker_rift/linked_rift = get_paired_rift()
+ if(!linked_rift)
+ return ..()
+ user.abstract_move(get_turf(linked_rift))
+
+/// On dispel, closes that pair of rifts, and create a new pair somewhere else.
+/obj/effect/riftwalker_rift/proc/on_dispel(datum/source, atom/dispeller)
+ SIGNAL_HANDLER
+
+ var/obj/effect/riftwalker_rift/linked_rift = get_paired_rift()
+ if(!QDELETED(linked_rift))
+ QDEL_NULL(linked_rift)
+ if(!QDELETED(src))
+ QDEL_NULL(src)
+
+ GLOB.riftwalker_network.spawn_pair() // new pair
+ return DISPEL_RESULT_DISPELLED
+
+/// Gets the sibling rift of a rift.
+/obj/effect/riftwalker_rift/proc/get_paired_rift()
+ if(!pair_id)
+ return null
+ for(var/obj/effect/riftwalker_rift/other_rift as anything in GLOB.riftwalker_network.rifts)
+ if(other_rift != src && other_rift.pair_id == pair_id)
+ return other_rift
+ return null
+
+// Determines if a mob can see it.
+/datum/atom_hud/alternate_appearance/basic/riftwalker/mobShouldSee(mob/viewer)
+ if(!isliving(viewer))
+ return FALSE
+ return HAS_TRAIT(viewer, TRAIT_ABERRANT_RIFTWALKER)
+
+#undef RIFTWALKER_MIN_PAIRS
+#undef RIFTWALKER_MAX_PAIRS
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/riftwalker.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/riftwalker.dm
new file mode 100644
index 00000000000000..0c18847e9829be
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/riftwalker.dm
@@ -0,0 +1,17 @@
+/*
+ You can walk through persistent rifts.
+*/
+/datum/power/aberrant/riftwalker
+ name = "Riftwalker"
+ desc = "You see bluespace gateways unseen to those around you. Each station has several unique pairs of rifts that are connected that you can interact, teleporting you between them. Only you can see and interact with them.\
+ \n Interacting with it while dragging someone or something will drag them along. You cannot use these rifts while silenced."
+ security_record_text = "Subject can see and use special bluespace rifts, teleporting them between two specific points."
+ security_threat = POWER_THREAT_MAJOR
+ mob_trait = TRAIT_ABERRANT_RIFTWALKER
+ value = 5 // even if it gets you into fun places, it is rng dependent and you sometimes just end up with really bad rifts.
+ required_powers = list(/datum/power/aberrant_root/anomalous)
+
+// need the mob to be instantiated to generate rifts safely.
+/datum/power/aberrant/riftwalker/post_add(client/client_source)
+ ..()
+ GLOB.riftwalker_network.generate_rifts()
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm
new file mode 100644
index 00000000000000..a6a2af1732e75d
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm
@@ -0,0 +1,352 @@
+/** Lets us shapeshift into other mobs!
+ * Health damage carries over; halved if transforming back manually.
+ * Prones on exit
+**/
+/datum/power/aberrant/shapechange
+ name = "Shapechange"
+ desc = "You can adjust your body to turn into a specific type of animal (chosen in the power).\
+ \n Activating the ability transforms you into the chosen animal. It does not have your name or any other identifying traits, but the number is always the same when you use it (and the security record for this power elaborates on what creature and numbers). \
+ \n Using the ability makes you hungry, and cannot be used while you're starving.\
+ \n If the creature dies or the effect ends, you are reverted to your normal form (prone on the ground), and all damage taken is transfered to your original form (halved if reverting back manually)."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+ species_blacklist = list(/datum/species/android/holosynth) // there are SO MANY BUGS with holosynths I'd rather just NOT.
+
+ required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous)
+ required_allow_any = TRUE
+ action_path = /datum/action/cooldown/power/aberrant/shapechange
+
+/datum/power/aberrant/shapechange/get_security_record_text()
+ var/datum/action/cooldown/power/aberrant/shapechange/shape_action = action_path
+ if(!istype(shape_action))
+ return ""
+
+ // Resolve from the action itself so overrides (wolf/spider) are reflected in records.
+ if(!ispath(shape_action.animal_form))
+ shape_action.animal_form = shape_action.get_shapechange_type(power_holder?.client)
+
+ var/animal_name = "animal" // if we don't get anything good
+ if(ispath(shape_action.animal_form, /mob/living))
+ var/mob/living/animal_type = shape_action.animal_form
+ animal_name = initial(animal_type.name)
+
+ if(!shape_action.shape_identifier)
+ return ""
+ return "Subject can shapechange into a [animal_name] with persistent identifier #[shape_action.shape_identifier]."
+
+// Sets a persistent number identifier for the mob used in both sec records and the mob.
+/datum/power/aberrant/shapechange/post_add()
+ . = ..()
+ var/datum/action/cooldown/power/aberrant/shapechange/shape_action = action_path
+ if(!istype(shape_action))
+ return
+ if(!shape_action.shape_identifier)
+ shape_action.shape_identifier = rand(1, 999)
+ if(!ispath(shape_action.animal_form))
+ shape_action.animal_form = shape_action.get_shapechange_type(power_holder?.client)
+
+ power_holder?.refresh_security_power_records()
+
+/datum/action/cooldown/power/aberrant/shapechange
+ name = "Shapechange"
+ desc = "Change into your chosen animal form!"
+ button_icon = 'icons/mob/simple/pets.dmi'
+ button_icon_state = "corgi"
+
+ cooldown_time = 300
+
+ // We're an animorph; this would lock us out if its true.
+ human_only = FALSE
+ /// Amount of time it takes to transform.
+ use_time = 2 SECONDS
+ /// Nutrition cost when changing into animal form.
+ var/hunger_cost = 50
+ /// Tracks if the current activation performed a shift (not a revert).
+ var/just_shifted = FALSE
+ /// Persistent identifier used for the shapeshifted form.
+ var/shape_identifier = 0
+ /// The chosen animal form.
+ var/animal_form
+
+// Register dispel listener on the owner
+/datum/action/cooldown/power/aberrant/shapechange/Grant(mob/granted_to)
+ . = ..()
+ RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+/datum/action/cooldown/power/aberrant/shapechange/Remove(mob/removed_from)
+ . = ..()
+ UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
+
+/// On dispel forces you out of the mob form.
+/datum/action/cooldown/power/aberrant/shapechange/proc/on_dispel(mob/living/user, atom/dispeller)
+ SIGNAL_HANDLER
+ if(user?.has_status_effect(/datum/status_effect/shapechange_mob/aberrant))
+ user.remove_status_effect(/datum/status_effect/shapechange_mob/aberrant)
+ active = FALSE
+ to_chat(user, span_userdanger("You have been forced out of your shapeshifted form!"))
+ StartCooldown(300)
+ return DISPEL_RESULT_DISPELLED
+ return NONE
+
+// Special checks because changing mobs like this is apparently quite janky.
+/datum/action/cooldown/power/aberrant/shapechange/can_use(mob/living/user, atom/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(user.IsStun() || user.IsKnockdown())
+ owner.balloon_alert(user, "stunned!")
+ return FALSE
+ // Can't shift while being held as an item (e.g. undersized in-hand).
+ if(istype(user.loc, /obj/item/mob_holder))
+ owner.balloon_alert(user, "can't while held!")
+ return FALSE
+ // Can't shift while ventcrawling; it breaks transfer into the new mob.
+ if(user.movement_type & VENTCRAWLING)
+ owner.balloon_alert(user, "can't while ventcrawling!")
+ return FALSE
+ // We shouldn't have any active powers because it will make this power 10x more glitchy. This checks against it.
+ var/datum/action/cooldown/power/blocking_power = get_blocking_active_power(user)
+ if(blocking_power)
+ owner.balloon_alert(user, "active: [blocking_power.name]")
+ return FALSE
+ // Can't shapeshift while starving unless it is to turn back.
+ if(!user.has_status_effect(/datum/status_effect/shapechange_mob/aberrant) && user.nutrition <= NUTRITION_LEVEL_STARVING)
+ owner.balloon_alert(user, "too hungry!")
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/power/aberrant/shapechange/use_action(mob/living/user, atom/target)
+ var/datum/status_effect/shapechange_mob/aberrant/shapechange = user.has_status_effect(/datum/status_effect/shapechange_mob/aberrant)
+ if(shapechange) // we don't check for active since that doesn't carry over.
+ shapechange.manual_revert = TRUE
+ user.remove_status_effect(/datum/status_effect/shapechange_mob/aberrant)
+ active = FALSE
+ return TRUE
+
+ just_shifted = FALSE
+ if(!animal_form)
+ animal_form = get_shapechange_type(user?.client)
+ // Makes the icon look like your form after use.
+ if(ispath(animal_form))
+ var/atom/shape_path = animal_form
+ button_icon = initial(shape_path.icon)
+ button_icon_state = initial(shape_path.icon_state)
+ build_all_button_icons(UPDATE_BUTTON_ICON)
+ var/mob/living/new_shape = create_shapechange_mob(user)
+ if(!new_shape)
+ return FALSE
+
+ user.buckled?.unbuckle_mob(user, force = TRUE)
+ var/datum/status_effect/shapechange_mob/aberrant/applied = new_shape.apply_status_effect(/datum/status_effect/shapechange_mob/aberrant, user, src)
+ if(!applied)
+ to_chat(user, span_warning("Unable to shapechange!"))
+ qdel(new_shape)
+ return FALSE
+ just_shifted = TRUE
+ active = TRUE
+ return TRUE
+
+// Override do_use_time for a custom effect.
+/datum/action/cooldown/power/aberrant/shapechange/do_use_time(mob/living/user, atom/target)
+ // Skip the wind-up if we're reverting.
+ if(user.has_status_effect(/datum/status_effect/shapechange_mob/aberrant))
+ return TRUE
+ if(use_time <= 0)
+ return TRUE
+ if(DOING_INTERACTION_WITH_TARGET(user, user))
+ return FALSE
+
+ var/old_transform = user.transform
+ var/animate_step = use_time / 6
+ playsound(user, 'sound/effects/wounds/crack1.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ animate(user, transform = matrix() * 1.1, time = animate_step, easing = SINE_EASING)
+ animate(transform = matrix() * 0.9, time = animate_step, easing = SINE_EASING)
+ animate(transform = matrix() * 1.2, time = animate_step, easing = SINE_EASING)
+ animate(transform = matrix() * 0.8, time = animate_step, easing = SINE_EASING)
+ animate(transform = matrix() * 1.3, time = animate_step, easing = SINE_EASING)
+ animate(transform = matrix() * 0.1, time = animate_step, easing = SINE_EASING)
+
+ user.balloon_alert(user, "transforming...")
+ if(!do_after(user, delay = use_time, target = user))
+ animate(user, transform = matrix(), time = 0, easing = SINE_EASING)
+ user.transform = old_transform
+ return FALSE
+ // Restore the original transform after the animation sequence completes.
+ animate(user, transform = old_transform, time = 0, easing = SINE_EASING)
+ user.transform = old_transform
+ user.visible_message(span_warning("[user]'s body rearranges itself with a horrible crunching sound!"))
+ playsound(user, 'sound/effects/magic/demon_consume.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+
+// Subtract hunger on succesful use
+/datum/action/cooldown/power/aberrant/shapechange/on_action_success(mob/living/user, atom/target)
+ . = ..()
+ if(just_shifted)
+ user.adjust_nutrition(-hunger_cost)
+ just_shifted = FALSE
+
+/// Creates the relevant mob for shapeshift.
+/datum/action/cooldown/power/aberrant/shapechange/proc/create_shapechange_mob(mob/living/user)
+ var/shape_type = animal_form
+ if(!ispath(shape_type))
+ return null
+ var/mob/living/new_shape = new shape_type(user.loc)
+ // Ensure the new form inherits the caster's languages while keeping its existing ones.
+ new_shape.get_language_holder().copy_languages(user.get_language_holder())
+ apply_shape_identifier(new_shape)
+ return new_shape
+
+/// Creates the persistent random number for the shapechanger.
+/datum/action/cooldown/power/aberrant/shapechange/proc/apply_shape_identifier(mob/living/shape)
+ if(!shape)
+ return
+ if(!shape_identifier)
+ shape_identifier = rand(1, 999)
+ shape.identifier = shape_identifier
+ shape.name = initial(shape.name)
+ shape.set_name()
+
+/// Gets the chosen mob from preferend choices.
+/datum/action/cooldown/power/aberrant/shapechange/proc/get_shapechange_type(client/client_source)
+ var/choice = client_source?.prefs?.read_preference(/datum/preference/choiced/shapechange_form)
+ // defaults to parrot incase something is wrong so we don't runtime everything.
+ if(isnull(choice))
+ choice = "Parrot"
+ var/shape_type = GLOB.shapechange_form_types[choice]
+ if(ispath(shape_type))
+ return shape_type
+ return GLOB.shapechange_form_types["Parrot"]
+
+/// Returns the first active power action that should block shapechanging.
+/datum/action/cooldown/power/aberrant/shapechange/proc/get_blocking_active_power(mob/living/user)
+ if(!user || !user.powers)
+ return null
+ for(var/datum/power/power as anything in user.powers)
+ var/datum/action/cooldown/power/power_action = power.action_path
+ if(!istype(power_action))
+ continue
+ if(power_action == src)
+ continue
+ if(power_action.active)
+ return power_action
+ return null
+
+//Shapechange status effect for aberrant power. We make our own to prevent gibbed RR.
+/datum/status_effect/shapechange_mob/aberrant
+ id = "shapechange_aberrant"
+ /// The power action that caused the change
+ var/datum/weakref/source_weakref
+ /// Cached transform of the caster so we can restore non-default scaling (e.g. undersized/oversized).
+ var/matrix/caster_transform
+ /// Whether the shifted body was gibbed when it died
+ var/last_gibbed = FALSE
+ /// Whether the revert was manually triggered.
+ var/manual_revert = FALSE
+
+/datum/status_effect/shapechange_mob/aberrant/on_creation(mob/living/new_owner, mob/living/caster, datum/action/cooldown/power/aberrant/shapechange/source_action)
+ if(!istype(source_action))
+ stack_trace("Mob shapechange \"aberrant\" status effect applied without a source action.")
+ qdel(src)
+ return
+
+ source_weakref = WEAKREF(source_action)
+ return ..()
+
+/datum/status_effect/shapechange_mob/aberrant/on_apply()
+ var/datum/action/cooldown/power/aberrant/shapechange/source_action = source_weakref.resolve()
+ if(!QDELETED(source_action) && source_action.owner == caster_mob)
+ source_action.Grant(owner)
+ if(caster_mob)
+ caster_transform = matrix(caster_mob.transform)
+ return ..()
+
+/datum/status_effect/shapechange_mob/aberrant/restore_caster(kill_caster_after)
+ var/datum/action/cooldown/power/aberrant/shapechange/source_action = source_weakref.resolve()
+ if(!QDELETED(source_action) && source_action.owner == owner)
+ source_action.Grant(caster_mob)
+
+ if(owner?.contents)
+ // Prevent round removal and consuming stuff when losing shapechange
+ for(var/atom/movable/thing as anything in owner.contents)
+ if(thing == caster_mob || HAS_TRAIT(thing, TRAIT_NOT_BARFABLE))
+ continue
+ thing.forceMove(get_turf(owner))
+
+ return ..()
+
+/datum/status_effect/shapechange_mob/aberrant/on_shape_death(datum/source, gibbed)
+ last_gibbed = gibbed
+ manual_revert = FALSE
+ if(QDELETED(owner))
+ return
+ restore_caster()
+ return
+
+/datum/status_effect/shapechange_mob/aberrant/after_unchange()
+ . = ..()
+ if(QDELETED(caster_mob) || QDELETED(owner))
+ return
+
+ // Ensure any transform scaling from the shift animation is cleared.
+ caster_mob.transform = caster_transform ? caster_transform : matrix()
+
+ // Transfer damage from the shifted body back to the caster.
+ var/damage_mult = manual_revert ? 0.5 : 1
+ var/brute = owner.getBruteLoss() * damage_mult
+ var/burn = owner.getFireLoss() * damage_mult
+ var/tox = owner.getToxLoss() * damage_mult
+ var/oxy = owner.getOxyLoss() * damage_mult
+ if(brute)
+ caster_mob.apply_damage(brute, BRUTE, forced = TRUE)
+ if(burn)
+ caster_mob.apply_damage(burn, BURN, forced = TRUE)
+ if(tox)
+ caster_mob.apply_damage(tox, TOX, forced = TRUE)
+ if(oxy)
+ caster_mob.apply_damage(oxy, OXY, forced = TRUE)
+
+ caster_mob.Knockdown(6 SECONDS, ignore_canstun = TRUE)
+
+ // If we died by being gibbed, we instead apply a lot of damage and lob off a limb.
+ if(last_gibbed)
+ if(iscarbon(caster_mob))
+ var/mob/living/carbon/carbon_caster = caster_mob
+ var/zone = carbon_caster.get_random_valid_zone(even_weights = TRUE)
+ var/obj/item/bodypart/part = carbon_caster.get_bodypart(zone)
+ carbon_caster.apply_damage(150, BRUTE, part ? part : zone, forced = TRUE)
+ // MY LEG
+ if(part && part.body_zone != BODY_ZONE_HEAD && part.body_zone != BODY_ZONE_CHEST)
+ if(part.can_dismember() && !(part.bodypart_flags & BODYPART_UNREMOVABLE))
+ part.dismember()
+ else
+ caster_mob.apply_damage(150, BRUTE, forced = TRUE)
+ last_gibbed = FALSE
+ manual_revert = FALSE
+
+// Preference choice for Shapechange form selection.
+/datum/preference/choiced/shapechange_form
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "shapechange_form"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/shapechange_form/create_default_value()
+ return "Parrot"
+
+/datum/preference/choiced/shapechange_form/init_possible_values()
+ var/list/values = list()
+ for(var/choice in GLOB.shapechange_form_types)
+ values += choice
+ return values
+
+/datum/preference/choiced/shapechange_form/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return TRUE
+
+/datum/preference/choiced/shapechange_form/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/datum/power_constant_data/shapechange
+ associated_typepath = /datum/power/aberrant/shapechange
+ customization_options = list(/datum/preference/choiced/shapechange_form)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_spider.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_spider.dm
new file mode 100644
index 00000000000000..bd7cf5ee038a03
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_spider.dm
@@ -0,0 +1,74 @@
+/// Shapechange spider override.
+/datum/power/aberrant/shapechange_spider
+ name = "Shapechange: Spider"
+ desc = "Overrides your chosen Shapechange form with a spider variant. \n Hunters are fast but fragile, guards are slow and sturdy and ambush spiders are very slow, but have strong grabs, hard-hitting attacks and invisiblity in webs."
+ value = 3
+
+ required_powers = list(/datum/power/aberrant/shapechange)
+ /// Saved form so we can restore on removal.
+ var/previous_form
+
+/datum/power/aberrant/shapechange_spider/post_add()
+ . = ..()
+ var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action()
+ if(!shape_action)
+ return
+ previous_form = shape_action.animal_form
+ shape_action.animal_form = get_spider_form()
+ power_holder?.refresh_security_power_records()
+
+/datum/power/aberrant/shapechange_spider/remove()
+ var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action()
+ if(shape_action)
+ shape_action.animal_form = previous_form
+ power_holder?.refresh_security_power_records()
+ previous_form = null
+ return ..()
+
+/// Gets and returns the shapeshift action responsible
+/datum/power/aberrant/shapechange_spider/proc/get_shapechange_action()
+ if(!power_holder?.powers)
+ return null
+ for(var/datum/power/aberrant/shapechange/shape_power in power_holder.powers)
+ var/datum/action/cooldown/power/aberrant/shapechange/shape_action = shape_power.action_path
+ if(istype(shape_action))
+ return shape_action
+ return null
+
+/// Gets the preference choiced options for the spider form
+/datum/power/aberrant/shapechange_spider/proc/get_spider_form()
+ var/choice = power_holder?.client?.prefs?.read_preference(/datum/preference/choiced/shapechange_spider_form)
+ if(isnull(choice))
+ choice = "Guard"
+ var/spider_type = GLOB.shapechange_spider_form_types[choice]
+ if(ispath(spider_type))
+ return spider_type
+ return GLOB.shapechange_spider_form_types["Guard"]
+
+/// Preference choice for Shapechange spider form selection.
+/datum/preference/choiced/shapechange_spider_form
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "shapechange_spider_form"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/shapechange_spider_form/create_default_value()
+ return "Guard"
+
+/datum/preference/choiced/shapechange_spider_form/init_possible_values()
+ var/list/values = list()
+ for(var/choice in GLOB.shapechange_spider_form_types)
+ values += choice
+ return values
+
+/datum/preference/choiced/shapechange_spider_form/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+
+ return TRUE
+
+/datum/preference/choiced/shapechange_spider_form/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/datum/power_constant_data/shapechange_spider
+ associated_typepath = /datum/power/aberrant/shapechange_spider
+ customization_options = list(/datum/preference/choiced/shapechange_spider_form)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm
new file mode 100644
index 00000000000000..d4224d52d83e21
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm
@@ -0,0 +1,44 @@
+/// Inside you are two wolves. This one's an example of how to override the shapechange with special mobs.
+/datum/power/aberrant/shapechange_wolf
+ name = "Shapechange: Wolf"
+ desc = "Overrides your chosen Shapechange form with a Wolf; a fast creature with a strong bite attack."
+ value = 2
+
+ required_powers = list(/datum/power/aberrant/shapechange)
+ /// Saved form so we can restore on removal.
+ var/previous_form
+
+/datum/power/aberrant/shapechange_wolf/post_add()
+ . = ..()
+ var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action()
+ if(!shape_action)
+ return
+ previous_form = shape_action.animal_form
+ shape_action.animal_form = /mob/living/basic/mining/wolf/fast
+ power_holder?.refresh_security_power_records() // updates sec records so it lists the right mob
+
+/datum/power/aberrant/shapechange_wolf/remove()
+ var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action()
+ if(shape_action)
+ shape_action.animal_form = previous_form
+ power_holder?.refresh_security_power_records() // updates sec records so it lists the right mob
+ previous_form = null
+ return ..()
+
+/// Gets the action reference for shapechange
+/datum/power/aberrant/shapechange_wolf/proc/get_shapechange_action()
+ if(!power_holder?.powers)
+ return null
+ for(var/datum/power/aberrant/shapechange/shape_power in power_holder.powers)
+ var/datum/action/cooldown/power/aberrant/shapechange/shape_action = shape_power.action_path
+ if(istype(shape_action))
+ return shape_action
+ return null
+
+// Wolves are pack animals and only deal 7dmg wich is SAD. We have a special version, which is less tanky but faster and bitier
+/mob/living/basic/mining/wolf/fast
+ maxHealth = 100
+ health = 100
+ melee_damage_lower = 10
+ melee_damage_upper = 20
+ speed = -0.1 // keeps pace with naked humanoid mobs
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm
new file mode 100644
index 00000000000000..b8eb6d51a606d7
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm
@@ -0,0 +1,344 @@
+/*
+ You can be summoned by speaking a specific keywords.
+*/
+/datum/power/aberrant/summonable
+ name = "Summonable"
+ desc = "By speaking a specific name or word, you appear next to the speaker after a short delay. The summoning takes time, you are stunned throughout, is entirely involuntary and can only be stopped by being silenced, buckled or dispelled.\
+ \n After being succesfuly summoned, you are unable to be summoned again for 1 minute. \
+ \n The chosen word is a partial secret; the Security Records on your powers contain the word as well. It cannot contain any special characters, only standard letters and numbers."
+ security_threat = POWER_THREAT_MAJOR
+ value = 7
+
+ required_powers = list(/datum/power/aberrant_root/anomalous)
+
+ /// Reference to the beetlejuice component
+ var/datum/component/beetlejuice/summonable/summon_component
+
+// Lists the word in sec records.
+/datum/power/aberrant/summonable/get_security_record_text()
+ var/keyword = summon_component?.keyword
+ if(!keyword)
+ keyword = power_holder?.client?.prefs?.read_preference(/datum/preference/text/summonable_keyword)
+ if(!keyword)
+ var/datum/preference/text/summonable_keyword/pref_entry = GLOB.preference_entries[/datum/preference/text/summonable_keyword]
+ keyword = pref_entry?.create_default_value() || "Beetlejuice"
+ return "Subject is summonable via keyword \"[keyword]\"."
+
+// Adds the custom beetlejuice component and sets the beetlejuiec word.
+/datum/power/aberrant/summonable/post_add()
+ if(!power_holder)
+ return
+
+ var/mob/living/holder = power_holder
+ var/datum/component/beetlejuice/summonable/component = holder.GetComponent(/datum/component/beetlejuice/summonable)
+ if(!component)
+ component = holder.AddComponent(/datum/component/beetlejuice/summonable)
+
+ summon_component = component
+
+ var/keyword = holder.client?.prefs?.read_preference(/datum/preference/text/summonable_keyword)
+ if(!keyword)
+ var/datum/preference/text/summonable_keyword/pref_entry = GLOB.preference_entries[/datum/preference/text/summonable_keyword]
+ keyword = pref_entry?.create_default_value() || "Beetlejuice"
+
+ component.keyword = keyword
+ component.update_regex()
+ component.rune_color = holder.client?.prefs?.read_preference(/datum/preference/color/summonable_rune_color) || component.rune_color
+
+ . = ..()
+
+/datum/power/aberrant/summonable/remove()
+ . = ..()
+ if(summon_component)
+ QDEL_NULL(summon_component)
+
+// Custom beetlejuice component for Summonable.
+/datum/component/beetlejuice/summonable
+ min_count = 1
+ cooldown = 60 SECONDS // for the love of god don't make this shorter than 10 seconds you will break things.
+ /// Delay after your name being mentioned before the summoning begins
+ var/summon_delay = 1 SECONDS
+ /// How long it takes for you to fully float up
+ var/float_time = 3.5 SECONDS
+ /// Radius for orbiting runes
+ var/rune_orbit_radius = 30
+ /// Rotation speed for orbiting runes
+ var/rune_rotation_speed = 30
+ /// Amount of runes that orbit
+ var/rune_count = 8
+ /// Duration between each rune being sapwned
+ var/rune_spawn_interval = 3.4
+ /// Time for runes to fade in
+ var/rune_fade_time = 6
+ /// Color of the runes
+ var/rune_color = "#ff2a2a"
+
+ /// Are we currently being summoned? (mostly used for dispels)
+ var/summoning = FALSE
+ /// Are we currently being beamed up? (mostly used for dispels)
+ var/beaming_up = FALSE
+ /// List of currnet active runes orbiting the mob.
+ var/list/obj/effect/summonable_rune_orbiter/current_runes
+
+// Custom apport because frankly put its cooler this way.
+/datum/component/beetlejuice/summonable/apport(atom/target)
+ var/atom/movable/summoned = parent
+ if(ismob(summoned))
+ var/mob/living/living_summoned = summoned
+ if(living_summoned.buckled || HAS_TRAIT(living_summoned, TRAIT_RESONANCE_SILENCED))
+ return
+ var/turf/target_turf = get_adjacent_open_turf(target)
+ if(QDELETED(summoned) || !target_turf)
+ return
+ active = FALSE
+ addtimer(VARSET_CALLBACK(src, active, TRUE), cooldown)
+ addtimer(CALLBACK(src, PROC_REF(begin_summon), summoned, target_turf), summon_delay)
+
+/// Gets a valid nearby turf within the mob's area.
+/datum/component/beetlejuice/summonable/proc/get_adjacent_open_turf(atom/target)
+ var/turf/center = get_turf(target)
+ if(!center)
+ return null
+ var/list/candidates = list()
+ for(var/turf/T in orange(1, center))
+ if(T == center)
+ continue
+ if(T.is_blocked_turf(exclude_mobs = FALSE, ignore_atoms = list(/obj/structure/table), type_list = TRUE))
+ continue
+ candidates += T
+ if(!length(candidates))
+ return null
+ return pick(candidates)
+
+/// Starts the timers and starts manifesting effects.
+/datum/component/beetlejuice/summonable/proc/begin_summon(atom/movable/summoned, turf/target_turf)
+ if(QDELETED(summoned) || QDELETED(target_turf))
+ return
+ if(isliving(summoned))
+ var/mob/living/living_summoned = summoned
+ if(HAS_TRAIT(living_summoned, TRAIT_RESONANCE_SILENCED))
+ return
+ summoning = TRUE
+ beaming_up = TRUE
+ // Start departure immediately while runes are appearing.
+ var/turf/origin_turf = get_turf(summoned)
+ var/obj/effect/temp_visual/spotlight/summonable/origin_spotlight = origin_turf ? new(origin_turf, rune_color) : null
+
+ var/old_alpha = summoned.alpha
+ var/old_pixel_y = summoned.pixel_y
+
+ RegisterSignal(summoned, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+ summoned.anchored = TRUE
+ ADD_TRAIT(summoned, TRAIT_IMMOBILIZED, "summonable_apport")
+ // Keep them standing but unable to act; float without full levitation.
+ ADD_TRAIT(summoned, TRAIT_MOVE_FLOATING, "summonable_apport")
+
+ // Depart: float up and fade out at the origin.
+ summoned.visible_message(span_warning("[summoned] leaves the ground, and begins to vanish into thin air!"))
+ animate(summoned, alpha = 0, pixel_y = old_pixel_y + 32, time = float_time)
+ addtimer(CALLBACK(src, PROC_REF(clear_origin_spotlight), origin_spotlight), float_time)
+
+ var/list/obj/effect/summonable_rune_orbiter/runes = list()
+ current_runes = runes
+ addtimer(CALLBACK(src, PROC_REF(spawn_rune_sequence), summoned, target_turf, runes, 1, old_alpha, old_pixel_y), 0)
+
+/// Removes the spotlight
+/datum/component/beetlejuice/summonable/proc/clear_origin_spotlight(obj/effect/temp_visual/spotlight/summonable/origin_spotlight)
+ QDEL_NULL(origin_spotlight)
+
+/// Creates the cool floaty runes
+/datum/component/beetlejuice/summonable/proc/spawn_rune_sequence(atom/movable/summoned, turf/target_turf, list/obj/effect/summonable_rune_orbiter/runes, rune_index, old_alpha, old_pixel_y)
+ if(!summoning)
+ QDEL_LIST(runes)
+ return
+ if(QDELETED(summoned) || QDELETED(target_turf))
+ QDEL_LIST(runes)
+ return
+ if(rune_index > rune_count)
+ begin_arrival(summoned, target_turf, runes, old_alpha, old_pixel_y)
+ return
+
+ var/obj/effect/summonable_rune_orbiter/rune = new(target_turf, rune_color)
+ rune.orbit(target_turf, rune_orbit_radius, rotation_speed = rune_rotation_speed, rotation_segments = rune_count, pre_rotation = FALSE)
+ runes += rune
+
+ addtimer(CALLBACK(src, PROC_REF(spawn_rune_sequence), summoned, target_turf, runes, rune_index + 1, old_alpha, old_pixel_y), rune_spawn_interval)
+
+/// BEGINS THE RAPTURE
+/datum/component/beetlejuice/summonable/proc/begin_arrival(atom/movable/summoned, turf/target_turf, list/obj/effect/summonable_rune_orbiter/runes, old_alpha, old_pixel_y)
+ if(!summoning)
+ QDEL_LIST(runes)
+ return
+ if(QDELETED(summoned) || QDELETED(target_turf))
+ QDEL_LIST(runes)
+ return
+ beaming_up = FALSE
+
+ var/obj/effect/temp_visual/spotlight/summonable/spotlight = new(target_turf, rune_color)
+ fade_and_clear_runes(runes)
+
+ summoned.forceMove(target_turf)
+ summoned.alpha = 0
+ summoned.pixel_y = 32
+ animate(summoned, alpha = old_alpha, pixel_y = old_pixel_y, time = float_time)
+
+ playsound(summoned, 'sound/effects/magic/voidblink.ogg', 50, TRUE)
+ summoned.visible_message(span_warning("[summoned] appears out of thin air!"))
+
+ addtimer(CALLBACK(src, PROC_REF(finish_summon), summoned, target_turf, old_alpha, old_pixel_y, spotlight), float_time)
+
+/// Fade and clear the runes.
+/datum/component/beetlejuice/summonable/proc/fade_and_clear_runes(list/obj/effect/summonable_rune_orbiter/runes)
+ for(var/obj/effect/summonable_rune_orbiter/rune in runes)
+ animate(rune, alpha = 0, time = rune_fade_time)
+ addtimer(CALLBACK(src, PROC_REF(clear_runes), runes), rune_fade_time)
+
+/// Removes all active runes.
+/datum/component/beetlejuice/summonable/proc/clear_runes(list/obj/effect/summonable_rune_orbiter/runes)
+ QDEL_LIST(runes)
+
+/// Alright, shows over, he's here now. Time to pack up and go.
+/datum/component/beetlejuice/summonable/proc/finish_summon(atom/movable/summoned, turf/target_turf, old_alpha, old_pixel_y, obj/effect/temp_visual/spotlight/summonable/spotlight)
+ if(QDELETED(summoned))
+ QDEL_NULL(spotlight)
+ return
+
+ summoned.alpha = old_alpha
+ summoned.pixel_y = old_pixel_y
+ summoned.anchored = FALSE
+ REMOVE_TRAIT(summoned, TRAIT_IMMOBILIZED, "summonable_apport")
+ REMOVE_TRAIT(summoned, TRAIT_MOVE_FLOATING, "summonable_apport")
+ if(target_turf)
+ summoned.forceMove(target_turf)
+ // Explicitly trigger glass table break checks on landing. This isn't clean, but its too funny to not have it.
+ if(isliving(summoned))
+ var/mob/living/living_summoned = summoned
+ var/obj/structure/table/glass/glass_table = locate(/obj/structure/table/glass) in get_turf(living_summoned)
+ if(glass_table)
+ glass_table.check_break(living_summoned)
+
+ QDEL_NULL(spotlight)
+ UnregisterSignal(summoned, COMSIG_ATOM_DISPEL)
+ summoning = FALSE
+ beaming_up = FALSE
+ current_runes = null
+ active = FALSE
+ addtimer(VARSET_CALLBACK(src, active, TRUE), cooldown)
+
+/// Ends summon at certain stages.
+/datum/component/beetlejuice/summonable/proc/on_dispel(atom/movable/target, atom/dispeller)
+ SIGNAL_HANDLER
+ // Only cancel if they're currently being beamed up.
+ if(!beaming_up || !summoning)
+ return NONE
+ cancel_summon(target)
+ if(ishuman(target))
+ var/mob/living/carbon/human/failed_summon = target
+ // Do you have anything to brace your fall? Or do you possibly manage to get lucky?
+ var/obj/item/organ/wings/gliders = failed_summon.get_organ_by_type(/obj/item/organ/wings)
+ if(HAS_TRAIT(failed_summon, TRAIT_FREERUNNING) || gliders?.can_soften_fall() || prob(20))
+ failed_summon.visible_message(span_warning("[failed_summon] suddenly reappears and lands back on the ground!"), span_warning("You drop to the ground, but manage to catch yourself!"))
+ else
+ failed_summon.visible_message(span_warning("[failed_summon] suddenly reappears and falls face-first onto the ground!"), span_userdanger("You suddenly fall face-first onto the ground!"))
+ playsound(failed_summon, 'sound/effects/desecration/desecration-02.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ failed_summon.adjustBruteLoss(5)
+ failed_summon.Knockdown(3 SECONDS)
+ return DISPEL_RESULT_DISPELLED
+
+/// Ends the summoning right there and now.
+/datum/component/beetlejuice/summonable/proc/cancel_summon(atom/movable/summoned)
+ if(summoned)
+ summoned.alpha = initial(summoned.alpha)
+ summoned.pixel_y = initial(summoned.pixel_y)
+ summoned.anchored = FALSE
+ REMOVE_TRAIT(summoned, TRAIT_IMMOBILIZED, "summonable_apport")
+ REMOVE_TRAIT(summoned, TRAIT_MOVE_FLOATING, "summonable_apport")
+ UnregisterSignal(summoned, COMSIG_ATOM_DISPEL)
+ if(current_runes)
+ QDEL_LIST(current_runes)
+ current_runes = null
+ summoning = FALSE
+ beaming_up = FALSE
+
+// Preference choice for Summonable keyword selection.
+/datum/preference/text/summonable_keyword
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "summonable_keyword"
+ savefile_identifier = PREFERENCE_CHARACTER
+ can_randomize = FALSE
+ maximum_value_length = 32
+
+/datum/preference/text/summonable_keyword/create_default_value()
+ return "Beetlejuice"
+
+/datum/preference/text/summonable_keyword/is_valid(value)
+ if(!istext(value))
+ return FALSE
+ if(length(value) < 1 || length(value) >= maximum_value_length)
+ return FALSE
+ // Allow only ASCII letters and numbers.
+ var/quoted = REGEX_QUOTE(value)
+ var/static/regex/allowed_regex = regex("^" + ascii2text(91) + "A-Za-z0-9" + ascii2text(93) + "+$")
+ allowed_regex.next = 1
+ return !!allowed_regex.Find(quoted)
+
+/datum/preference/text/summonable_keyword/deserialize(input, datum/preferences/preferences)
+ var/value = ..()
+ if(!is_valid(value))
+ return null
+ return value
+
+/datum/preference/text/summonable_keyword/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+// Preference choice for Summonable rune/spotlight color.
+/datum/preference/color/summonable_rune_color
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "summonable_rune_color"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/color/summonable_rune_color/create_default_value()
+ return "ff2a2a"
+
+/datum/preference/color/summonable_rune_color/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+ return TRUE
+
+/datum/preference/color/summonable_rune_color/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/datum/power_constant_data/summonable
+ associated_typepath = /datum/power/aberrant/summonable
+ customization_options = list(/datum/preference/text/summonable_keyword, /datum/preference/color/summonable_rune_color)
+
+// Orbiting rune for Summonable arrival.
+/obj/effect/summonable_rune_orbiter
+ icon = 'icons/effects/eldritch.dmi'
+ icon_state = "small_rune_1"
+ layer = BELOW_MOB_LAYER
+ anchored = TRUE
+ mouse_opacity = 0
+
+// We set the specific icons because we don't want the color shifting. Beyond that, colors!
+/obj/effect/summonable_rune_orbiter/Initialize(mapload, rune_color = "#ff2a2a")
+ var/rune_state = "small_rune_[rand(1, 10)]"
+ var/icon/rune_icon = icon('icons/effects/eldritch.dmi', rune_state, frame = 1)
+ // Force the base green to a greyscale color.
+ rune_icon.MapColors(0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33)
+ // Boost brightness before applying the chosen color.
+ rune_icon.Blend(rgb(160, 160, 160), ICON_ADD)
+ // Apply the color from prefs.
+ rune_icon.Blend(rune_color, ICON_MULTIPLY)
+ icon = rune_icon
+ icon_state = null
+ return ..()
+
+// Green spotlight at the destination.
+/obj/effect/temp_visual/spotlight/summonable
+ color = COLOR_RED
+ duration = 3 SECONDS
+
+/obj/effect/temp_visual/spotlight/summonable/Initialize(mapload, spotlight_color = COLOR_RED)
+ color = spotlight_color
+ return ..()
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/tail_sweep.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/tail_sweep.dm
new file mode 100644
index 00000000000000..e5fb73adbf5de0
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/tail_sweep.dm
@@ -0,0 +1,77 @@
+/*
+ Swing your tail!
+*/
+/datum/power/aberrant/tailsweep
+ name = "Tail Sweep"
+ desc = "Your tail is a weapon in its own right. When activated, damages all creatures adjacent to you for 20 brute and 30 stamina, and knocks them away 2 spaces, potentially into walls.\
+ \n Has a short cooldown, consumes hunger and the damage is affected by your opponent's chest armor. Requires a tail. If you are a large mob (such as with the Oversized quirk), you gain +1 range."
+ security_record_text = "Subject can use their tail to damage and knock back foes in active combat."
+ security_threat = POWER_THREAT_MAJOR
+ value = 4
+
+ required_powers = list(/datum/power/aberrant_root/beastial)
+ action_path = /datum/action/cooldown/power/aberrant/tailsweep
+
+/datum/action/cooldown/power/aberrant/tailsweep
+ name = "Tail Sweep"
+ desc = "Your tail is a weapon in its own right. When activated, damages all creatures adjacent to you for 20 brute and 30 stamina, and knocks them away 2 spaces, potentially into walls."
+ button_icon = 'icons/mob/actions/actions_xeno.dmi'
+ button_icon_state = "tailsweep"
+ cooldown_time = 6 SECONDS
+
+ /// Base range.
+ var/range = 1
+ /// Throw distance
+ var/throw_dist = 2
+ /// Hunger cost of the power
+ var/hunger_cost = 10
+ /// How much brute damage it deals
+ var/damage = 20
+ /// How much stam damage it deals
+ var/stam_damage = 30
+ /// Path of the effect that appears when you get smacked by the tail
+ var/on_hit_vfx = /obj/effect/temp_visual/dir_setting/tailsweep
+
+/datum/action/cooldown/power/aberrant/tailsweep/can_use(mob/living/user, atom/target)
+ if(iscarbon(user)) // we don't check for tails on non-carbons; I figured it should only exist on others for admeme reasons.
+ var/mob/living/carbon/carbon_user = user
+ var/obj/item/organ/tail/tail = carbon_user.get_organ_slot(ORGAN_SLOT_EXTERNAL_TAIL)
+ if(!tail)
+ owner.balloon_alert(user, "no tail")
+ return FALSE
+ if(user.nutrition <= NUTRITION_LEVEL_STARVING) // can't use while starving
+ owner.balloon_alert(user, "too hungry!")
+ return FALSE
+ . = ..()
+
+/datum/action/cooldown/power/aberrant/tailsweep/use_action(mob/living/user, atom/target)
+ playsound(get_turf(user), 'sound/effects/magic/tail_swing.ogg', 80, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ user.visible_message(user, span_danger("[user] swings their tail aggressively in an arc around themselves!"))
+ user.spin(0.6 SECONDS, 1)
+ // checks if the mob is large; if so +1 to distance.
+ var/effective_range = range + ((user.mob_size >= MOB_SIZE_LARGE) ? 1 : 0)
+ for(var/mob/living/victim in oview(effective_range, user))
+ // feedback
+ to_chat(victim, span_userdanger("[user] knocks you back with their tail!"))
+ new on_hit_vfx(get_turf(victim), get_dir(user, victim))
+
+ // damaging. The reason why we complicate this is because we want it to be affected by body armour.
+ var/dmg_dealt = victim.apply_damage(damage, BRUTE, BODY_ZONE_CHEST, victim.run_armor_check(BODY_ZONE_CHEST, MELEE))
+ var/stam_dmg_dealt = victim.apply_damage(stam_damage, STAMINA, BODY_ZONE_CHEST, victim.run_armor_check(BODY_ZONE_CHEST, MELEE))
+
+ // logging
+ victim.log_message("was tail-sweeped by [user] for [dmg_dealt] brute damage and [stam_dmg_dealt] stamina damage.", LOG_VICTIM)
+ user.log_message("has tail-sweeped [victim] for [dmg_dealt] brute damage and [stam_dmg_dealt] stamina damage.", LOG_ATTACK)
+
+ // throwing
+ if(victim.anchored)
+ continue
+ var/dir_to_victim = get_dir(user, victim)
+ var/turf/throw_target = get_ranged_target_turf(victim, dir_to_victim, 2)
+ if(throw_target)
+ victim.throw_at(throw_target, throw_dist, 1, thrower = user, force = MOVE_FORCE_STRONG)
+ return TRUE
+
+/datum/action/cooldown/power/aberrant/shapechange/on_action_success(mob/living/user, atom/target)
+ if(iscarbon(user))
+ user.adjust_nutrition(-hunger_cost)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/vent_crawl.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/vent_crawl.dm
new file mode 100644
index 00000000000000..acc2226f74a1c4
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/vent_crawl.dm
@@ -0,0 +1,83 @@
+// Vent crawling with caveats. Beware; this is quite jerry-rigged just to keep it modular.
+/datum/power/aberrant/vent_crawl
+ name = "Vent Crawl"
+ desc = "Your anatomy is capable of fitting in tight spaces. You can crawl into vents if you are not wearing anything in your back slot, helmet slot or suit slot. \
+ \nIf you are undersized, you can crawl in vents while wearing your normal equipment. Does not work on oversized mobs."
+ security_record_text = "Subject can crawl through ventilation shafts."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+ power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES
+ required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous)
+ required_allow_any = TRUE
+
+/datum/power/aberrant/vent_crawl/add(client/client_source)
+ . = ..()
+ if(!power_holder)
+ return
+ ADD_TRAIT(power_holder, TRAIT_VENTCRAWLER_ALWAYS, src)
+ RegisterSignal(power_holder, COMSIG_MOB_ALTCLICKON, PROC_REF(on_altclick))
+
+/datum/power/aberrant/vent_crawl/remove()
+ if(power_holder)
+ REMOVE_TRAIT(power_holder, TRAIT_VENTCRAWLER_ALWAYS, src)
+ REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src)
+ UnregisterSignal(power_holder, COMSIG_MOB_ALTCLICKON)
+ return ..()
+
+/** Ventcrawling only has two states; always and nude. This is kind-of cringe; but I don't want to tweak ventcrawling.dm unncessarily just for one power.
+ * We process and check if they're vent_crawling; if true, we check for restricted gear. If true, we immobilize them til they fukken undress.
+ * It's gross but it's either mr riot suit crawling out of the vents or everyone being buck naked.
+**/
+/datum/power/aberrant/vent_crawl/process(seconds_per_tick)
+ if(!power_holder)
+ return
+ // If a different source grants always-ventcrawling, don't enforce restrictions here.
+ if(HAS_TRAIT(power_holder, TRAIT_VENTCRAWLER_ALWAYS) && !HAS_TRAIT_FROM_ONLY(power_holder, TRAIT_VENTCRAWLER_ALWAYS, src))
+ REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src)
+ return
+ // Disqualifies for gear check if not ventcrawling
+ if(!(power_holder.movement_type & VENTCRAWLING) || !HAS_TRAIT(power_holder, TRAIT_MOVE_VENTCRAWLING))
+ REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src)
+ return
+ // Disqualifies for gear check if undersized
+ if(HAS_TRAIT(power_holder, TRAIT_UNDERSIZED))
+ REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src)
+ return
+
+ // Check if they are wearing a back slot, helmet slot or suit slot. Hands are fine.
+ if(has_restricted_gear(power_holder))
+ ADD_TRAIT(power_holder, TRAIT_IMMOBILIZED, src)
+ // Clear it incase they don't and are immobilized from this.
+ else
+ REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src)
+
+/// Alt clicking on vents. Prevent them from venting if they're wearing too much crap.
+/datum/power/aberrant/vent_crawl/proc/on_altclick(mob/living/source, atom/target)
+ SIGNAL_HANDLER
+ if(!can_use_ventcrawl(source))
+ if(istype(target, /obj/machinery/atmospherics/components/unary))
+ return COMSIG_MOB_CANCEL_CLICKON
+ return
+ if(!istype(target, /obj/machinery/atmospherics/components/unary))
+ return
+ if(HAS_TRAIT(source, TRAIT_UNDERSIZED))
+ return
+ if(!has_restricted_gear(source))
+ return
+ source.balloon_alert(source, "Need empty back, helmet & suit slot!")
+ to_chat(source, span_warning("You need to remove your backpack, helmet, and suit to ventcrawl!"))
+ return COMSIG_MOB_CANCEL_CLICKON
+
+/// Are you TOO FUKKEN BIG? or are you SILENCED?
+/datum/power/aberrant/vent_crawl/proc/can_use_ventcrawl(mob/living/source)
+ if(HAS_TRAIT(source, TRAIT_RESONANCE_SILENCED))
+ source.balloon_alert(source, "Silenced!")
+ if(HAS_TRAIT(source, TRAIT_OVERSIZED))
+ source.balloon_alert(source, "You're too big to fit!")
+ return FALSE
+ return TRUE
+
+/// Checks for back slot, head slot and suit slot
+/datum/power/aberrant/vent_crawl/proc/has_restricted_gear(mob/living/source)
+ var/mob/living/carbon/carbon_source = source
+ return carbon_source.get_item_by_slot(ITEM_SLOT_BACK) || carbon_source.get_item_by_slot(ITEM_SLOT_HEAD) || carbon_source.get_item_by_slot(ITEM_SLOT_OCLOTHING)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_craft_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_craft_datum.dm
new file mode 100644
index 00000000000000..5a9551fdd75efa
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_craft_datum.dm
@@ -0,0 +1,74 @@
+// Used to store what web crafter can make and pass it back to the power. Partially so other powers can add onto it without too much hasle.
+/datum/web_craft_entry
+ /// Type spawned by this entry
+ var/obj/spawn_type
+ /// Hunger cost to craft
+ var/hunger_cost = 0
+ /// Time to craft (do_after). 0 for instant.
+ var/craft_time = 0
+ /// Display name for the radial
+ var/display_name
+ /// Description shown in tooltip
+ var/desc
+ /// Icon data for radial choice
+ var/icon
+ var/icon_state
+ /// Whether this should be placed on the turf instead of in hands
+ var/is_structure = FALSE
+
+/datum/web_craft_entry/New()
+ . = ..()
+ if(ispath(spawn_type, /obj/structure))
+ is_structure = TRUE
+ if(ispath(spawn_type))
+ if(!display_name)
+ display_name = spawn_type.name
+ if(!desc)
+ desc = spawn_type.desc
+ if(!icon)
+ icon = spawn_type.icon
+ if(!icon_state)
+ icon_state = spawn_type.icon_state
+
+/// Populates the list of radial menu choices.
+/datum/web_craft_entry/proc/get_radial_choice()
+ if(!display_name || !icon || !icon_state)
+ return null
+ var/datum/radial_menu_choice/choice = new()
+ choice.name = display_name
+ choice.image = image(icon = icon, icon_state = icon_state)
+ var/list/info_bits = list()
+ if(desc)
+ info_bits += desc
+ info_bits += "Cost: [hunger_cost] hunger"
+ if(craft_time > 0)
+ info_bits += "Time: [craft_time/10]s"
+ choice.info = jointext(info_bits, " ")
+ return choice
+
+/// Checks if the related web entry can be placed.
+/datum/web_craft_entry/proc/can_place(mob/living/user, turf/target_turf)
+ return TRUE
+
+/// In the event we need to pass data to the object, e.g tripwire webs or do other fancy stuff on spawn
+/datum/web_craft_entry/proc/spawn_entry(mob/living/user, turf/target_turf)
+ if(is_structure)
+ return spawn_structure(user, target_turf)
+ return spawn_item(user)
+
+/// Spawns the appropriate item
+/datum/web_craft_entry/proc/spawn_item(mob/living/user)
+ return new spawn_type(user)
+
+/// Spawns physical structures
+/datum/web_craft_entry/proc/spawn_structure(mob/living/user, turf/target_turf)
+ return new spawn_type(target_turf)
+
+/datum/web_craft_entry/stickyweb/can_place(mob/living/user, turf/target_turf)
+ if(HAS_TRAIT(target_turf, TRAIT_SPINNING_WEB_TURF))
+ user.balloon_alert(user, "already being webbed!")
+ return FALSE
+ if(locate(/obj/structure/spider/stickyweb) in target_turf)
+ user.balloon_alert(user, "already webbed!")
+ return FALSE
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_crafter_entries.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_crafter_entries.dm
new file mode 100644
index 00000000000000..d77c7ccab12073
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_crafter_entries.dm
@@ -0,0 +1,42 @@
+// Base two web crafting items that come with web_crafter
+/datum/web_craft_entry/cloth
+ desc = "Cloth made from your silk! Practically indistinguishable, but you might make people awkward if they start wearing clothes made from it."
+ spawn_type = /obj/item/stack/sheet/cloth
+ hunger_cost = 7
+ craft_time = 1 SECONDS
+
+/datum/web_craft_entry/stickyweb
+ desc = "A sticky web; sticky for everyone but you. Your colleagues may not appreciate it."
+ spawn_type = /obj/structure/spider/stickyweb
+ hunger_cost = 5
+ craft_time = 1 SECONDS
+ icon = 'icons/effects/web.dmi'
+ icon_state = "webpassage"
+
+// Binding Webs
+/datum/web_craft_entry/web_bola
+ desc = "Sticky bola. Others can't use it without risking snaring themselves."
+ spawn_type = /obj/item/restraints/legcuffs/bola/web
+ hunger_cost = 10
+
+/datum/web_craft_entry/web_restraints
+ desc = "Sticky zipties. Destroyed after use; others can't use it without risking binding themselves."
+ spawn_type = /obj/item/restraints/handcuffs/cable/zipties/web
+ hunger_cost = 10
+
+// Snare Webs
+/datum/web_craft_entry/web_snare
+ desc = "Creates a barely visible web snare that traps the legs of any mob that walk through it."
+ spawn_type = /obj/structure/spider/web_snare
+ hunger_cost = 10
+ craft_time = 2 SECONDS
+
+// Tripwire Webs
+/datum/web_craft_entry/tripwire_web
+ desc = "Creates a barely visible tripwire snare that silently tells you if a mob walk throughs it."
+ spawn_type = /obj/structure/spider/tripwire_web
+ hunger_cost = 5
+ craft_time = 1 SECONDS
+
+/datum/web_craft_entry/tripwire_web/spawn_structure(mob/living/user, turf/target_turf)
+ return new /obj/structure/spider/tripwire_web(target_turf, user)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/binding_webs.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/binding_webs.dm
new file mode 100644
index 00000000000000..ef0f1b3694214b
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/binding_webs.dm
@@ -0,0 +1,89 @@
+// Creates snaaaares
+/datum/power/aberrant/binding_webs
+ name = "Binding Webs"
+ desc = " Allows you to craft web restraints and web bolas using web crafter. Web restraints are functionally similar to zipties. Web Bolas can be thrown just like regular bolas."
+ security_record_text = "Subject can craft bolas and restraints from their spider silk."
+ security_threat = POWER_THREAT_MAJOR
+ value = 3
+
+ required_powers = list(/datum/power/aberrant/web_crafter)
+
+/datum/power/aberrant/binding_webs/post_add(client/client_source)
+ . = ..()
+ var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action()
+ if(!action)
+ return
+ action.web_craft_entries |= /datum/web_craft_entry/web_restraints
+ action.web_craft_entries |= /datum/web_craft_entry/web_bola
+
+/datum/power/aberrant/binding_webs/remove()
+ var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action()
+ if(!action)
+ return
+ action.web_craft_entries -= /datum/web_craft_entry/web_restraints
+ action.web_craft_entries -= /datum/web_craft_entry/web_bola
+
+/// Gets the approrpiate web crafter action
+/datum/power/aberrant/binding_webs/proc/get_web_crafter_action()
+ if(!power_holder)
+ return null
+ for(var/datum/action/cooldown/power/aberrant/web_crafter/action in power_holder.actions)
+ return action
+ return null
+
+// Reflavored
+/obj/item/restraints/handcuffs/cable/zipties/web
+ name = "web ties"
+ desc = "Sticky strings meant for binding pesky hands. Be careful not to get yourself stuck!"
+ breakouttime = 60 SECONDS // sticky = better
+ /// Tracks if this was actually used as cuffs so we can delete on uncuff only.
+ var/was_cuffed = FALSE
+
+// If you're not a web weaver yourself, you might get yourself stuck using it instead. Or if you're clumsy, it will DEFINETLY happy.
+/obj/item/restraints/handcuffs/cable/zipties/web/attempt_to_cuff(mob/living/carbon/victim, mob/living/user)
+ if(iscarbon(user) && !HAS_TRAIT(user, TRAIT_WEB_SURFER) && (HAS_TRAIT(user, TRAIT_CLUMSY) || prob(50)))
+ to_chat(user, span_warning("Your hands get stuck in the webs!"))
+ apply_cuffs(user, user)
+ return
+ return ..()
+
+/obj/item/restraints/handcuffs/cable/zipties/web/equipped(mob/living/user, slot)
+ . = ..()
+ if(slot == ITEM_SLOT_HANDCUFFED)
+ was_cuffed = TRUE
+ RegisterSignal(src, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(on_uncuffed))
+
+// why do we not have an uncuff proc on cuffs hello?!?!?!
+/obj/item/restraints/handcuffs/cable/zipties/web/on_uncuffed(datum/source, mob/living/wearer)
+ ..()
+ if(was_cuffed)
+ qdel(src)
+
+// Just normal bolas but extra webby and the same caveat as handcuffs.
+/obj/item/restraints/legcuffs/bola/web
+ name = "web bola"
+ desc = "A bola made out of a sticky material. Throwing this will definetly get at least one involved party stuck."
+ breakouttime = 6 SECONDS // sticky = better
+ icon = 'modular_doppler/modular_powers/icons/items/restraints.dmi'
+ /// Tracks if this was actually used as legcuffs so we can delete on uncuff only.
+ var/was_cuffed = FALSE
+
+// Just like webcuffs, chance of ensnaring yourself instead
+/obj/item/restraints/legcuffs/bola/web/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, gentle = FALSE, quickstart = TRUE, throw_type_path = /datum/thrownthing)
+ if(iscarbon(thrower) && !HAS_TRAIT(thrower, TRAIT_WEB_SURFER) && (HAS_TRAIT(thrower, TRAIT_CLUMSY) || prob(50)))
+ to_chat(thrower, span_warning("The bola sticks to your hands, whiffing the throw and entangling yourself instead!"))
+ ensnare(thrower)
+ return
+ return ..()
+
+/obj/item/restraints/legcuffs/bola/web/equipped(mob/living/user, slot)
+ . = ..()
+ if(slot == ITEM_SLOT_LEGCUFFED)
+ was_cuffed = TRUE
+ RegisterSignal(src, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(on_uncuffed))
+
+/// When uncuffed, destroy item.
+/obj/item/restraints/legcuffs/bola/web/proc/on_uncuffed(datum/source, force, atom/newloc, no_move, invdrop, silent)
+ SIGNAL_HANDLER
+ if(was_cuffed)
+ qdel(src)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/snare_webs.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/snare_webs.dm
new file mode 100644
index 00000000000000..4598d475e89b2b
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/snare_webs.dm
@@ -0,0 +1,124 @@
+// Snares on the ground! Its like a beartrap but it doesnt hurt and destroys itself after.
+/datum/power/aberrant/snare_webs
+ name = "Snare Webs"
+ desc = "Allows you to craft snares. These are placed on the ground and are hard to see; but can be disarmed.\
+ \n Mobs without the ability to walk through webs will be legcuffed if they walk through it.\
+ \n Simple mobs instead receive a slowing status effect for 8 seconds."
+ security_record_text = "Subject can craft leg snaring traps from their spider silk."
+ security_threat = POWER_THREAT_MAJOR
+ value = 3
+
+ required_powers = list(/datum/power/aberrant/web_crafter)
+
+/datum/power/aberrant/snare_webs/post_add(client/client_source)
+ ..()
+ var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action()
+ if(!action)
+ return
+ action.web_craft_entries |= /datum/web_craft_entry/web_snare
+
+/datum/power/aberrant/snare_webs/remove()
+ var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action()
+ if(!action)
+ return
+ action.web_craft_entries -= /datum/web_craft_entry/web_snare
+
+/// Gets the approrpiate web crafter action
+/datum/power/aberrant/snare_webs/proc/get_web_crafter_action()
+ if(!power_holder)
+ return null
+ for(var/datum/action/cooldown/power/aberrant/web_crafter/action in power_holder.actions)
+ return action
+ return null
+
+// Web snare (applied to legs)
+/obj/item/restraints/legcuffs/beartrap/web_snare
+ name = "web snare"
+ desc = "Sticky silk woven into a snare."
+ icon = 'icons/effects/web.dmi'
+ icon_state = "sticky_overlay"
+ armed = TRUE
+ trap_damage = 0
+ breakouttime = 8 SECONDS
+ item_flags = DROPDEL
+ /// Tracks if this was actually used as legcuffs so we can delete on uncuff only.
+ var/was_cuffed = FALSE
+
+/obj/item/restraints/legcuffs/beartrap/web_snare/update_icon_state()
+ . = ..()
+ icon_state = "sticky_overlay"
+ return .
+
+/obj/item/restraints/legcuffs/beartrap/web_snare/attack_self(mob/user)
+ return
+
+/obj/item/restraints/legcuffs/beartrap/web_snare/spring_trap(atom/movable/target, ignore_movetypes = FALSE, hit_prone = FALSE)
+ if(isliving(target) && HAS_TRAIT(target, TRAIT_WEB_SURFER))
+ return
+ return ..(target, ignore_movetypes, hit_prone)
+
+/obj/item/restraints/legcuffs/beartrap/web_snare/equipped(mob/living/user, slot)
+ ..()
+ if(slot == ITEM_SLOT_LEGCUFFED)
+ was_cuffed = TRUE
+ RegisterSignal(src, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(on_uncuffed))
+
+/// When the cuffs are removed, destroy em.
+/obj/item/restraints/legcuffs/beartrap/web_snare/proc/on_uncuffed(datum/source, force, atom/newloc, no_move, invdrop, silent)
+ SIGNAL_HANDLER
+ if(was_cuffed)
+ qdel(src)
+
+// Web snare structure (trigger on ground)
+/obj/structure/spider/web_snare
+ name = "web snare"
+ desc = "A barely visible snare woven from silk."
+ icon = 'icons/effects/web.dmi'
+ icon_state = "sticky_overlay"
+ anchored = TRUE
+ density = FALSE
+ alpha = 15
+ max_integrity = 10
+
+/obj/structure/spider/web_snare/CanAllowThrough(atom/movable/mover, border_dir)
+ . = ..()
+ if(!isliving(mover))
+ return .
+ var/mob/living/target = mover
+ if(HAS_TRAIT(target, TRAIT_WEB_SURFER))
+ return .
+ if(target.mob_size >= MOB_SIZE_HUGE) // the bigger they are the harder they don't fall.
+ qdel(src) // us humans dont care about tiny webs either.
+ return .
+ trigger_snare(target)
+ return TRUE
+
+/// When the snare is triggered.
+/obj/structure/spider/web_snare/proc/trigger_snare(mob/living/target)
+ if(!iscarbon(target))
+ trigger_snare_noncarbon(target)
+ return
+ trigger_snare_carbon(target)
+
+/// Applies the snare legtrap to the target.
+/obj/structure/spider/web_snare/proc/trigger_snare_carbon(mob/living/target)
+ var/mob/living/carbon/carbon_target = target
+ if(carbon_target.legcuffed || carbon_target.num_legs < 2) // no legs to cuff
+ qdel(src)
+ return
+ var/obj/item/restraints/legcuffs/beartrap/web_snare/snare = new /obj/item/restraints/legcuffs/beartrap/web_snare
+ carbon_target.equip_to_slot(snare, ITEM_SLOT_LEGCUFFED)
+ playsound(src, 'sound/effects/snap.ogg', 50, TRUE)
+ target.visible_message(span_danger("\The [src] ensnares [target]!"), span_userdanger("\The [src] ensnares you!"))
+ qdel(src)
+
+/// Non-carbons get a passive slowdown instead for 10sec.
+/obj/structure/spider/web_snare/proc/trigger_snare_noncarbon(mob/living/target)
+ target.add_movespeed_modifier(/datum/movespeed_modifier/web_snare, update = TRUE)
+ playsound(src, 'sound/effects/snap.ogg', 50, TRUE)
+ target.visible_message(span_danger("\The [src] ensnares [target]!"), span_userdanger("\The [src] ensnares you!"))
+ addtimer(CALLBACK(target, TYPE_PROC_REF(/mob, remove_movespeed_modifier), /datum/movespeed_modifier/web_snare), 10 SECONDS)
+ qdel(src)
+
+/datum/movespeed_modifier/web_snare
+ multiplicative_slowdown = 1
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/tripwire_webs.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/tripwire_webs.dm
new file mode 100644
index 00000000000000..df9e0699b22409
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/tripwire_webs.dm
@@ -0,0 +1,110 @@
+/datum/power/aberrant/tripwire_webs
+ name = "Tripwire Webs"
+ desc = "Allows you to place near- invisible tripwires using web crafter.\
+ \n Any creature that isn't able to safely pass webs will trigger the tripwire when they pass through it, destroying it and warning you of which wire was triggered.\
+ \n Creatures immune to resonant scrying can trigger the webs without notifying you. Extreme distances and non-movement destruction will also not notify you."
+ security_record_text = "Subject can craft tripwires from their spider silk."
+ value = 3
+
+ required_powers = list(/datum/power/aberrant/web_crafter)
+
+/datum/power/aberrant/tripwire_webs/post_add(client/client_source)
+ . = ..()
+ var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action()
+ if(!action)
+ return
+ action.web_craft_entries |= /datum/web_craft_entry/tripwire_web
+
+/datum/power/aberrant/tripwire_webs/remove()
+ var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action()
+ if(!action)
+ return
+ action.web_craft_entries -= /datum/web_craft_entry/tripwire_web
+
+/// Returns the web crafter action.
+/datum/power/aberrant/tripwire_webs/proc/get_web_crafter_action()
+ if(!power_holder)
+ return null
+ for(var/datum/action/cooldown/power/aberrant/web_crafter/action in power_holder.actions)
+ return action
+ return null
+
+/obj/structure/spider/tripwire_web
+ name = "tripwire web"
+ desc = "Nearly invisible silk stretched tight."
+ icon = 'icons/effects/navigation.dmi' // see pick_icon_state
+ icon_state = "2-5" // default shown for the webcrafting
+ anchored = TRUE
+ density = FALSE
+ alpha = 15
+ max_integrity = 1
+ layer = ABOVE_OPEN_TURF_LAYER
+ plane = FLOOR_PLANE
+ /// Who placed the tripwire, if any
+ var/datum/weakref/maker_ref
+
+/obj/structure/spider/tripwire_web/Initialize(mapload, mob/living/maker)
+ . = ..()
+ if(maker)
+ maker_ref = WEAKREF(maker)
+ pick_icon_state()
+
+/** So we don't actually have the old web sprites; a lot of web sprites are DENSE and noticeable. So we take the navigation lines and place one randomly on the tile. Boom, tripwire.
+ * We filter out the ones that start with a 0 because they're dead-ends
+**/
+/obj/structure/spider/tripwire_web/proc/pick_icon_state()
+ var/static/list/valid_states
+ if(!valid_states)
+ valid_states = list()
+ for(var/state in icon_states(icon))
+ if(copytext(state, 1, 2) == "0")
+ continue
+ valid_states += state
+ if(length(valid_states))
+ icon_state = pick(valid_states)
+
+// Basically the most reliable proc to use for passing through a space.
+/obj/structure/spider/tripwire_web/CanAllowThrough(atom/movable/mover, border_dir)
+ . = ..()
+ if(!isliving(mover))
+ return .
+ if(HAS_TRAIT(mover, TRAIT_WEB_SURFER))
+ return .
+ triggered(mover)
+ return TRUE
+
+/// When the tripwire is triggered.
+/obj/structure/spider/tripwire_web/proc/triggered(mob/living/triggerer)
+ var/mob/living/maker = maker_ref?.resolve()
+ if(!should_notify_maker(maker, triggerer))
+ qdel(src)
+ return
+ if(maker)
+ var/area/area_loc = get_area(src)
+ var/area_name = area_loc ? area_loc.name : "unknown area"
+ to_chat(maker, span_warning("Your tripwire in [area_name] was triggered!"))
+ qdel(src)
+
+/// We do not notify the maker under certain circumstances.
+/obj/structure/spider/tripwire_web/proc/should_notify_maker(mob/living/maker, mob/living/triggerer)
+ // DO WE EXIST?
+ if(!maker)
+ return FALSE
+ // They're not on the same z level (maps with multiple Zs are fine)
+ var/turf/maker_turf = get_turf(maker)
+ var/turf/web_turf = get_turf(src)
+ if(!maker_turf || !web_turf || !is_valid_z_level(maker_turf, web_turf))
+ return FALSE
+ // It was destroyed without a triggerer
+ if(!triggerer)
+ return FALSE
+ // The triggerer is immune to resonance
+ if(triggerer.can_block_resonance())
+ return FALSE
+ // The triggerer is immune to magic
+ if(triggerer.can_block_magic(MAGIC_RESISTANCE))
+ return FALSE
+ // The triggerer is immune to scrying
+ if(HAS_TRAIT(triggerer, TRAIT_ANTIRESONANCE_SCRYING))
+ return FALSE
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm
new file mode 100644
index 00000000000000..de809aa39e6589
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm
@@ -0,0 +1,177 @@
+// Web crafting! Create various doodads associated with web crafting.
+/datum/power/aberrant/web_crafter
+ name = "Web Crafter"
+ desc = "Threads of spidery silk crafted at your leisure. You gain the Web Crafting ability. You can use it to make passive webs in an area (which do not slow you down); or you can use it to make cloth.\
+ \n Creating anything using web crafter makes you hungry, and you cannot use it if you are starving.\
+ \n Double-tap to quickly create the last item you crafted."
+ mob_trait = TRAIT_WEB_SURFER // lets us walk on webs
+ security_record_text = "Subject can create spider-like silk from their body."
+ value = 3
+
+ required_powers = list(/datum/power/aberrant_root/beastial)
+ action_path = /datum/action/cooldown/power/aberrant/web_crafter
+
+/datum/action/cooldown/power/aberrant/web_crafter
+ name = "Web Crafter"
+ desc = "Spend some of your satiation to craft web-like objects! Double-tap to quickly create the last item you crafted."
+ button_icon = 'icons/effects/web.dmi'
+ button_icon_state = "webpassage"
+
+ /// Double-tap window to quick-craft the last made item.
+ var/double_tap_window = 0.8 SECONDS
+ /// World time of the last menu tap.
+ var/last_menu_tap_time = 0
+ /// Most recently crafted entry, if any.
+ var/datum/web_craft_entry/last_crafted_entry
+
+ /// Entries shown in the radial menu. Other powers can append to this.
+ /// Accepts /datum/web_craft_entry instances or typepaths of that datum.
+ var/list/web_craft_entries = list(
+ /datum/web_craft_entry/cloth,
+ /datum/web_craft_entry/stickyweb
+ )
+
+/datum/action/cooldown/power/aberrant/web_crafter/use_action(mob/living/user, atom/target)
+ var/current_time = world.time
+ var/radial_uniqueid = get_radial_uniqueid(user)
+ var/datum/radial_menu/menu = GLOB.radial_menus[radial_uniqueid]
+ // Doublet-tap interaction to quickly make last item
+ if(menu && current_time <= last_menu_tap_time + double_tap_window)
+ if(menu)
+ menu.finished = TRUE
+ if(!last_crafted_entry)
+ user.balloon_alert(user, "no recent craft!")
+ return FALSE
+ if(!can_craft_entry(user, last_crafted_entry))
+ return FALSE
+ if(!do_after(user, last_crafted_entry.craft_time, target = user))
+ return FALSE
+ // Craft the item.
+ if(!create_obj(user, last_crafted_entry))
+ return FALSE
+ last_menu_tap_time = current_time
+ return TRUE
+ else if(menu) // if you're too slow, activating the action again will just close it if the menu is open
+ menu.finished = TRUE
+ last_menu_tap_time = current_time
+ return FALSE
+
+ // stores last tap so we know if its a double-time
+ last_menu_tap_time = current_time
+ var/list/entries = get_web_craft_entries()
+ if(!length(entries))
+ user.balloon_alert(user, "no web crafts!")
+ return FALSE
+
+ var/list/key_to_entry = list()
+ var/list/radial_options = build_radial_options(entries, key_to_entry)
+ if(!length(radial_options))
+ user.balloon_alert(user, "no web crafts!")
+ return FALSE
+
+ var/picked_key = show_radial_menu(user, user, radial_options, uniqueid = radial_uniqueid, tooltips = TRUE)
+
+ if(!picked_key)
+ return FALSE
+
+ var/datum/web_craft_entry/entry = key_to_entry[picked_key]
+ if(!entry)
+ return FALSE
+
+ if(!can_craft_entry(user, entry))
+ return FALSE
+
+ // Small craft time so its a tad more defensive.
+ if(entry.craft_time > 0)
+ if(!do_after(user, entry.craft_time, target = user))
+ return FALSE
+
+ if(!create_obj(user, entry))
+ return FALSE
+ last_crafted_entry = entry
+ return TRUE
+
+/datum/action/cooldown/power/aberrant/web_crafter/on_action_success(mob/living/user, atom/target)
+ . = ..()
+ if(!HAS_TRAIT(user, TRAIT_NOHUNGER))
+ user.adjust_nutrition(-last_crafted_entry.hunger_cost)
+
+/datum/action/cooldown/power/aberrant/web_crafter/can_use(mob/living/user, atom/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ // No using when you're hungry.
+ if(!HAS_TRAIT(user, TRAIT_NOHUNGER) && user.nutrition <= NUTRITION_LEVEL_STARVING)
+ owner.balloon_alert(user, "too hungry!")
+ return FALSE
+ return TRUE
+
+/// Populates the list of web entries
+/datum/action/cooldown/power/aberrant/web_crafter/proc/get_web_craft_entries()
+ // Normalize any typepaths to instances.
+ for(var/i in 1 to length(web_craft_entries))
+ var/entry = web_craft_entries[i]
+ if(ispath(entry, /datum/web_craft_entry))
+ web_craft_entries[i] = new entry
+ return web_craft_entries
+
+/// Creates and shows the options in the radial menu.
+/datum/action/cooldown/power/aberrant/web_crafter/proc/build_radial_options(list/entries, list/key_to_entry)
+ var/list/options = list()
+ for(var/datum/web_craft_entry/entry as anything in entries)
+ if(!istype(entry))
+ continue
+ var/datum/radial_menu_choice/choice = entry.get_radial_choice()
+ if(!choice)
+ continue
+ var/key = entry.display_name
+ if(!key)
+ key = "[entry.type]"
+ var/original_key = key
+ var/dupe_index = 2
+ while(options[key])
+ key = "[original_key] ([dupe_index])"
+ dupe_index++
+ options[key] = choice
+ key_to_entry[key] = entry
+ return options
+
+/// Unique radial menu id for this action and user.
+/datum/action/cooldown/power/aberrant/web_crafter/proc/get_radial_uniqueid(mob/living/user)
+ return "web_crafter_[REF(user)]"
+
+/// Check before crafting.
+/datum/action/cooldown/power/aberrant/web_crafter/proc/can_craft_entry(mob/living/user, datum/web_craft_entry/entry)
+ // Are we hungy?
+ if(!HAS_TRAIT(user, TRAIT_NOHUNGER) && user.nutrition <= NUTRITION_LEVEL_STARVING)
+ user.balloon_alert(user, "too hungry!")
+ return FALSE
+ // Are we silenced. Yes, shooting strings from your body is resonant; you go ahead and explain how spiderman does it with your fancy psuedo-science..
+ if(HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED))
+ user.balloon_alert(user, "silenced!")
+ return FALSE
+ // We don't have the entry?
+ if(!entry)
+ return FALSE
+ // Special requirements for structure placement.
+ if(entry.is_structure)
+ if(!isturf(user.loc))
+ user.balloon_alert(user, "invalid location!")
+ return FALSE
+ var/turf/target_turf = get_turf(user)
+ if(!entry.can_place(user, target_turf))
+ return FALSE
+ return TRUE
+
+// Actually creates the item.
+/datum/action/cooldown/power/aberrant/web_crafter/proc/create_obj(mob/living/user, datum/web_craft_entry/entry)
+ if(entry.is_structure)
+ var/turf/target_turf = get_turf(user)
+ if(!target_turf)
+ return FALSE
+ entry.spawn_entry(user, target_turf)
+ return TRUE
+
+ var/obj/item/new_item = entry.spawn_entry(user, null)
+ user.put_in_hands(new_item)
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_action.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_action.dm
new file mode 100644
index 00000000000000..ab1d5d383c2591
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_action.dm
@@ -0,0 +1,62 @@
+/datum/action/cooldown/power/cultivator
+ name = "abstract cultivator power action - ahelp this"
+ background_icon_state = "bg_revenant"
+ overlay_icon_state = "bg_spell_border"
+ button_icon = 'icons/mob/actions/backgrounds.dmi'
+
+ /// The component that talks with cultivator energy. Mostly all functions here communicate with this.
+ var/datum/component/cultivator_energy/energy_component
+
+ /// The UI element displaying how much energy we have.
+ var/atom/movable/screen/cultivator_energy/cultivator_ui
+
+ /// Cost in Energy to use
+ var/cost
+ /// Bypasses the cost check while active. On_action_success still subtracts it as normal.
+ var/bypass_cost
+ /// Does this power get called by _cultivator_energy.dm when we check for aura farming? Used for potential future powers that allow you to aura farm in other ways.
+ var/contributes_to_aura_farming = FALSE
+
+
+/datum/action/cooldown/power/cultivator/Grant(mob/grant_to)
+ . = ..()
+ ValidateEnergyComponent()
+ return .
+
+/// Feng Shui / Aura farming mechanics; get stuff in the environment, increase energy based on it
+/// This function should be responsible for checking all the environmental stuff, calculating it and then returning it to the energy system.
+/datum/action/cooldown/power/cultivator/proc/aura_farm()
+ return 0
+
+/// Since Cultivator has multiple roots and a persistent resource system, we use a component for handling Energy
+/datum/action/cooldown/power/cultivator/proc/ValidateEnergyComponent()
+ if(owner) // Prevents runtiming on start
+ var/mob/living/carrier = owner
+ energy_component = carrier.GetComponent(/datum/component/cultivator_energy)
+ if(!energy_component)
+ return FALSE
+ return TRUE
+
+/// Validation handled in the energy component.
+/datum/action/cooldown/power/cultivator/proc/adjust_energy(amount, override_cap)
+ energy_component.adjust_energy(amount, override_cap)
+
+///Easy access to energy
+/datum/action/cooldown/power/cultivator/proc/get_energy()
+ return energy_component.energy
+
+// We check to see if our energy component is actually there, because usually things will go bad if they don't.
+/datum/action/cooldown/power/cultivator/try_use(mob/living/user, mob/living/target)
+ if(!ValidateEnergyComponent())
+ owner.balloon_alert(owner, "Yell at the coders; you're missing your energy system!")
+ return FALSE
+ if(energy_component.energy < cost && !bypass_cost)
+ user.balloon_alert(user, "needs [cost] energy!")
+ return FALSE
+ . = .. ()
+
+// Make sure the cost gets deducted after using the power (we already checked if we can afford it)
+/datum/action/cooldown/power/cultivator/on_action_success(mob/living/user, atom/target)
+ if(cost)
+ adjust_energy(-cost)
+ return
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm
new file mode 100644
index 00000000000000..20eaaf0f0f3411
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm
@@ -0,0 +1,220 @@
+/*
+ Because Cultivator's alignments have a consistent throughline fo behavior, the alignment powers are subtyped like so.
+ Set up to be modular and also very VV-able; because its cool if some event antag shows up in a color that nobody knows.
+*/
+/datum/action/cooldown/power/cultivator/alignment
+ name = "abstract alignment"
+
+ /// The size of the glow effect around the mob for alignment.
+ var/alignment_outline_size = 2
+ /// The overlay color for alignment, if it has one
+ var/alignment_outline_color = "#66d5dd"
+ /// The name for the filter (dont need to change this)
+ var/filter_id = "alignment_outline"
+
+ /// Light object for the alignment
+ var/obj/effect/dummy/lighting_obj/moblight/alignment_light
+ /// Sounds to play when activating alignment
+ var/alignment_activation_sound = 'sound/effects/magic/lightningbolt.ogg'
+
+ /// Mutable appearance stuff for the overlay.
+ var/mutable_appearance/alignment_overlay
+ /// Icon for the effect sprite of the alignment overlay. This is distinct from the outline, but IS affected by the outline.
+ var/alignment_overlay_icon = 'icons/effects/effects.dmi'
+ /// Icon state for the efffect sprite of the alignment overlay.
+ var/alignment_overlay_state = "lightning"
+ /// Layer on which the effect sprite sits for the alignment overlay
+ var/alignment_overlay_layer = ABOVE_MOB_LAYER
+
+ /// The armor datum given when in alignment. You SHOULD modify this if you want to change the armor type.
+ var/datum/armor/alignment_defense = /datum/armor/alignment_unarmored_defense
+ /// The armor datum we actually add after comparing current armor against alignment_defense. You should NOT need to modify this.
+ var/datum/armor/alignment_added_armor
+ /// The damage type for the alignment
+ var/alignment_damage_type = BRUTE
+ /// The bonus damage for the alignment
+ var/alignment_damage_bonus = CULTIVATOR_ALIGNMENT_DAMAGE_BONUS
+ /// The upkeep cost of the alignment
+ var/alignment_upkeep_cost = CULTIVATOR_ALIGNMENT_UPKEEP_COST
+
+ cooldown_time = 5 // to prevent spam-clicking it off
+ contributes_to_aura_farming = TRUE // needs to be always be on or you won't get aura from alignment
+ cost = CULTIVATOR_ALIGNMENT_ACTIVATION_COST
+
+// Removes stray listeners.
+/datum/action/cooldown/power/cultivator/alignment/Destroy()
+ . = ..()
+ if(owner)
+ UnregisterSignal(owner, list(COMSIG_HUMAN_UNARMED_HIT, COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM, COMSIG_ATOM_DISPEL))
+ remove_alignment_armor()
+
+/// The proc for onhit. Override as desired.
+/datum/action/cooldown/power/cultivator/alignment/proc/on_unarmed_hit(mob/living/user, mob/living/target, obj/item/bodypart/affecting, damage, armor_block, limb_sharpness)
+ SIGNAL_HANDLER
+ if(!active)
+ return
+ if(alignment_damage_bonus)
+ apply_damage_with_armor(target, alignment_damage_bonus, alignment_damage_type, affecting, armor_block, attack_flag = MELEE)
+
+// Basically handles active state and activation fx. Override as needed; but please make sure to get the essentials.
+/datum/action/cooldown/power/cultivator/alignment/use_action(mob/living/carbon/user)
+ if(!active) // If inactive, we activate (if we can pay the cost)
+ enable_alignment(user)
+ return TRUE
+ if(active) // If active, we disable.
+ disable_alignment(user)
+ return TRUE
+ return FALSE
+
+/// COOL effects to show your AURA.
+/datum/action/cooldown/power/cultivator/alignment/proc/activation_fx(mob/living/carbon/user, atom/target)
+ if(isnull(alignment_outline_color) && isnull(alignment_outline_size))
+ return
+ // Adds the color effects
+ user.remove_filter(filter_id)
+ user.add_filter(filter_id, 2, outline_filter(size = alignment_outline_size, color = alignment_outline_color))
+
+ var/filter = user.get_filter(filter_id)
+ if(filter)
+ animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1)
+ animate(alpha = 40, time = 2.5 SECONDS)
+
+ // Adds the glowing light.
+ QDEL_NULL(alignment_light)
+ alignment_light = user.mob_light(
+ range = 3,
+ power = 1,
+ color = alignment_outline_color
+ )
+ // adds overlay
+ if(!alignment_overlay)
+ alignment_overlay = mutable_appearance(alignment_overlay_icon, alignment_overlay_state, alignment_overlay_layer)
+ alignment_overlay.color = alignment_outline_color
+ user.add_overlay(alignment_overlay)
+
+ // plays sound
+ playsound(owner, alignment_activation_sound, 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+
+/// Everything that needs to happen when enabling alignment
+/datum/action/cooldown/power/cultivator/alignment/proc/enable_alignment(mob/living/carbon/user)
+ active = TRUE
+ bypass_cost = TRUE // makes it so we don't check for cost next time.
+ activation_fx(user)
+ RegisterSignal(user, COMSIG_HUMAN_UNARMED_HIT, PROC_REF(on_unarmed_hit))
+ RegisterSignal(user, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_equipment_changed))
+ RegisterSignal(user, COMSIG_MOB_UNEQUIPPED_ITEM, PROC_REF(on_equipment_changed))
+ RegisterSignal(user, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+ recompute_alignment_armor(user)
+ SEND_SIGNAL(user, COMSIG_CULTIVATOR_ALIGNMENT_ENABLED, src)
+ return TRUE
+
+/// Everything that needs to happen when disabling alignment
+/datum/action/cooldown/power/cultivator/alignment/proc/disable_alignment(mob/living/carbon/user)
+ active = FALSE
+ bypass_cost = FALSE
+ if(alignment_overlay)
+ user.cut_overlay(alignment_overlay)
+ QDEL_NULL(alignment_overlay)
+ UnregisterSignal(user, list(COMSIG_HUMAN_UNARMED_HIT, COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM, COMSIG_ATOM_DISPEL))
+ user.remove_filter(filter_id)
+ remove_alignment_armor()
+ QDEL_NULL(alignment_light)
+ SEND_SIGNAL(user, COMSIG_CULTIVATOR_ALIGNMENT_DISABLED, src)
+ return TRUE
+
+/// Dispel handler: drains Energy if alignment is active.
+/// For balance reasons this should not end on dispel; it is really an all eggs in one basket power.
+/datum/action/cooldown/power/cultivator/alignment/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+ if(!active)
+ return NONE
+ if(ValidateEnergyComponent())
+ adjust_energy(-CULTIVATOR_ENERGY_MODERATE)
+ return DISPEL_RESULT_DISPELLED
+
+// Deactivating the power doesn't cost anything so we skip the cost component.
+/datum/action/cooldown/power/cultivator/alignment/on_action_success(mob/living/carbon/user)
+ if(!active)
+ return
+ else
+ . = ..()
+
+/*
+ Below is the big scary block of 'how to maths out other armor'
+ Because we want armor to never EXCEED the alignment due to stacking armor items with alignment, we need to COMPARE it against the current armor and change the armor values on the fly.
+ This is called when either A. you activate the alignment or B. equip/unequip stuff.
+ 'Other armor' as a type applies globally to all slots, so wearing a really good helmet/chest will still disable the alignment damage bonus for other slots that may be uncovered.
+*/
+
+/// Whenever we change anything about our loadout, recompute.
+/datum/action/cooldown/power/cultivator/alignment/proc/on_equipment_changed(datum/source, obj/item/item, slot)
+ SIGNAL_HANDLER
+ if(!active)
+ return
+ recompute_alignment_armor(source)
+
+/// The builder that actually applies the maths from calc_needed_internal_armor and applies it.
+/datum/action/cooldown/power/cultivator/alignment/proc/recompute_alignment_armor(mob/living/carbon/user)
+ if(!ishuman(user))
+ return
+ var/mob/living/carbon/human/human_user = user
+
+ remove_alignment_armor()
+
+ var/datum/armor/target_armor = alignment_defense
+ if(ispath(target_armor))
+ target_armor = get_armor_by_type(target_armor)
+
+ var/list/add_values = list()
+ for(var/armor_type in ARMOR_LIST_ALL())
+ var/target_total = target_armor.get_rating(armor_type)
+ var/needed = calc_needed_internal_armor(human_user, armor_type, target_total)
+ if(needed > 0)
+ add_values[armor_type] = needed
+
+ if(LAZYLEN(add_values))
+ var/datum/armor/base_armor = new /datum/armor
+ alignment_added_armor = base_armor.generate_new_with_specific(add_values)
+ human_user.physiology.armor = human_user.physiology.armor.add_other_armor(alignment_added_armor)
+
+/// Compares the user's current worn armor against the armor from alignment_defense and returns the difference, to ensure we don't stack alignment armor past 40 armor.
+/datum/action/cooldown/power/cultivator/alignment/proc/calc_needed_internal_armor(mob/living/carbon/human/human_target, armor_type, target_total)
+ var/list/covering_clothing = list(
+ human_target.head, human_target.wear_mask, human_target.wear_suit, human_target.w_uniform, human_target.back, human_target.gloves, human_target.shoes, human_target.belt, human_target.s_store, human_target.glasses, human_target.ears, human_target.wear_id, human_target.wear_neck)
+
+ var/clothing_multiplier = 1.0
+ for(var/obj/item/clothing/clothing_item in covering_clothing)
+ if(!clothing_item)
+ continue
+ var/clothing_rating = min(clothing_item.get_armor_rating(armor_type), 100)
+ clothing_multiplier *= (100 - clothing_rating) * 0.01
+
+ var/current_internal = human_target.physiology.armor.get_rating(armor_type)
+ var/current_total = human_target.getarmor(null, armor_type)
+ if(current_total >= target_total)
+ return 0
+
+ var/required_internal = 100 * (1 - (1 - target_total / 100) / max(clothing_multiplier, 0.0001))
+ return max(0, required_internal - current_internal)
+
+/// Removes the lingering effects of the alignment armor.
+/datum/action/cooldown/power/cultivator/alignment/proc/remove_alignment_armor()
+ if(!alignment_added_armor || !ishuman(owner))
+ alignment_added_armor = null
+ return
+ var/mob/living/carbon/human/human_owner = owner
+ human_owner.physiology.armor = human_owner.physiology.armor.subtract_other_armor(alignment_added_armor)
+ alignment_added_armor = null
+
+// base armor for alignment powers.
+/datum/armor/alignment_unarmored_defense
+ acid = 40
+ bio = 40
+ melee = 40
+ bullet = 40
+ bomb = 40
+ energy = 40
+ laser = 40
+ fire = 40
+ melee = 40
+ wound = 40
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_energy.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_energy.dm
new file mode 100644
index 00000000000000..8b63d68f629f76
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_energy.dm
@@ -0,0 +1,122 @@
+/// Helper to format the text that gets thrown onto the energy hud element.
+#define FORMAT_ENERGY_TEXT(charges) MAPTEXT("
[floor(charges)]
")
+
+/datum/component/cultivator_energy
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+
+ /// The mob we’re attached to is always parent.
+ var/mob/living/attached_mob
+
+ /// Current Energy
+ var/energy = 0
+ /// Max energy you can store
+ var/max_energy = CULTIVATOR_ENERGY_MAX
+ /// The UI itself
+ var/atom/movable/screen/cultivator_energy/cultivator_ui
+
+ /// Minimum Energy gained from the Aura mechanic
+ var/aura_min = CULTIVATOR_MIN_CULTIVATION_BONUS
+ /// Maximum Energy gained from the Aura mechanic
+ var/aura_max = CULTIVATOR_MAX_CULTIVATION_BONUS
+
+/datum/component/cultivator_energy/Initialize()
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ attached_mob = parent
+ RegisterWithParent()
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/component/cultivator_energy/RegisterWithParent()
+ . = ..()
+ if(attached_mob.hud_used)
+ install_energy_hud(parent)
+ else
+ RegisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created))
+
+/datum/component/cultivator_energy/UnregisterFromParent()
+ . = ..()
+ if(attached_mob) // prevents runtiming when adding/removing duplicate components
+ UnregisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED)
+
+/datum/component/cultivator_energy/Destroy()
+ UnregisterFromParent()
+ STOP_PROCESSING(SSfastprocess, src)
+
+ if(!attached_mob)
+ return
+
+ if(attached_mob.hud_used && cultivator_ui)
+ attached_mob.hud_used.infodisplay -= cultivator_ui
+ qdel(cultivator_ui)
+ cultivator_ui = null
+
+ attached_mob = null
+ return ..()
+
+// Processing is responsible for most of the aura farming / 'passive energy gain'.
+/datum/component/cultivator_energy/process(seconds_per_tick)
+ if(!attached_mob)
+ return
+
+ // Handles upkeep for alignment powers.
+ for(var/datum/action/cooldown/power/cultivator/alignment/power in attached_mob.actions)
+ if(power.active)
+ adjust_energy(-(power.alignment_upkeep_cost * seconds_per_tick))
+ if(energy <= 0) // disable if we're out of energy
+ to_chat(attached_mob, span_boldwarning("You've ran out of Energy!"))
+ power.disable_alignment(attached_mob)
+
+ // Aura farming code below
+ if(HAS_TRAIT(attached_mob, TRAIT_RESONANCE_SILENCED)) // no aura farming when silenced
+ return
+ // Just for the sake of future proofing, you can have multiple sources of aura farming.
+ var/total = 0
+ for(var/datum/action/cooldown/power/cultivator/power in attached_mob.actions)
+ if(power.contributes_to_aura_farming && !power.active) // needs to have the contributing flag and not be active
+ total += power.aura_farm()
+
+ total = clamp(total, aura_min, aura_max)
+ total *= seconds_per_tick // I love spess game time-based maths
+
+ adjust_energy(total)
+
+/// Waits for the HUD to load before installing the UI element
+/datum/component/cultivator_energy/proc/on_hud_created(datum/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/living_holder = attached_mob
+ if(!living_holder || !living_holder.hud_used)
+ return
+
+ install_energy_hud(living_holder)
+
+/// Places the energy HUD on the player's UI
+/datum/component/cultivator_energy/proc/install_energy_hud(mob/living/living_holder)
+ if(cultivator_ui) // already installed
+ return
+
+ var/datum/hud/hud_used = living_holder.hud_used
+ cultivator_ui = new /atom/movable/screen/cultivator_energy(null, hud_used)
+ hud_used.infodisplay += cultivator_ui
+
+ // Set initial text so it isn't blank until first adjust.
+ cultivator_ui.maptext = FORMAT_ENERGY_TEXT(energy)
+
+ hud_used.show_hud(hud_used.hud_version)
+
+/// Changes how much energy we have within the confines of our limits, unless overriden.
+/datum/component/cultivator_energy/proc/adjust_energy(amount, override_cap)
+ if(!isnum(amount))
+ return
+ var/cap_to = isnum(override_cap) ? override_cap : max_energy
+ energy = clamp(energy + amount, 0, cap_to)
+
+ cultivator_ui?.maptext = FORMAT_ENERGY_TEXT(energy)
+
+// UI Elements for energy
+/atom/movable/screen/cultivator_energy
+ name = "energy"
+ icon = 'icons/hud/blob.dmi' // TODO: Get sprites/UI for this.
+ icon_state = "block"
+ screen_loc = CULTIVATOR_UI_SCREEN_LOC
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_power.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_power.dm
new file mode 100644
index 00000000000000..1b6b67d3de7011
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_power.dm
@@ -0,0 +1,8 @@
+/datum/power/cultivator
+ name = "Cultivator Power"
+ desc = "You're not meant to look at the roots in the ground; you're meant to look at the tree that sprouts from it. Stop staring at abstract types!"
+
+ archetype = POWER_ARCHETYPE_RESONANT
+ path = POWER_PATH_CULTIVATOR
+ priority = POWER_PRIORITY_BASIC
+ abstract_parent_type = /datum/power/cultivator
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm
new file mode 100644
index 00000000000000..31d96cc17000d5
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm
@@ -0,0 +1,41 @@
+/datum/power/cultivator_root
+ name = "Abstract cultivator root"
+ desc = "For decades, I have honed my body, my skill. Like calligraphists have mastered the stroke of a brush, I have mastered the brush of my hand along the keyboard. \
+ Lines upon lines of code created at but the single flick of the wrist. So know what is good with you; and report this abstract root."
+ abstract_parent_type = /datum/power/cultivator_root
+
+ archetype = POWER_ARCHETYPE_RESONANT
+ path = POWER_PATH_CULTIVATOR
+ priority = POWER_PRIORITY_ROOT
+
+/datum/power/cultivator_root/post_add() // I'd love to run this during add but that runtimes at round start.
+ if(!power_holder) // So it doesn't runtime at init
+ return
+ // We pass along the piety component to actually handle most of the piety stuff.
+ power_holder.AddComponent(/datum/component/cultivator_energy, power_holder)
+ // Passes along meditation.
+ var/has_meditate = FALSE
+ for(var/datum/action/action as anything in power_holder.actions)
+ if(istype(action, /datum/action/cooldown/power/resonant_meditate))
+ has_meditate = TRUE
+ break
+ if(!has_meditate)
+ grant_action(/datum/action/cooldown/power/resonant_meditate)
+ . = ..()
+
+/datum/power/cultivator_root/remove()
+ . = ..()
+ var/mob/living/holder = power_holder
+ if(!holder)
+ return
+
+ // We check for other roots of our type, in the event that admin shenanigangs gave multiple roots. Don't want to throw out the whole component when other things are still in use.
+ var/has_other_root = FALSE
+ for(var/datum/power/power as anything in holder.powers)
+ if(istype(power, /datum/power/cultivator_root))
+ has_other_root = TRUE
+ break
+
+ if(!has_other_root)
+ var/tobedel = holder.GetComponent(/datum/component/cultivator_energy)
+ QDEL_NULL(tobedel)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm
new file mode 100644
index 00000000000000..56eb0acff17a52
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm
@@ -0,0 +1,93 @@
+/datum/power/cultivator_root/astral_touched
+ name = "Astral Touched Alignment"
+ desc = "You gain Energy through Aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do extra burn damage.\
+ \nPassively, your cold temprature tolerance is increased by 40C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\
+ \nYou gain armor IV across your whole body. Has diminishing effects with your worn armor."
+ security_record_text = "Subject is capable of entering a heightened state by observing space, granting them resistance to damage, deadlier punches and the ability to ignore cold tempratures and low pressure."
+ security_threat = POWER_THREAT_MAJOR
+ action_path = /datum/action/cooldown/power/cultivator/alignment/astral_touched
+
+ value = 6
+
+ /// bonus to cold tolerance
+ var/cold_tolerance_bonus = 40
+
+// Gives innate resistance to cold.
+/datum/power/cultivator_root/astral_touched/post_add()
+ . = ..()
+ if(!iscarbon(power_holder))
+ return
+ var/mob/living/carbon/owner = power_holder
+ owner.dna.species.bodytemp_cold_damage_limit -= cold_tolerance_bonus
+
+/datum/power/cultivator_root/astral_touched/remove()
+ . = ..()
+ if(!iscarbon(power_holder))
+ return
+ var/mob/living/carbon/owner = power_holder
+ owner.dna.species.bodytemp_cold_damage_limit += cold_tolerance_bonus
+
+/datum/action/cooldown/power/cultivator/alignment/astral_touched
+ name = "Astral Touched Alignment"
+ desc = "Activates your Astral Touched Alignment aura, granting you immune to cold and pressure, increasing your defenses (if unarmored), and increasing your strength with unarmed attacks."
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "teleport"
+
+ alignment_outline_color = "#c1effa"
+ alignment_activation_sound = 'sound/effects/magic/cosmic_energy.ogg'
+ alignment_overlay_state = "shieldsparkles"
+
+ alignment_damage_type = BURN
+
+// Adds pressure immunity & cold immunity.
+/datum/action/cooldown/power/cultivator/alignment/astral_touched/enable_alignment(mob/living/carbon/user)
+ . = ..()
+ user.add_traits(list(TRAIT_RESISTLOWPRESSURE, TRAIT_RESISTCOLD), src)
+
+/datum/action/cooldown/power/cultivator/alignment/astral_touched/disable_alignment(mob/living/carbon/user)
+ . = ..()
+ user.remove_traits(list(TRAIT_RESISTLOWPRESSURE, TRAIT_RESISTCOLD), src)
+
+/datum/action/cooldown/power/cultivator/alignment/astral_touched/aura_farm()
+ var/total = 0
+ var/mob/living/owner_mob = owner
+ if(!owner_mob)
+ return total
+
+ var/space_value = CULTIVATOR_AURA_FARM_MINOR * 0.6 // the real thing
+ var/glass_value = CULTIVATOR_AURA_FARM_MINOR * 0.3 // not as cool but its something
+ var/fake_space_value = CULTIVATOR_AURA_FARM_MINOR * 0.4 // looks pretty real.
+ var/space_cube_value = CULTIVATOR_AURA_FARM_MINOR * 0.5 // Praise the space cube.
+ var/in_space_value = CULTIVATOR_AURA_FARM_MAJOR // Being out in space basically guarantees 50% charge.
+ var/voidwalker_value = CULTIVATOR_AURA_FARM_MAJOR // They are void space people-things. Could make for great roleplay.
+
+ // Do we see space turfs?
+ for(var/turf/T in view(owner_mob))
+ if(istype(T, /turf/open/space))
+ total += space_value
+ continue
+ if(istype(T, /turf/open/floor/glass)) // Note, we check if you can see space on the z-level below. If you can or there's no z-level you get the space bonus.
+ var/turf/below = locate(T.x, T.y, T.z - 1)
+ if(!below || istype(below, /turf/open/space))
+ total += glass_value
+ continue
+ if(istype(T, /turf/open/floor/fakespace))
+ total += fake_space_value
+ continue
+
+ // PRAISE THE SPACE CUBE. IT HAS SPACE ON IT - THAT COUNTS!
+ for(var/obj/structure/sign/poster/contraband/space_cube/cube in view(owner_mob))
+ total += space_cube_value
+ for(var/obj/item/dice/d6/space/cube in view(owner_mob))
+ total += space_cube_value
+
+ // Are we in space?
+ var/turf/owner_turf = get_turf(owner_mob)
+ if(istype(owner_turf, /turf/open/space))
+ total += in_space_value
+
+ // They're made out of space, they're cool.
+ for(var/mob/living/basic/voidwalker/voidman in view(owner_mob))
+ total += voidwalker_value
+
+ return total
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm
new file mode 100644
index 00000000000000..d78655cb39499f
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm
@@ -0,0 +1,137 @@
+/datum/power/cultivator/energy_dash
+ name = "Energy Dash"
+ desc = "While in Alignment, you can dash forth at extreme speeds. Choose a space that can be reached by walking (even if it requires reasonable detours). You immediately dash there and arrive near-instantly. Costs Dantian to use."
+ security_record_text = "Subject can dash at extreme speeds while in their heightened state."
+ security_threat = POWER_THREAT_MAJOR
+ value = 4
+ required_powers = list(/datum/power/cultivator_root)
+ required_allow_subtypes = TRUE
+ action_path = /datum/action/cooldown/power/cultivator/energy_dash
+
+/datum/action/cooldown/power/cultivator/energy_dash
+ name = "Energy Dash"
+ desc = "While in Alignment, choose a space to dash to at extreme speeds, so long as you can reach the location by walking. Costs Dantian to use."
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "blink"
+ cost = 25
+ target_type = /turf
+ click_to_activate = TRUE
+
+ /// How much seconds inbetween steps. Lower the number = the faster we go.
+ var/dash_step_delay = 0.05 SECONDS
+ /// Max amount of distance from our starting point that we can START a dash towards
+ var/dash_max_distance = 30
+ /// Max amounts of steps we can take WHILST a dash is happening
+ var/dash_max_steps = 50
+
+// Extra movement gating.
+/datum/action/cooldown/power/cultivator/energy_dash/can_use(mob/living/user, atom/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ // We can't dash if we're already dashing.
+ if(active)
+ user.balloon_alert(user, "already dashing!")
+ return FALSE
+ // We can't dash if we're on our ass
+ if(user.IsKnockdown())
+ owner.balloon_alert(user, "knocked down!")
+ return FALSE
+ // We can't dash if we're immobilized
+ if(HAS_TRAIT(user, TRAIT_IMMOBILIZED))
+ owner.balloon_alert(user, "immobilized!")
+ return FALSE
+ // We can't dash if we're legcuffed.
+ if(iscarbon(user))
+ var/mob/living/carbon/carbon_user = user
+ if(carbon_user.legcuffed)
+ owner.balloon_alert(user, "legcuffed!")
+ return FALSE
+ return TRUE
+
+// Dash to the clicked location using pathfinding.
+/datum/action/cooldown/power/cultivator/energy_dash/use_action(mob/living/user, atom/target)
+ if(!target)
+ return FALSE
+ // check & store alignment
+ var/datum/action/cooldown/power/cultivator/alignment/alignment_action = get_alignment_action(user)
+ if(!alignment_action || !alignment_action.active)
+ user.balloon_alert(user, "alignment required!")
+ return FALSE
+
+ // Gets our current location & target turf and checks if its a valid space.
+ var/turf/user_turf = get_turf(user)
+ var/turf/target_turf = get_turf(target)
+ if(!user_turf || !target_turf)
+ return FALSE
+ if(!is_valid_destination(user, target_turf))
+ user.balloon_alert(user, "invalid destination!")
+ return FALSE
+
+ // Pathfinds the destination
+ var/list/path = get_path_to(user, target_turf, max_distance = dash_max_distance, mintargetdist = 0, access = user.get_access(), simulated_only = !HAS_TRAIT(user, TRAIT_SPACEWALK), skip_first = TRUE)
+ if(!length(path))
+ user.balloon_alert(user, "no clear path!")
+ return FALSE
+ if(path[length(path)] != target_turf)
+ path += target_turf
+
+ // we start dashing!
+ active = TRUE
+ INVOKE_ASYNC(src, PROC_REF(dash_along_path), user, path, alignment_action.alignment_outline_color)
+ return TRUE
+
+/// Moves us along our pre-determined path.
+/datum/action/cooldown/power/cultivator/energy_dash/proc/dash_along_path(mob/living/user, list/path, alignment_color)
+ ADD_TRAIT(user, TRAIT_IMMOBILIZED, src) // we don't want em moving.
+ var/steps = 0
+ // for loop that creates afterimages, moves us to the next space and repeats til we're at our destination.
+ for(var/turf/next_turf as anything in path)
+ if(steps >= dash_max_steps)
+ break
+ if(QDELETED(user) || user.stat >= DEAD)
+ break
+ var/dir_to_next = get_dir(user, next_turf)
+ new /obj/effect/temp_visual/energy_dash_afterimage(user.loc, dir_to_next, alignment_color)
+ var/atom/old_loc = user.loc
+ user.Move(next_turf, get_dir(user, next_turf), FALSE, TRUE)
+ if(old_loc == user.loc)
+ break
+ steps++
+ SLEEP_CHECK_DEATH(dash_step_delay, user)
+ REMOVE_TRAIT(user, TRAIT_IMMOBILIZED, src)
+ active = FALSE
+
+/// Validates we can land on the destination turf.
+/datum/action/cooldown/power/cultivator/energy_dash/proc/is_valid_destination(mob/living/user, turf/target_turf)
+ if(!target_turf || !isopenturf(target_turf))
+ return FALSE
+ return TRUE
+
+/// Returns an active cultivator alignment action, or the first one found.
+/datum/action/cooldown/power/cultivator/energy_dash/proc/get_alignment_action(mob/living/user)
+ if(!user)
+ return null
+ var/datum/action/cooldown/power/cultivator/alignment/first_alignment
+ for(var/datum/action/cooldown/power/cultivator/alignment/alignment_action in user.actions)
+ if(!first_alignment)
+ first_alignment = alignment_action
+ if(alignment_action.active)
+ return alignment_action
+ return first_alignment
+
+/obj/effect/temp_visual/energy_dash_afterimage
+ name = "afterimage"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "blank_white"
+ duration = 5
+ randomdir = FALSE
+
+// colors the afterimage to match the alignment
+/obj/effect/temp_visual/energy_dash_afterimage/Initialize(mapload, dir_override, alignment_color)
+ . = ..()
+ if(dir_override)
+ setDir(dir_override)
+ if(alignment_color)
+ add_atom_colour(alignment_color, FIXED_COLOUR_PRIORITY)
+ animate(src, alpha = 0, time = duration)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm
new file mode 100644
index 00000000000000..0322fe23c09e0b
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm
@@ -0,0 +1,184 @@
+/datum/power/cultivator_root/flame_soul
+ name = "Flame Soul Alignment"
+ desc = "You gain Energy through Aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do extra burn damage.\
+ \nPassively, your high temprature threshold is increased by 60C regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\
+ \nYou gain armor III (with laser VI and fire X) across your whole body. Has diminishing effects with your worn armor."
+ security_record_text = "Subject is capable of entering a heightened state by observing fires, granting them resistance to damage (especially lasers & fire), deadlier punches and the ability to ignore hot tempratures and fire."
+ security_threat = POWER_THREAT_MAJOR
+ action_path = /datum/action/cooldown/power/cultivator/alignment/flame_soul
+ value = 6
+
+ /// bonus to heat tolerance
+ var/heat_tolerance_bonus = 60
+
+// Gives innate resistance to heat.
+/datum/power/cultivator_root/flame_soul/post_add()
+ . = ..()
+ if(!iscarbon(power_holder))
+ return
+ var/mob/living/carbon/owner = power_holder
+ owner.dna.species.bodytemp_heat_damage_limit += heat_tolerance_bonus
+
+/datum/power/cultivator_root/flame_soul/remove()
+ . = ..()
+ if(!iscarbon(power_holder))
+ return
+ var/mob/living/carbon/owner = power_holder
+ owner.dna.species.bodytemp_heat_damage_limit -= heat_tolerance_bonus
+
+/datum/action/cooldown/power/cultivator/alignment/flame_soul
+ name = "Flame Soul Alignment"
+ desc = "Activates your Astral-Touched Alignment aura, granting you immunity to fire, increasing your defenses (if unarmored), and increasing your strength with unarmed attacks."
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "sacredflame"
+
+ alignment_outline_color = "#e99a3f"
+ alignment_activation_sound = 'sound/effects/magic/fireball.ogg'
+ alignment_overlay_icon = 'icons/effects/eldritch.dmi'
+ alignment_overlay_state = "ring_leader_effect"
+ alignment_overlay_layer = LOW_MOB_LAYER
+
+ alignment_damage_type = BURN
+ alignment_defense = /datum/armor/alignment_flamesoul_defense
+
+// Adds pressure immunity & cold immunity.
+/datum/action/cooldown/power/cultivator/alignment/flame_soul/enable_alignment(mob/living/carbon/user)
+ . = ..()
+ user.add_traits(list(TRAIT_RESISTHEAT, TRAIT_NOFIRE), src)
+
+/datum/action/cooldown/power/cultivator/alignment/flame_soul/disable_alignment(mob/living/carbon/user)
+ . = ..()
+ user.remove_traits(list(TRAIT_RESISTHEAT, TRAIT_NOFIRE), src)
+
+
+// special laser & fire proofed armor for flamesoul.
+/datum/armor/alignment_flamesoul_defense
+ acid = 30
+ bio = 30
+ melee = 30
+ bullet = 30
+ bomb = 30
+ energy = 30
+ laser = 60
+ fire = 100
+ melee = 30
+ wound = 30
+
+
+/datum/action/cooldown/power/cultivator/alignment/flame_soul/aura_farm()
+ var/total = 0
+ var/mob/living/owner_mob = owner
+ if(!owner_mob)
+ return total
+
+ var/object_on_fire_value = CULTIVATOR_AURA_FARM_MINOR * 0.3 // stuff that is on fire that shouldnt be e.g a paper stack
+ var/natural_fire_object_value = CULTIVATOR_AURA_FARM_MINOR * 0.5 // exposed flames that are intended e.g candles
+ var/big_natural_fire_object_value = CULTIVATOR_AURA_FARM_MODERATE // exposed flames that are intended and big e.g bonfires
+ var/fire_turf_value = CULTIVATOR_AURA_FARM_MINOR // turfs being on fire e.g plasma fire
+ var/smoking_value = CULTIVATOR_AURA_FARM_MODERATE // smoking is cool and good for aura.
+ var/lava_value = CULTIVATOR_AURA_FARM_MINOR // lava turfs: hot shit.
+
+ var/others_on_fire_value = CULTIVATOR_AURA_FARM_MODERATE // someone else is on fire
+ var/user_on_fire_value = CULTIVATOR_AURA_FARM_MAJOR // we're on fire
+
+ // Big ol list of objects that are meant to be onfire.
+ var/static/list/natural_fire_typecache = typecacheof(list(
+ /obj/item/flashlight/flare,
+ /obj/item/flashlight/flare/candle,
+ /obj/item/match,
+ /obj/item/lighter,
+ /obj/item/oxygen_candle,
+ /obj/item/sparkler,
+ /obj/structure/wall_torch
+ ))
+
+
+ // Big ol list of big objects that are meant to be on fire.
+ var/static/list/big_natural_fire_typecache = typecacheof(list(
+ /obj/structure/bonfire,
+ /obj/structure/fireplace,
+ /obj/structure/firepit
+ ))
+
+ // Checks for hotspots aka is the engine on fire and does that let us aura farm? Also checks for lava
+ for(var/turf/open/open_turf in view(owner_mob))
+ if(istype(open_turf, /turf/open/lava))
+ total += lava_value
+ if(open_turf.active_hotspot)
+ total += fire_turf_value
+
+ // Check if there is anyone on fire nearby.
+ for(var/mob/living/burning_mob in view(owner_mob))
+ if(burning_mob == owner_mob) // we check this separetely.
+ continue
+ if(burning_mob.on_fire)
+ total += others_on_fire_value
+
+ // Check if we are on fire.
+ if(owner_mob.on_fire)
+ total += user_on_fire_value
+
+ // Check if we are smoking something in our mask slot.
+ var/obj/item/mask_item = owner_mob.get_item_by_slot(ITEM_SLOT_MASK)
+ if(istype(mask_item, /obj/item/cigarette))
+ var/obj/item/cigarette/smoking_item = mask_item
+ if(smoking_item.lit)
+ total += smoking_value
+
+ // Goes through all the objects in view.
+ for(var/obj/scene_object in view(owner_mob))
+ // List that goes through all the big items and checks if they are on fire.
+ if(is_type_in_typecache(scene_object, big_natural_fire_typecache))
+ if(istype(scene_object, /obj/structure/bonfire))
+ var/obj/structure/bonfire/bonfire_object = scene_object
+ if(bonfire_object.burning)
+ total += big_natural_fire_object_value
+ continue
+ if(istype(scene_object, /obj/structure/fireplace))
+ var/obj/structure/fireplace/fireplace_object = scene_object
+ if(fireplace_object.lit)
+ total += big_natural_fire_object_value
+ continue
+ if(istype(scene_object, /obj/structure/firepit))
+ var/obj/structure/firepit/firepit_object = scene_object
+ if(firepit_object.active)
+ total += big_natural_fire_object_value
+ continue
+ // List that goes through all the smaller scene objects and check if they are on fire.
+ if(is_type_in_typecache(scene_object, natural_fire_typecache))
+ if(istype(scene_object, /obj/item/flashlight/flare))
+ var/obj/item/flashlight/flare/flare_object = scene_object
+ if(flare_object.light_on)
+ total += natural_fire_object_value
+ continue
+ if(istype(scene_object, /obj/structure/wall_torch))
+ var/obj/structure/wall_torch/wall_torch_object = scene_object
+ if(wall_torch_object.burning)
+ total += natural_fire_object_value
+ continue
+ if(istype(scene_object, /obj/item/oxygen_candle))
+ var/obj/item/oxygen_candle/oxygen_candle_object = scene_object
+ if(oxygen_candle_object.processing)
+ total += natural_fire_object_value
+ continue
+ if(istype(scene_object, /obj/item/match))
+ var/obj/item/match/match_object = scene_object
+ if(match_object.lit)
+ total += natural_fire_object_value
+ continue
+ if(istype(scene_object, /obj/item/lighter))
+ var/obj/item/lighter/lighter_object = scene_object
+ if(lighter_object.lit)
+ total += natural_fire_object_value
+ continue
+ if(istype(scene_object, /obj/item/sparkler))
+ var/obj/item/sparkler/sparkler_object = scene_object
+ if(sparkler_object.lit)
+ total += natural_fire_object_value
+ continue
+
+ // Checks if the item is on fire when its nto meant to be on fire.
+ if(scene_object.resistance_flags & ON_FIRE)
+ total += object_on_fire_value
+
+ return total
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/fly_like_a_shooting_star.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/fly_like_a_shooting_star.dm
new file mode 100644
index 00000000000000..b496e0ff0df226
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/fly_like_a_shooting_star.dm
@@ -0,0 +1,59 @@
+/datum/power/cultivator/fly_like_a_shooting_star
+ name = "Fly Like A Shooting Star"
+ desc = "Whilst your alignment is active, you can fly. You can propel yourself through the air and space as if wearing a jetpack. \
+ If you aren't able to use your legs, you're able to move around with this ability, regardless of the current gravity."
+ security_record_text = "Subject can fly regardless of gravitational environment whilst in their heightened state."
+ value = 3
+ required_powers = list(/datum/power/cultivator_root/astral_touched)
+
+ /// the trailing particles
+ var/datum/effect_system/trail_follow/ion/grav_allowed/flight_trail
+ /// ref to the root power's action
+ var/datum/action/cooldown/power/cultivator/alignment/astral_touched/astral_alignment
+
+/datum/power/cultivator/fly_like_a_shooting_star/add(client/client_source)
+ . = ..()
+ if(!power_holder)
+ return
+ RegisterSignal(power_holder, COMSIG_CULTIVATOR_ALIGNMENT_ENABLED, PROC_REF(on_alignment_enabled))
+ RegisterSignal(power_holder, COMSIG_CULTIVATOR_ALIGNMENT_DISABLED, PROC_REF(on_alignment_disabled))
+
+/datum/power/cultivator/fly_like_a_shooting_star/remove()
+ if(power_holder)
+ UnregisterSignal(power_holder, list(COMSIG_CULTIVATOR_ALIGNMENT_ENABLED, COMSIG_CULTIVATOR_ALIGNMENT_DISABLED))
+ remove_flight(power_holder)
+ . = ..()
+
+/// When alignemnt is enabled, start flying
+/datum/power/cultivator/fly_like_a_shooting_star/proc/on_alignment_enabled(mob/living/user, datum/action/cooldown/power/cultivator/alignment/alignment_action)
+ SIGNAL_HANDLER
+ if(!istype(alignment_action, /datum/action/cooldown/power/cultivator/alignment/astral_touched))
+ return
+ apply_flight(user)
+
+/// When alignment is disabled, stop flying
+/datum/power/cultivator/fly_like_a_shooting_star/proc/on_alignment_disabled(mob/living/user, datum/action/cooldown/power/cultivator/alignment/alignment_action)
+ SIGNAL_HANDLER
+ if(!istype(alignment_action, /datum/action/cooldown/power/cultivator/alignment/astral_touched))
+ return
+ remove_flight(user)
+
+/// Adds the flight traits and particles on alignment activation
+/datum/power/cultivator/fly_like_a_shooting_star/proc/apply_flight(mob/living/user)
+ if(!user)
+ return
+ user.AddElementTrait(TRAIT_ASTRAL_TOUCHED_FLIGHT, REF(src), /datum/element/forced_gravity, 0)
+ user.AddElementTrait(TRAIT_ASTRAL_TOUCHED_FLIGHT, REF(src), /datum/element/simple_flying)
+ if(!flight_trail)
+ flight_trail = new
+ flight_trail.set_up(user)
+ flight_trail.start()
+
+/// Removes the flight trait and particles on alignment deactivation
+/datum/power/cultivator/fly_like_a_shooting_star/proc/remove_flight(mob/living/user)
+ if(!user)
+ return
+ REMOVE_TRAIT(user, TRAIT_ASTRAL_TOUCHED_FLIGHT, REF(src))
+ if(flight_trail)
+ flight_trail.stop()
+ QDEL_NULL(flight_trail)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/from_friction_comes_flame.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/from_friction_comes_flame.dm
new file mode 100644
index 00000000000000..c6009772800da1
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/from_friction_comes_flame.dm
@@ -0,0 +1,46 @@
+/*
+ Punches cause heat build-up; sets on fire at a certain heat target (warm-bloods beware!)
+*/
+/datum/power/cultivator/from_friction_comes_flame
+ name = "From Friction Comes Flame"
+ desc = "Your punches while in alignment cause the target to heat up. Once they reach 80C, your strikes also combust the target."
+ security_record_text = "Subject heats up and ignites targets with their punches while in their heightened state."
+ security_threat = POWER_THREAT_MAJOR
+ value = 3
+ required_powers = list(/datum/power/cultivator_root/flame_soul)
+
+ /// how much we BRING THE HEAT on our punches
+ var/bonus_heat = 20
+ /// the flame stacks we apply per punch
+ var/bonus_flame_stacks = 0.15
+ /// the threshold on setting targets on fire in KELVIN (basically celcius but +273)
+ var/temperature_threshold = 353
+ /// reference to flame soul alignment
+ var/datum/action/cooldown/power/cultivator/alignment/flame_soul/flame_soul_alignment
+
+/datum/power/cultivator/from_friction_comes_flame/add()
+ RegisterSignal(power_holder, COMSIG_HUMAN_UNARMED_HIT, PROC_REF(on_unarmed_hit))
+
+/datum/power/cultivator/from_friction_comes_flame/remove()
+ UnregisterSignal(power_holder, COMSIG_HUMAN_UNARMED_HIT)
+
+/// Sends a signal to the new signaler for unarmed punches.
+/// Will probably be used a lot more with cultivator.
+/datum/power/cultivator/from_friction_comes_flame/proc/on_unarmed_hit(mob/living/user, mob/living/target, obj/item/bodypart/affecting, damage, armor_block, limb_sharpness)
+ SIGNAL_HANDLER
+ if(!target || !is_flame_soul_alignment_active(user))
+ return
+ target.adjust_bodytemperature(bonus_heat, 0, 1000)
+ if(target.bodytemperature >= temperature_threshold)
+ target.adjust_fire_stacks(bonus_flame_stacks)
+ target.ignite_mob()
+
+/// Checks if our alignment is active.
+/datum/power/cultivator/from_friction_comes_flame/proc/is_flame_soul_alignment_active(mob/living/user)
+ if(!flame_soul_alignment)
+ for(var/datum/action/cooldown/power/cultivator/alignment/flame_soul/alignment_action in user.actions)
+ flame_soul_alignment = alignment_action
+ break
+ if(!flame_soul_alignment)
+ return FALSE
+ return flame_soul_alignment.active
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm
new file mode 100644
index 00000000000000..40d6a35afaaa89
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm
@@ -0,0 +1,377 @@
+/datum/power/cultivator/many_stars
+ name = "The Many Stars that Dot the Endless Sky"
+ desc = "An active ability. Activating it sends forth a little star, which stops when it reaches it's destination (or hits an object) passively glowing in an area as a light source for 60 seconds. \
+ ou can have up to 8 of these active. \
+ While in alignment, you can right click with this ability to explode all active stars that are not in motion dealing 20 burn damage to all creatures in a 3x3 area centered on it. \
+ Exploding the stars consumes Energy per star. No cooldown."
+ security_record_text = "Subject can shoot lights to illuminate an area, which can be detonated while in a heightened state to explode and damage those around it."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+ required_powers = list(/datum/power/cultivator_root/astral_touched)
+ action_path = /datum/action/cooldown/power/cultivator/many_stars
+
+// Tracks what mouse button was pressed and routes it through use_action
+/// No mouse-press, either fallback or something weird happened.
+#define STARS_CLICK_NONE 0
+/// Left mouse click.
+#define STARS_CLICK_LEFT 1
+/// Right mouse click.
+#define STARS_CLICK_RIGHT 2
+
+/datum/action/cooldown/power/cultivator/many_stars
+ name = "The Many Stars that Dot the Endless Sky"
+ desc = "Activating the ability sends forth a little star, which stops when it reaches it's destination (or hits an object) passively glowing in an area as a light source for 5 minutes. \
+ ou can have up to 8 of these active. \
+ While in alignment, you can right click with this ability to explode all active stars that are not in motion dealing 20 burn damage to all creatures in a 3x3 area centered on it. \
+ Exploding the stars consumes Energy per star. No cooldown."
+ button_icon = 'icons/effects/eldritch.dmi'
+ button_icon_state = "ring_leader_effect"
+
+ click_to_activate = TRUE
+ unset_after_click = FALSE
+ anti_magic_on_target = FALSE
+ click_cd_override = 3 // matches cooldown between shots
+
+ /// Icon for the stars we throw
+ var/star_icon = 'icons/effects/eldritch.dmi'
+ /// Icon state for the stars we throw
+ var/star_state = "ring_leader_effect"
+ /// Color for the stars we throw
+ var/star_color = "#c1effa"
+
+ /// how big are the stars
+ var/star_size = 0.7
+ /// how long do the stars last
+ var/star_duration = 300 SECONDS
+ /// the light range
+ var/star_light_range = 5
+ /// the light's power (how strong of a light)
+ var/star_light_power = 1
+ /// the max amount of stars
+ var/max_active_stars = 8
+ /// list of stars that are currently active
+ var/list/active_stars
+
+ /// world.time for when we should be able to shoot our next shot
+ var/next_star_shot_time = 0
+ /// Cooldown for shots in miliseconds.
+ var/star_shot_delay = 3
+
+ /// how much damage does the star do on explosion
+ var/star_explosion_damage = 25
+ /// the explosion effect range
+ var/star_explosion_range = 1
+ /// the explosion sound
+ var/star_explosion_sound = 'sound/effects/magic/wandodeath.ogg'
+ /// the energy cost for exploding the stars
+ var/star_explosion_cost = CULTIVATOR_ENERGY_TRIVIAL * 2
+ /// the energy cost per star
+ var/star_explosion_cost_per_star = CULTIVATOR_ENERGY_TRIVIAL
+
+ /// Cached alignment action for gating effects.
+ var/datum/action/cooldown/power/cultivator/alignment/astral_touched/astral_alignment
+ /// Which mouse click is used in use_action
+ var/stars_click_type = STARS_CLICK_NONE
+
+/datum/action/cooldown/power/cultivator/many_stars/InterceptClickOn(mob/living/clicker, params, atom/target)
+ var/list/modifiers = params2list(params)
+ if(LAZYACCESS(modifiers, RIGHT_CLICK)) // EXPLOSION
+ stars_click_type = STARS_CLICK_RIGHT
+ else
+ stars_click_type = STARS_CLICK_LEFT
+ . = ..()
+ if(!.)
+ stars_click_type = STARS_CLICK_NONE
+ return TRUE
+
+/datum/action/cooldown/power/cultivator/many_stars/use_action(mob/living/user, atom/target)
+ // Sets the click type.
+ if(stars_click_type == STARS_CLICK_RIGHT) // if right click, explode
+ return explode_active_stars(user)
+ if(world.time < next_star_shot_time) // otherwise, we shoot stars.
+ return FALSE
+ next_star_shot_time = world.time + star_shot_delay
+ if(fire_projectile(user, target, /obj/projectile/resonant/many_stars))
+ playsound(user, 'sound/effects/magic/cosmic_energy.ogg', 60, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+ return FALSE
+
+/// If we get dispelled, our stars end. Whilst this goes around the ususal philosophy of dispelling the target object, you are still linked to them with the ability to blow it up.
+/datum/action/cooldown/power/cultivator/many_stars/proc/dispel(atom/target, atom/dispeller)
+ var/list/stars_to_del = active_stars.Copy()
+ if(stars_to_del)
+ to_chat(target, span_boldwarning("Your stars suddenly vanish!"))
+ for(var/obj/effect/many_stars_star/star as anything in stars_to_del)
+ qdel(star)
+
+/// Checks where to place the star
+/datum/action/cooldown/power/cultivator/many_stars/proc/can_place_star(turf/target_turf)
+ if(!target_turf || !isopenturf(target_turf))
+ return FALSE
+ if(target_turf.is_blocked_turf(exclude_mobs = TRUE)) // space needs to not be blocked
+ return FALSE
+ if(locate(/obj/effect/many_stars_star) in target_turf) // space can't already have a star
+ return FALSE
+ return TRUE
+
+/// Adds a star to the list and removes the oldest if it exceeds the max
+/datum/action/cooldown/power/cultivator/many_stars/proc/register_star(obj/effect/many_stars_star/new_star)
+ if(!new_star)
+ return
+ LAZYINITLIST(active_stars)
+ active_stars += new_star
+ if(length(active_stars) > max_active_stars)
+ var/obj/effect/many_stars_star/oldest_star = active_stars[1]
+ if(oldest_star)
+ qdel(oldest_star)
+
+/// Removes a star from the list.
+/datum/action/cooldown/power/cultivator/many_stars/proc/unregister_star(obj/effect/many_stars_star/old_star)
+ if(!active_stars || !old_star)
+ return
+ active_stars -= old_star
+
+/// KA-BOOOOOOOM. Blows up all stars in our active stars list.
+/datum/action/cooldown/power/cultivator/many_stars/proc/explode_active_stars(mob/living/user)
+ if(!is_astral_alignment_active(user))
+ if(user)
+ user.balloon_alert(user, "alignment required!")
+ return
+ if(!active_stars || !length(active_stars))
+ return
+ if(energy_component.energy < star_explosion_cost)
+ user.balloon_alert(user, "needs more energy!")
+ if(user)
+ user.log_message("detonated their Many Stars.", LOG_GAME)
+
+ var/list/stars_to_explode = active_stars.Copy()
+ adjust_energy(-star_explosion_cost) // removes the base cost
+ for(var/obj/effect/many_stars_star/star as anything in stars_to_explode)
+ if(QDELETED(star))
+ continue
+ var/turf/star_turf = get_turf(star)
+ if(!star_turf)
+ continue
+ playsound(star_turf, star_explosion_sound, 75, TRUE)
+ star_turf.visible_message(span_bolddanger("The star explodes in a wave of energy!"))
+ // applies damage, does cool effects, does logging.
+ var/obj/effect/temp_visual/circle_wave/explosion_fx = new /obj/effect/temp_visual/circle_wave/many_stars(star_turf)
+ explosion_fx.color = star_color
+ for(var/turf/effect_turf in range(star_explosion_range, star_turf))
+ for(var/mob/living/target in effect_turf)
+ // We skip this whole thing if the mob is immune to resonance
+ if(target.can_block_resonance(ANTIRESONANCE_BASE_CHARGE_COST) || target.can_block_magic(MAGIC_RESISTANCE, ANTIRESONANCE_BASE_CHARGE_COST))
+ continue
+ var/dam_dealt = apply_damage_with_armor(target, star_explosion_damage, damage_type = astral_alignment?.alignment_damage_type || BURN, attack_flag = BOMB)
+ target.log_message("was hit by a Many Stars detonation from [user] for [dam_dealt] damage.", LOG_VICTIM, color="blue")
+ if(user)
+ user.log_message("detonated Many Stars against [target] for [dam_dealt] damage.", LOG_ATTACK, color="red")
+ to_chat(target, span_userdanger("You are hit by an explosive blast of energy!"))
+ qdel(star)
+
+ // Removes cost per star; if we end up at 0, explode no more stars and shut off their power.
+ adjust_energy(-star_explosion_cost_per_star)
+ if(energy_component.energy <= 0)
+ user.balloon_alert(user, "no more energy!")
+ if(astral_alignment.active)
+ astral_alignment.disable_alignment(user)
+ break
+
+/// Gets & sets astral allignment. We only really reference it in the explosion.
+/datum/action/cooldown/power/cultivator/many_stars/proc/is_astral_alignment_active(mob/living/user)
+ if(!astral_alignment)
+ for(var/datum/action/cooldown/power/cultivator/alignment/astral_touched/alignment_action in user.actions)
+ astral_alignment = alignment_action
+ break
+ if(!astral_alignment)
+ return FALSE
+ return astral_alignment.active
+
+/// Creates the lingering star on impact.
+/datum/action/cooldown/power/cultivator/many_stars/proc/spawn_star(turf/impact_turf, turf/fallback_turf, obj/projectile/resonant/many_stars/source_projectile)
+ var/turf/placement_turf = null
+ if(can_place_star(impact_turf))
+ placement_turf = impact_turf
+ else if(can_place_star(fallback_turf))
+ placement_turf = fallback_turf
+
+ if(!placement_turf)
+ return
+
+ var/obj/effect/many_stars_star/new_star = new(placement_turf)
+ new_star.owner_power = src
+ new_star.lifespan = star_duration
+ new_star.light_range = star_light_range
+ new_star.light_power = star_light_power
+ // Copies the effects of the soruce projectile if possible.
+ if(source_projectile)
+ new_star.icon = source_projectile.icon
+ new_star.icon_state = source_projectile.icon_state
+ new_star.color = source_projectile.color
+ new_star.light_color = source_projectile.star_color ? source_projectile.star_color : source_projectile.color
+ new_star.star_size = source_projectile.star_size
+ else
+ new_star.icon = star_icon
+ new_star.icon_state = star_state
+ new_star.color = star_color
+ new_star.light_color = star_color
+ new_star.star_size = star_size
+
+ new_star.apply_star_scale()
+
+ register_star(new_star)
+
+// Applies the configured effects to the star.
+/datum/action/cooldown/power/cultivator/many_stars/ready_projectile(obj/projectile/projectile_instance, atom/target, mob/living/user)
+ . = ..()
+ if(!projectile_instance || !istype(projectile_instance, /obj/projectile/resonant/many_stars))
+ return
+
+ // Applies the icon, state and color to the projectie.
+ var/obj/projectile/resonant/many_stars/star_proj = projectile_instance
+ star_proj.star_icon = star_icon
+ star_proj.star_state = star_state
+ star_proj.star_color = star_color
+
+ if(star_icon)
+ star_proj.icon = star_icon
+ if(star_state)
+ star_proj.icon_state = star_state
+ if(star_color)
+ star_proj.color = star_color
+ if(star_size)
+ star_proj.star_size = star_size
+ star_proj.apply_star_scale()
+
+ // Applies the light to hte projectile.
+ star_proj.light_range = star_light_range
+ star_proj.light_power = star_light_power
+ star_proj.light_color = star_color ? star_color : star_proj.color
+ star_proj.light_on = TRUE
+ star_proj.set_light(star_light_range, star_light_power, star_color ? star_color : star_proj.color, l_on = TRUE)
+
+ var/turf/target_turf = get_turf(target)
+ star_proj.target_turf = target_turf
+
+/obj/projectile/resonant/many_stars
+ name = "star"
+ icon = 'icons/effects/eldritch.dmi'
+ icon_state = "ring_leader_effect"
+
+ /// Icon of the star
+ var/star_icon
+ /// Icon state of the star
+ var/star_state
+ /// Color of the star
+ var/star_color
+ /// Size of the star's sprite
+ var/star_size = 0.5
+ /// The last turf we passed through as a star
+ var/turf/last_passed_turf
+ /// The turf the caster aimed us ast
+ var/turf/target_turf
+ /// Have we reached our destination?
+ var/reached_target = FALSE
+
+// Tracks the last space we were in
+/obj/projectile/resonant/many_stars/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE)
+ . = ..()
+ if(old_loc)
+ last_passed_turf = get_turf(old_loc)
+ if(!reached_target && target_turf && get_turf(src) == target_turf) // if we're at the click target we stop
+ reached_target = TRUE
+ deletion_queued = PROJECTILE_RANGE_DELETE
+
+// Runs the star spawning on hit
+/obj/projectile/resonant/many_stars/on_hit(atom/target, blocked, pierce_hit)
+ . = ..()
+ var/turf/impact_turf = get_turf(target)
+ var/datum/action/cooldown/power/cultivator/many_stars/power = creating_power
+ if(power)
+ power.spawn_star(impact_turf, last_passed_turf, src)
+
+// If we cap out on range
+/obj/projectile/resonant/many_stars/on_range()
+ . = ..()
+ var/turf/impact_turf = get_turf(src)
+ var/datum/action/cooldown/power/cultivator/many_stars/power = creating_power
+ if(power)
+ power.spawn_star(impact_turf, last_passed_turf, src)
+
+/// Applies the size to the projectile
+/obj/projectile/resonant/many_stars/proc/apply_star_scale()
+ if(!star_size)
+ return
+ var/matrix/scale_matrix = matrix()
+ scale_matrix.Scale(star_size, star_size)
+ transform = scale_matrix
+
+// The lingering star effect
+/obj/effect/many_stars_star
+ name = "star"
+ icon = 'icons/effects/eldritch.dmi'
+ icon_state = "ring_leader_effect"
+ anchored = TRUE
+ density = FALSE
+ max_integrity = 1
+ light_range = 3
+ light_power = 1
+ light_color = "#66c5dd"
+ /// Size of the object
+ var/star_size = 0.5
+ /// Lifespan of the object
+ var/lifespan = 300 SECONDS
+ /// Reference to the action datum that created this
+ var/datum/action/cooldown/power/cultivator/many_stars/owner_power
+
+// Adds expiration timer and size modifier.
+/obj/effect/many_stars_star/Initialize(mapload)
+ . = ..()
+ apply_star_scale()
+ if(lifespan)
+ addtimer(CALLBACK(src, PROC_REF(expire)), lifespan)
+
+/obj/effect/many_stars_star/Destroy()
+ if(owner_power)
+ owner_power.unregister_star(src)
+ owner_power = null
+ return ..()
+
+/// On expiration, just remove.
+/obj/effect/many_stars_star/proc/expire()
+ qdel(src)
+
+// Dissipate harmlessly on attack
+/obj/effect/many_stars_star/attackby(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers)
+ . = ..()
+ if(attacking_item?.force)
+ if(user)
+ to_chat(user, span_notice("You smack the star, and it vanishes!"))
+ qdel(src)
+ return .
+
+// Same with an unarmed touch.
+/obj/effect/many_stars_star/attack_hand(mob/living/user, list/modifiers)
+ . = ..()
+ if(user)
+ to_chat(user, span_notice("You interact with the star, and it vanishes!"))
+ qdel(src)
+ return .
+
+/// Applies the size modifier on the star item.
+/obj/effect/many_stars_star/proc/apply_star_scale()
+ if(!star_size)
+ return
+ var/matrix/scale_matrix = matrix()
+ scale_matrix.Scale(star_size, star_size)
+ transform = scale_matrix
+
+// The aoe explosion circle from many_stars
+/obj/effect/temp_visual/circle_wave/many_stars
+ color = COLOR_CYAN
+ duration = 0.5 SECONDS
+ amount_to_scale = 1.5
+
+#undef STARS_CLICK_NONE
+#undef STARS_CLICK_LEFT
+#undef STARS_CLICK_RIGHT
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm
new file mode 100644
index 00000000000000..877de48e636aec
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm
@@ -0,0 +1,210 @@
+/datum/power/cultivator/set_fire_to_dry_hay
+ name = "Set Fire to Dry Hay"
+ desc = "You can set fire onto anything you touch. This works similary to a ligher in terms of functionality. \
+ While in Alignment, you can right click shoot a flameblast that ignite everything in the area where it lands. \
+ Using the alignment version consumes Energy. No cooldown."
+ security_record_text = "Subject can set fire to any object in melee range. While in a heightened state, they can shoot motes of flame to ignite anything hit as well."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+ required_powers = list(/datum/power/cultivator_root/flame_soul)
+ action_path = /datum/action/cooldown/power/cultivator/set_fire_to_dry_hay
+
+// Mouse click handlers
+/// No mouse click
+#define FIRE_CLICK_NONE 0
+/// Left mouse click
+#define FIRE_CLICK_LEFT 1
+/// Right mouse click
+#define FIRE_CLICK_RIGHT 2
+
+/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay
+ name = "Set Fire to Dry Hay"
+ desc = "You can set fire onto anything you touch. This works similary to a lighter in terms of functionality. \
+ While in Alignment, you can right click to shoot a flameblast that ignite everything in the area where it lands. \
+ Using the alignment version consumes Energy. No cooldown."
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "fireball"
+
+ click_to_activate = TRUE
+ unset_after_click = FALSE
+ click_cd_override = 5 // matches cooldown between shots
+
+ /// Cooldown for right click projectile, in deciseconds.
+ var/projectile_delay = 5
+ /// World-time for when the next projectile is ready
+ var/next_projectile_time = 0
+
+ /// cost for flameblast projectile
+ var/flameblast_cost = 20
+ /// Icon for flameblast projectile
+ var/flameblast_icon = null
+ /// Icon state for flamebast projectile
+ var/flameblast_icon_state = "fireball"
+ /// Icon scale for the flameblast projectile
+ var/flameblast_scale = 0.7
+
+ /// Flamebast's light range
+ var/flameblast_light_range = 3
+ /// Flameblast's light power
+ var/flameblast_light_power = 1
+ /// Flameblast's light color
+ var/flameblast_light_color = "#e99a3f"
+
+ /// Flameblast projectile's on-hit damage
+ var/flameblast_damage = 10
+ /// Flaemblast projectile's firestacks on hit
+ var/flameblast_firestacks = 0.1
+ /// The sound of flameblast impacting
+ var/flameblast_impact_sound = 'sound/effects/fire_puff.ogg'
+ /// Cached alignment action for gating right click effects.
+ var/datum/action/cooldown/power/cultivator/alignment/flame_soul/flame_soul_alignment
+ /// Which mouse click is used in use_action
+ var/fire_click_type = FIRE_CLICK_NONE
+
+// We use both left and right mouse button.
+/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/InterceptClickOn(mob/living/clicker, params, atom/target)
+ var/list/modifiers = params2list(params)
+ if(LAZYACCESS(modifiers, RIGHT_CLICK))
+ fire_click_type = FIRE_CLICK_RIGHT
+ else
+ fire_click_type = FIRE_CLICK_LEFT
+ . = ..()
+ if(!.)
+ fire_click_type = FIRE_CLICK_NONE
+ return TRUE // Always consume the click to avoid normal click interactions.
+
+/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/use_action(mob/living/user, atom/target)
+ // Sets the click type.
+ if(fire_click_type == FIRE_CLICK_RIGHT) // shoots flameblasts instead of lighting cigs.
+ return shoot_flameblast(user, target)
+ if(!target)
+ return FALSE
+ // Lighter version only works in melee range.
+ var/turf/user_turf = get_turf(user)
+ var/turf/target_turf = get_turf(target)
+ if(user_turf && target_turf && get_dist(user_turf, target_turf) > 1)
+ owner.balloon_alert(user, "Out of range!")
+ return FALSE
+ var/obj/item/cultivator_virtual_lighter/lighter = new
+ // Allow the lighter's cigarette lighting behavior on mobs.
+ if(isliving(target))
+ var/mob/living/target_mob = target
+ lighter.attack(target_mob, user, list(), list())
+ qdel(lighter)
+ return TRUE
+ // Allow lighting loose cigarettes directly.
+ if(istype(target, /obj/item/cigarette))
+ var/obj/item/cigarette/cig = target
+ cig.attackby(lighter, user, list(), list())
+ qdel(lighter)
+ return TRUE
+
+ // First run normal item-interaction handlers (candles use this path).
+ var/item_interact_result = target.item_interaction(user, lighter, list())
+ if(!(item_interact_result & ITEM_INTERACT_ANY_BLOCKER))
+ // Fallback to attackby handlers (bonfires use this path).
+ target.attackby(lighter, user, list(), list())
+ // Finally, raw ignition for generic flammables.
+ if((target.resistance_flags & FLAMMABLE) && !(target.resistance_flags & FIRE_PROOF))
+ target.fire_act(lighter.get_temperature())
+ qdel(lighter)
+ playsound(user, 'sound/effects/fire_puff.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ // Always return TRUE to keep the click ability active.
+ return TRUE
+
+/// Gets & caches flame soul alignment for gating the right click.
+/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/proc/is_flame_soul_alignment_active(mob/living/user)
+ if(!flame_soul_alignment)
+ for(var/datum/action/cooldown/power/cultivator/alignment/flame_soul/alignment_action in user.actions)
+ flame_soul_alignment = alignment_action
+ break
+ if(!flame_soul_alignment)
+ return FALSE
+ return flame_soul_alignment.active
+
+/// Shoots a lil flameblast when we're in alignment.
+/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/proc/shoot_flameblast(mob/living/user, atom/target)
+ if(!is_flame_soul_alignment_active(user))
+ user.balloon_alert(user, "alignment required!")
+ return FALSE
+ if(world.time < next_projectile_time)
+ return FALSE
+ next_projectile_time = world.time + projectile_delay
+ fire_projectile(user, target, /obj/projectile/resonant/fire_to_dry_hay)
+ playsound(user, 'sound/effects/fire_puff.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ adjust_energy(-flameblast_cost)
+ return TRUE
+
+// Applies projectile customization here.
+/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/ready_projectile(obj/projectile/projectile_instance, atom/target, mob/living/user)
+ . = ..()
+ if(!projectile_instance)
+ return
+ var/obj/projectile/flame_projectile = projectile_instance
+ if(flameblast_icon)
+ flame_projectile.icon = flameblast_icon
+ if(flameblast_icon_state)
+ flame_projectile.icon_state = flameblast_icon_state
+ if(flameblast_scale)
+ var/matrix/scale_matrix = matrix(flame_projectile.transform)
+ scale_matrix.Scale(flameblast_scale, flameblast_scale)
+ flame_projectile.transform = scale_matrix
+ flame_projectile.damage = flameblast_damage
+ flame_projectile.light_range = flameblast_light_range
+ flame_projectile.light_power = flameblast_light_power
+ flame_projectile.light_color = flameblast_light_color
+ flame_projectile.light_on = TRUE
+ flame_projectile.set_light(flame_projectile.light_range, flame_projectile.light_power, flame_projectile.light_color, l_on = TRUE)
+
+// Because welder/lighter interactions check for get_temprature on the item we kind of have to make an abstract item do the work for us.
+/obj/item/cultivator_virtual_lighter
+ parent_type = /obj/item/lighter
+ name = "\improper cultivator flame"
+ desc = "A conjured spark of flame."
+ fancy = TRUE
+ heat_while_on = HIGH_TEMPERATURE_REQUIRED - 100
+
+/obj/item/cultivator_virtual_lighter/Initialize(mapload)
+ . = ..()
+ lit = FALSE // so we have to make sure its unlit before we light it or it won't work. I love it here.
+ set_lit(TRUE)
+
+/obj/item/cultivator_virtual_lighter/get_fuel()
+ return INFINITY
+
+/obj/item/cultivator_virtual_lighter/ignition_effect(atom/A, mob/user)
+ if(get_temperature())
+ return span_infoplain(span_rose("[user] touches the tip of [A] with [user.p_their()] finger and it ignites. Badass!"))
+ return ""
+
+// The fire projectile
+/obj/projectile/resonant/fire_to_dry_hay
+ name = "flameblast"
+ icon_state = "fireball"
+ damage = 10
+ armour_penetration = 0 // doesnt do jack to fireproofing
+ damage_type = BURN
+ armor_flag = FIRE
+
+/obj/projectile/resonant/fire_to_dry_hay/on_hit(atom/target, blocked, pierce_hit)
+ . = ..()
+ var/turf/impact_turf = get_turf(target)
+ if(!impact_turf)
+ return
+ var/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/power = creating_power
+ if(power?.flameblast_impact_sound)
+ playsound(impact_turf, power.flameblast_impact_sound, 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ // Apply fire stacks to mobs; ignite objects on the turf.
+ for(var/mob/living/burning_mob in impact_turf.contents)
+ burning_mob.adjust_fire_stacks(power?.flameblast_firestacks || 0)
+ burning_mob.ignite_mob()
+ for(var/atom/movable/ignite_target in impact_turf.contents)
+ if(ismob(ignite_target))
+ continue
+ if(ignite_target.resistance_flags & FIRE_PROOF)
+ continue
+ ignite_target.fire_act(500)
+
+#undef FIRE_CLICK_NONE
+#undef FIRE_CLICK_LEFT
+#undef FIRE_CLICK_RIGHT
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm
new file mode 100644
index 00000000000000..573c8039408172
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm
@@ -0,0 +1,227 @@
+/datum/power/cultivator_root/shadow_walker
+ name = "Shadow Walker Alignment"
+ desc = "You gain Energy through Aura by being in dark rooms and environments. Activating it wraps you in an aura of shadow.\
+ \nYou are entirely unrecognizeable in this state and your punches do extra brute damage.\
+ \nPassively, you have enhanced darkvision, and gain full on night vision while your alignment is activated.\
+ \nYou gain armor IV across your whole body. Has diminishing effects with your worn armor."
+ security_record_text = "Subject can enter a heightened state by observing darkness, granting them resistance to damage, deadlier punches, the abiliy to become unrecognizeable as a dark silhouette and the ability to see perfectly in the dark."
+ security_threat = POWER_THREAT_MAJOR
+ action_path = /datum/action/cooldown/power/cultivator/alignment/shadow_walker
+ value = 5
+
+// Lets you see in the dark.
+/datum/power/cultivator_root/shadow_walker/post_add()
+ . = ..()
+ ADD_TRAIT(power_holder, TRAIT_MINOR_NIGHT_VISION, REF(src))
+ power_holder.update_sight()
+
+/datum/power/cultivator_root/shadow_walker/remove()
+ . = ..()
+ REMOVE_TRAIT(power_holder, TRAIT_MINOR_NIGHT_VISION, REF(src))
+ power_holder.update_sight()
+
+/datum/action/cooldown/power/cultivator/alignment/shadow_walker
+ name = "Shadow Walker Alignment"
+ desc = "Activates your Shadow Walker Alignment aura, granting you immunity to slowdowns, increasing your defenses (if unarmored), and increasing your strength with unarmed attacks."
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "void_magnet"
+
+
+ alignment_outline_color = "#000000"
+ alignment_overlay_state = "curse"
+
+ /// the spooky overlay unique to shadow walker
+ var/mutable_appearance/echo_overlay
+ /// global name/identity masking
+ var/datum/shadowwalker_identity/shadowwalker_identity
+
+// Adds pressure immunity & cold immunity.
+/datum/action/cooldown/power/cultivator/alignment/shadow_walker/enable_alignment(mob/living/carbon/user)
+ . = ..()
+ ADD_TRAIT(user, TRAIT_TRUE_NIGHT_VISION, src)
+ user.update_sight()
+ RegisterSignal(user, COMSIG_MOB_UPDATE_HELD_ITEMS, PROC_REF(on_held_items_updated))
+ RegisterSignal(user, COMSIG_LIVING_POST_UPDATE_TRANSFORM, PROC_REF(on_transform_updated))
+ if(!shadowwalker_identity)
+ shadowwalker_identity = new(user)
+ refresh_echo_overlay(user)
+ //extra spooky 4 clown
+ if(is_clown_job(user.mind?.assigned_role))
+ playsound(user, 'sound/misc/scary_horn.ogg', 60, TRUE)
+
+/datum/action/cooldown/power/cultivator/alignment/shadow_walker/disable_alignment(mob/living/carbon/user)
+ . = ..()
+ REMOVE_TRAIT(user, TRAIT_TRUE_NIGHT_VISION, src)
+ user.update_sight()
+ UnregisterSignal(user, COMSIG_MOB_UPDATE_HELD_ITEMS)
+ UnregisterSignal(user, COMSIG_LIVING_POST_UPDATE_TRANSFORM)
+ QDEL_NULL(shadowwalker_identity)
+ user.cut_overlay(echo_overlay)
+
+/datum/action/cooldown/power/cultivator/alignment/shadow_walker/aura_farm()
+ var/total = 0
+ var/mob/living/owner_mob = owner
+ if(!owner_mob)
+ return total
+
+ // For reference: dark means lum <=0.2, dim is lum <=0.5 and everything above that is called bright.
+ var/dim_space_value = CULTIVATOR_AURA_FARM_TRIVIAL * 0.2 // if there's dim light
+ var/darkness_space_value = CULTIVATOR_AURA_FARM_TRIVIAL // darkness itself
+ var/stood_in_darkness = CULTIVATOR_AURA_FARM_MODERATE // if we are stood in the dark
+ var/fully_dark_bonus = CULTIVATOR_AURA_FARM_MODERATE // only seeing dim and dark
+ var/spacious_fully_dark_bonus = CULTIVATOR_AURA_FARM_MODERATE // only seeing dim and dark + is spacious (30)
+
+ var/dim_threshold = 0.5
+ var/viewable_turfs = 0
+ var/any_bright = FALSE
+
+ // Gets the dim and darkness of every space
+ for(var/turf/T in view(owner_mob))
+ if(!istype(T, /turf/open)) // no walls
+ continue
+ if(IS_OPAQUE_TURF(T)) // no non-opaque stuff (shutters, blackened windows, etc) that still counts as open
+ continue
+ viewable_turfs++
+ var/lum = T.get_lumcount()
+ if(lum <= LIGHTING_TILE_IS_DARK)
+ total += darkness_space_value
+ else if(lum <= dim_threshold)
+ total += dim_space_value
+ else
+ any_bright = TRUE
+
+ // Are we stood in darkness? Or are we stuffed away somewhere that the light probably doesn't see us?
+ var/turf/owner_turf = get_turf(owner_mob)
+ if(!isturf(owner_mob.loc) || (owner_turf && owner_turf.get_lumcount() <= dim_threshold))
+ total += stood_in_darkness
+
+ // Are there any bright tiles?
+ if(!any_bright)
+ total += fully_dark_bonus
+ if(viewable_turfs >= 30)
+ total += spacious_fully_dark_bonus
+
+ return total
+
+// We override the normal fx activation because this looks cooler.
+/datum/action/cooldown/power/cultivator/alignment/shadow_walker/activation_fx(mob/living/carbon/user, atom/target)
+ refresh_echo_overlay(user)
+
+ // adds overlay
+ if(!alignment_overlay)
+ alignment_overlay = mutable_appearance(alignment_overlay_icon, alignment_overlay_state, alignment_overlay_layer)
+ alignment_overlay.color = alignment_outline_color
+ user.add_overlay(alignment_overlay)
+
+/// Refreshes the overlay, because mechanically we want to always keep the user covered, we need to actually reupdate it during various animations (knockdown e.g)
+/datum/action/cooldown/power/cultivator/alignment/shadow_walker/proc/refresh_echo_overlay(mob/living/carbon/user)
+ // Use the same matrix as echolocation
+ var/static/list/black_white_matrix = list(
+ 85, 85, 85, 0,
+ 85, 85, 85, 0,
+ 85, 85, 85, 0,
+ 0, 0, 0, 1,
+ -254, -254, -254, 0
+ )
+ user.cut_overlay(echo_overlay)
+ echo_overlay = new /mutable_appearance(user.icon, user.icon_state)
+ echo_overlay.copy_overlays(user)
+ echo_overlay.dir = user.dir
+ echo_overlay.color = black_white_matrix
+ echo_overlay.filters += outline_filter(size = 1, color = COLOR_WHITE)
+
+ echo_overlay.layer = user.layer
+ user.add_overlay(echo_overlay)
+
+ // Keep the pulsing outline filter alive through rebuilds.
+ user.remove_filter(filter_id)
+ user.add_filter(filter_id, 2, outline_filter(size = alignment_outline_size, color = "#ffffff"))
+ var/filter = user.get_filter(filter_id)
+ if(filter)
+ animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1)
+ animate(alpha = 40, time = 2.5 SECONDS)
+
+/// Whenever any held item is changed that would possibly alter the sprite's appearance
+/datum/action/cooldown/power/cultivator/alignment/shadow_walker/proc/on_held_items_updated(mob/living/carbon/user)
+ SIGNAL_HANDLER
+ if(!user)
+ return
+ refresh_echo_overlay(user)
+
+/// When animation effect occurs.
+/datum/action/cooldown/power/cultivator/alignment/shadow_walker/proc/on_transform_updated(mob/living/carbon/user)
+ SIGNAL_HANDLER
+ if(!user)
+ return
+ refresh_echo_overlay(user)
+
+
+/*
+ Global identity masking for Shadow Walker alignment.
+*/
+/datum/shadowwalker_identity
+ /// Mob that's being affected by the identity mask
+ var/mob/living/carbon/human/owner
+ /// Weakref to the owner
+ var/datum/weakref/owner_ref
+ /// Is it on or not?
+ var/active = FALSE
+
+/datum/shadowwalker_identity/New(mob/living/carbon/human/owner_arg)
+ . = ..()
+ owner = owner_arg
+ owner_ref = WEAKREF(owner)
+ apply()
+
+/datum/shadowwalker_identity/Destroy()
+ clear()
+ owner = null
+ owner_ref = null
+ return ..()
+
+/// Applies various signalers to override info about the mob.
+/datum/shadowwalker_identity/proc/apply()
+ var/mob/living/carbon/human/owner = src.owner || owner_ref?.resolve()
+ if(!istype(owner))
+ return
+ active = TRUE
+ RegisterSignal(owner, COMSIG_HUMAN_GET_VISIBLE_NAME, PROC_REF(on_visible_name))
+ RegisterSignal(owner, COMSIG_HUMAN_GET_FORCED_NAME, PROC_REF(on_forced_name))
+ RegisterSignal(owner, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine))
+ owner.update_visible_name()
+
+/// Removes all traces of the shadowwalker_identity
+/datum/shadowwalker_identity/proc/clear()
+ var/mob/living/carbon/human/owner = src.owner || owner_ref?.resolve()
+ if(owner)
+ UnregisterSignal(owner, list(COMSIG_HUMAN_GET_VISIBLE_NAME, COMSIG_HUMAN_GET_FORCED_NAME, COMSIG_ATOM_EXAMINE))
+ owner.update_visible_name()
+ active = FALSE
+
+/// When a mob gets the visible name of the mob
+/datum/shadowwalker_identity/proc/on_visible_name(mob/living/carbon/human/source, list/identity)
+ SIGNAL_HANDLER
+ if(!active)
+ return
+ if(identity[VISIBLE_NAME_FORCED])
+ return
+ identity[VISIBLE_NAME_FACE] = "Unknown"
+ identity[VISIBLE_NAME_ID] = "Unknown"
+
+/// WHen a mob gets the visible name of the mob; this ones route a littel differently so we have to call both.
+/datum/shadowwalker_identity/proc/on_forced_name(mob/living/carbon/human/source, list/identity)
+ SIGNAL_HANDLER
+ if(!active)
+ return
+ identity[VISIBLE_NAME_FORCED] = INFINITY
+ identity[VISIBLE_NAME_FACE] = "Unknown"
+ identity[VISIBLE_NAME_ID] = "Unknown"
+
+/// When a mob attempts to examine our owner.
+/datum/shadowwalker_identity/proc/on_examine(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+ if(!active)
+ return NONE
+ examine_list.Cut()
+ examine_list += span_warning("It's too shrouded in shadow to make out any details.")
+ return NONE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm
new file mode 100644
index 00000000000000..bf8a233e28519a
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm
@@ -0,0 +1,111 @@
+/*
+ Teleports you from one dark turf to the other.
+ Note: I am disgusted at the sheer amount of ifs but uhh, this is the best I could think of.
+*/
+
+
+/datum/power/cultivator/travel_under_the_veil_of_night
+ name = "Travel Under the Veil of Night"
+ desc = "Whilst your alignment is active, you can spend 1 second channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Energy cost; no cooldown."
+ security_record_text = "Subject can teleport in darkness while in their heightened state."
+ security_threat = POWER_THREAT_MAJOR
+ value = 4
+ required_powers = list(/datum/power/cultivator_root/shadow_walker)
+ action_path = /datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night
+
+/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night
+ name = "Travel Under the Veil of Night"
+ desc = "Whilst your alignment is active, you can spend 1 second channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Energy cost; no cooldown."
+ button_icon = 'icons/effects/effects.dmi'
+ button_icon_state = "blank"
+
+ click_to_activate = TRUE
+ unset_after_click = TRUE
+ cost = 25
+ target_type = /turf
+ use_time = 1 SECONDS
+ /// Cached alignment action for gating effects.
+ var/datum/action/cooldown/power/cultivator/alignment/shadow_walker/shadow_walker_alignment
+
+// Handles the channel-time delay and mid-channel validation.
+/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/do_use_time(mob/living/user, atom/target)
+ if(!check_travel_requirements(user, target))
+ return FALSE
+ return ..()
+
+/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/use_action(mob/living/user, atom/target)
+ // Revalidate after channeling.
+ if(!check_travel_requirements(user, target))
+ return FALSE
+ var/turf/user_turf = get_turf(user)
+ var/turf/target_turf = get_turf(target)
+ if(!user_turf || !target_turf)
+ return FALSE
+
+ // Okay so after that giant check of requirements now we actually try to teleport the person.
+ if(!do_teleport(user, target_turf, no_effects = TRUE))
+ return FALSE
+ playsound(target_turf, 'sound/effects/nightmare_poof.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ playsound(user_turf, 'sound/effects/nightmare_reappear.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ // After image
+ new /obj/effect/temp_visual/blank_echo(user_turf)
+ return TRUE
+
+/// Shared validation for use-time, mid-channel, and post-channel checks.
+/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/proc/check_travel_requirements(mob/living/user, atom/target)
+ if(!target)
+ return FALSE
+ // no teleporting out of where-ever-the-nowhere you are
+ var/turf/user_turf = get_turf(user)
+ if(!user_turf)
+ return FALSE
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ return FALSE
+ // alignment required + darkness
+ if(!is_shadow_walker_alignment_active(user) || !is_turf_in_darkness(user_turf))
+ user.balloon_alert(user, "alignment + darkness required!")
+ return FALSE
+ // LOS requirement
+ if(!(target in view(user.client.view, user)))
+ user.balloon_alert(user, "out of view!")
+ return FALSE
+ // is it open & unblocked?
+ if(!is_valid_destination(target_turf))
+ user.balloon_alert(user, "invalid destination!")
+ return FALSE
+ return TRUE
+
+/// Basically is the turf open/blocked?
+/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/proc/is_valid_destination(turf/target_turf)
+ if(!target_turf || !isopenturf(target_turf))
+ return FALSE
+ if(target_turf.is_blocked_turf(exclude_mobs = TRUE))
+ return FALSE
+ return is_turf_in_darkness(target_turf)
+
+/// IS IT DARK?!
+/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/proc/is_turf_in_darkness(turf/target_turf)
+ if(!target_turf)
+ return FALSE
+ return target_turf.get_lumcount() <= LIGHTING_TILE_IS_DARK
+
+/// ARE WE GOING SUPER SAIYAN?!
+/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/proc/is_shadow_walker_alignment_active(mob/living/user)
+ if(!shadow_walker_alignment || QDELETED(shadow_walker_alignment))
+ for(var/datum/action/cooldown/power/cultivator/alignment/shadow_walker/alignment_action in user.actions)
+ shadow_walker_alignment = alignment_action
+ break
+ if(!shadow_walker_alignment)
+ return FALSE
+ return shadow_walker_alignment.active
+
+/obj/effect/temp_visual/blank_echo
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "blank"
+ duration = 2 SECONDS
+ randomdir = FALSE
+
+/obj/effect/temp_visual/blank_echo/Initialize(mapload)
+ . = ..()
+ animate(src, alpha = 0, time = duration)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/vanish_unseen_into_shadow.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/vanish_unseen_into_shadow.dm
new file mode 100644
index 00000000000000..5a982007f2396e
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/vanish_unseen_into_shadow.dm
@@ -0,0 +1,83 @@
+/*
+ Untrackable by resonant means and no slowdown in darkness. Quick getaways ahoy.
+*/
+
+/datum/power/cultivator/vanish_unseen_into_shadow
+ name = "Vanish Unseen into Shadow"
+ desc = "You are untrackable within the shadows. You are immune to resonant scrying and slowdowns while you're stood in darkness or are in alignment."
+ security_record_text = "Subject is exceedingly fast and immune to resonant-based detection while stood in darkness."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+ required_powers = list(/datum/power/cultivator_root/shadow_walker)
+ power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES
+
+ /// Cached alignment action for gating effects.
+ var/datum/action/cooldown/power/cultivator/alignment/shadow_walker/shadow_walker_alignment
+ /// Current instance of the status effect
+ var/datum/status_effect/power/vanish_unseen_into_shadow/active_effect
+
+// Cleanup lingering effects
+/datum/power/cultivator/vanish_unseen_into_shadow/remove()
+ if(active_effect)
+ qdel(active_effect)
+ active_effect = null
+ return ..()
+
+// Keeps the status effect applied while in darkness or alignment.
+/datum/power/cultivator/vanish_unseen_into_shadow/process(seconds_per_tick)
+ var/mob/living/user = power_holder
+ if(!user)
+ return
+
+ var/should_apply = is_in_darkness(user) || is_shadow_walker_alignment_active(user)
+ if(should_apply)
+ if(!active_effect || QDELETED(active_effect))
+ active_effect = user.apply_status_effect(/datum/status_effect/power/vanish_unseen_into_shadow)
+ return
+
+ if(active_effect)
+ qdel(active_effect)
+ active_effect = null
+
+/// Are we in a dark space?
+/datum/power/cultivator/vanish_unseen_into_shadow/proc/is_in_darkness(mob/living/user)
+ var/turf/user_turf = get_turf(user)
+ if(!user_turf)
+ return FALSE
+ return user_turf.get_lumcount() <= LIGHTING_TILE_IS_DARK
+
+/// Gets and sets our alignment if its not there; then checks if its active.
+/datum/power/cultivator/vanish_unseen_into_shadow/proc/is_shadow_walker_alignment_active(mob/living/user)
+ if(!shadow_walker_alignment || QDELETED(shadow_walker_alignment))
+ for(var/datum/action/cooldown/power/cultivator/alignment/shadow_walker/alignment_action in user.actions)
+ shadow_walker_alignment = alignment_action
+ break
+ if(!shadow_walker_alignment)
+ return FALSE
+ return shadow_walker_alignment.active
+
+// Status effect that handles the bonuses.
+/datum/status_effect/power/vanish_unseen_into_shadow
+ id = "vanish_unseen_into_shadow"
+ duration = STATUS_EFFECT_PERMANENT
+ tick_interval = STATUS_EFFECT_NO_TICK
+ alert_type = /atom/movable/screen/alert/status_effect/vanish_unseen_into_shadow
+
+/datum/status_effect/power/vanish_unseen_into_shadow/on_apply()
+ if(!owner)
+ return FALSE
+ owner.ignore_slowdown(type)
+ ADD_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, type)
+ return TRUE
+
+/datum/status_effect/power/vanish_unseen_into_shadow/on_remove()
+ if(owner)
+ owner.unignore_slowdown(type)
+ REMOVE_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, type)
+ return
+
+/atom/movable/screen/alert/status_effect/vanish_unseen_into_shadow
+ name = "Vanish Unseen Into Shadow"
+ desc = "You are undetectable through scrying and are unaffected by slowdowns."
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "blank"
diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm
new file mode 100644
index 00000000000000..617be370a53814
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm
@@ -0,0 +1,159 @@
+/* Since this is used by two different archetypes there will be a bit of snowflaking.
+Reduces stress for psykers and restores Energy for cultivators
+*/
+
+/datum/action/cooldown/power/resonant_meditate
+ name = "Resonant Meditation"
+ desc = "Restores the full potential of your resonant powers."
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "chuuni"
+
+ /// Both Cultivator and Psyker can benefit from meditate.
+ var/psyker_spotlight_color = "#ba2cc9"
+
+ /// Reference to the psyker organ, if any
+ var/obj/item/organ/resonant/psyker/psyker_organ
+ /// Reference to the cultivator energy component, if any
+ var/datum/component/cultivator_energy/cultivator_energy
+
+ /// used for the do while loop
+ var/keep_going
+
+// Makes it end meditation by clicking it again.
+/datum/action/cooldown/power/resonant_meditate/Trigger(mob/clicker, trigger_flags, atom/target)
+ if(active)
+ keep_going = FALSE
+ else
+ . = ..()
+ return TRUE
+
+/datum/action/cooldown/power/resonant_meditate/use_action()
+ . = ..()
+ keep_going = TRUE
+ var/mob/living/spotlighttarget = owner // cause we need to call it on a mob/living
+
+ to_chat(owner, span_notice("You start meditating."))
+ // Gets the owner's psyker organ & cultivator component
+ update_components()
+ // Adds visual effects
+ var/list/spotlight_config = get_meditation_spotlight_config(owner)
+ spotlighttarget.apply_status_effect(/datum/status_effect/spotlight_light/meditation, 3000, null, spotlight_config["color"], spotlight_config["emit_light"])
+ do
+ active = TRUE
+ if(do_after(owner, 25, target = owner))
+ if(user_has_active_power(owner))
+ to_chat(owner, span_notice("You have active abilities draining your resources!"))
+ keep_going = FALSE
+ break
+ if(!psyker_organ && !cultivator_energy)
+ to_chat(owner, span_notice("I have nothing to meditate on!"))
+ if(psyker_organ)
+ var/stress_recovery = PSYKER_STRESS_MEDITATION_POWER
+
+ // Checks if you have the right psyker power, otherwise reduces it to a third.
+ if(psyker_organ.has_compatible_root() && !psyker_organ.has_matching_root())
+ stress_recovery *= PSYKER_MISMATCHED_ORGAN_EFFICIENCY
+
+ psyker_organ.modify_stress(-stress_recovery)
+ if(psyker_organ.stress <= 0)
+ to_chat(owner, span_notice("I no longer feel any stress"))
+ if(cultivator_energy)
+ cultivator_energy.adjust_energy(CULTIVATOR_ENERGY_MEDITATION_POWER)
+ if(cultivator_energy.energy >= CULTIVATOR_ENERGY_MAX)
+ to_chat(owner, span_notice("My Energy is fully charged."))
+ else
+ keep_going = FALSE
+ break
+ while (keep_going)
+
+ to_chat(owner, span_notice("You stop meditating."))
+ active = FALSE
+ spotlighttarget.remove_status_effect(/datum/status_effect/spotlight_light/meditation)
+ return
+
+/// Changes the colors on meditate to whatever matches alignment.
+/datum/action/cooldown/power/resonant_meditate/proc/get_meditation_spotlight_config(mob/living/user)
+ var/list/config = list(
+ "color" = null,
+ "emit_light" = TRUE,
+ )
+ var/datum/action/cooldown/power/cultivator/alignment/alignment_action = get_alignment_action(user)
+ if(alignment_action)
+ config["color"] = alignment_action.alignment_outline_color
+ config["emit_light"] = should_alignment_spotlight_emit_light(alignment_action)
+ return config
+ if(psyker_organ) // alignment color gets priority over psyker.
+ config["color"] = psyker_spotlight_color
+ return config
+
+/// Gets the first alignment action that's used as our root.
+/datum/action/cooldown/power/resonant_meditate/proc/get_alignment_action(mob/living/user)
+ if(!user)
+ return null
+ var/datum/action/cooldown/power/cultivator/alignment/first_alignment
+ for(var/datum/action/cooldown/power/cultivator/alignment/alignment_action in user.actions)
+ if(!first_alignment)
+ first_alignment = alignment_action
+ if(alignment_action.active)
+ return alignment_action
+ return first_alignment
+
+/// Checkes if we need to emit light; basically no dark colors.
+/datum/action/cooldown/power/resonant_meditate/proc/should_alignment_spotlight_emit_light(datum/action/cooldown/power/cultivator/alignment/alignment_action)
+ if(!alignment_action)
+ return TRUE
+ var/alignment_color = alignment_action.alignment_outline_color
+ // Dark colors should not emit a light source.
+ if(alignment_color && is_color_dark(alignment_color))
+ return FALSE
+ return TRUE
+
+/// gets the psyker organ and the cultivator component
+/datum/action/cooldown/power/resonant_meditate/proc/update_components()
+ psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER)
+ cultivator_energy = owner.GetComponent(/datum/component/cultivator_energy)
+
+/// Returns TRUE if any active Cultivator or Psyker power is active on the target.
+/datum/action/cooldown/power/resonant_meditate/proc/user_has_active_power(mob/living/user)
+ if(!istype(user, /mob/living) || !user.powers)
+ return FALSE
+ for(var/datum/power/power in user.powers)
+ if(power.path != POWER_PATH_CULTIVATOR && power.path != POWER_PATH_PSYKER)
+ continue
+ var/datum/action/cooldown/power/action = power.action_path
+ if(action && action.active)
+ return TRUE
+ return FALSE
+
+// Meditation spotlight with runtime color/light config.
+/datum/status_effect/spotlight_light/meditation
+ id = "meditation_spotlight"
+ var/emit_light = TRUE
+
+/datum/status_effect/spotlight_light/meditation/on_creation(mob/living/new_owner, duration, additional_overlay, custom_spotlight_color, custom_emit_light)
+ if(!isnull(custom_spotlight_color))
+ spotlight_color = custom_spotlight_color
+ if(!isnull(custom_emit_light))
+ emit_light = custom_emit_light
+ . = ..()
+
+/datum/status_effect/spotlight_light/meditation/on_apply()
+ if(emit_light)
+ return ..()
+
+ beam_from_above_a = new /obj/effect/overlay/spotlight
+ beam_from_above_a.color = spotlight_color
+ beam_from_above_a.alpha = 62
+ owner.vis_contents += beam_from_above_a
+ beam_from_above_a.layer = BELOW_MOB_LAYER
+
+ beam_from_above_b = new /obj/effect/overlay/spotlight
+ beam_from_above_b.color = spotlight_color
+ beam_from_above_b.alpha = 62
+ beam_from_above_b.layer = ABOVE_MOB_LAYER
+ beam_from_above_b.pixel_y = -2 // Slight vertical offset for an illusion of volume.
+ owner.vis_contents += beam_from_above_b
+
+ if(additional_overlay)
+ owner.add_overlay(additional_overlay)
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_action.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_action.dm
new file mode 100644
index 00000000000000..99f3c44a81438e
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_action.dm
@@ -0,0 +1,70 @@
+/datum/action/cooldown/power/psyker
+ name = "abstract psyker power action - ahelp this"
+ background_icon_state = "bg_hive"
+ overlay_icon_state = "bg_hive_border"
+ button_icon = 'icons/mob/actions/backgrounds.dmi'
+
+ // We're a psychic we don't need hands.
+ need_hands_free = FALSE
+
+ /// The organ that processes most of the Psyker Powers. Mostly all functions here communicate with this.
+ var/obj/item/organ/resonant/psyker/psyker_organ
+
+ /// If the spell (flavorwise) affects the target's mind. So this should be FALSE for things like telekinesis but TRUE for mind reading.
+ var/mental = TRUE
+
+ /// charge cost on antimagic powers. If it has a cooldown and is non-spamable then this should be 1; otherwise keep it as is. 0 means the target isn't made aware they get targeted as well.
+ var/antimagic_charge_cost = 0
+
+/datum/action/cooldown/power/psyker/New()
+ . = ..()
+ ValidateOrgan()
+
+/// Actually checks if our Psyker Organ is there. We really want to check this every use.
+/datum/action/cooldown/power/psyker/proc/ValidateOrgan()
+ if(owner) // Prevents runtiming on start
+ psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER)
+ if(!psyker_organ)
+ return FALSE
+ return TRUE
+
+/// This doesn't actually add the stress itself; it merely tells the organ to add the stress. Validation is handled on the organ side.
+/datum/action/cooldown/power/psyker/proc/modify_stress(amount, override_cap)
+ psyker_organ.modify_stress(amount, override_cap)
+
+// We added checking for organs on try_use, as well as making sure that if we are wearing a tinfoil cap, we can't just wield our psychic powers.
+/datum/action/cooldown/power/psyker/try_use(mob/living/user, mob/living/target)
+ if(!ValidateOrgan())
+ owner.balloon_alert(owner, "No paracausal gland!")
+ return FALSE
+ // This checks against mental on the target
+ if(isliving(target) && mental && !can_affect_mental(target, antimagic_charge_cost))
+ modify_stress(PSYKER_STRESS_MINOR)
+ owner.balloon_alert(owner, "The target's mind is unreachable!")
+ to_chat(owner, span_boldnotice("The target's mind is unreachable!"))
+ return FALSE
+ . = .. ()
+
+/// Checks if the target can be affected by mental based psyker stuff, since it has its own litle list of unique immunities. Returns TRUE if the target has nothing that affects mental.
+/datum/action/cooldown/power/psyker/proc/can_affect_mental(mob/living/target, charge_cost)
+ if(!charge_cost)
+ charge_cost = antimagic_charge_cost
+ if(target.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = charge_cost))
+ return FALSE
+ if(target.can_block_magic(MAGIC_RESISTANCE, charge_cost = charge_cost))
+ return FALSE
+ if(target.can_block_resonance(charge_cost))
+ return FALSE
+ if(HAS_TRAIT(target, TRAIT_DUMB)) // this is a feature
+ return FALSE
+ return TRUE
+
+/// Checks if the target can be affected by specifically psyker's scrying
+/datum/action/cooldown/power/psyker/proc/can_affect_scrying(mob/living/target, charge_cost = 0)
+ if(!charge_cost)
+ charge_cost = antimagic_charge_cost
+ if(!can_affect_mental(target, charge_cost))
+ return FALSE
+ if(HAS_TRAIT(target, TRAIT_ANTIRESONANCE_SCRYING))
+ return FALSE
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_power.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_power.dm
new file mode 100644
index 00000000000000..8ab03775baa747
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_power.dm
@@ -0,0 +1,12 @@
+
+/datum/power/psyker_power
+ name = "Abstract Psyker Power"
+ desc = "My claivoyance lets me see into the unseen: \
+ and oh god it has shown this debug code. Please report this!"
+ abstract_parent_type = /datum/power/psyker_power
+
+ archetype = POWER_ARCHETYPE_RESONANT
+ path = POWER_PATH_PSYKER
+ priority = POWER_PRIORITY_BASIC
+ required_powers = list(/datum/power/psyker_root)
+ required_allow_subtypes = TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_root.dm
new file mode 100644
index 00000000000000..629dcfbe45a184
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_root.dm
@@ -0,0 +1,26 @@
+/*
+ Doesn't do much besides give you a grumpy organ. I prefer it gave a ribbon or at least some sort of positive, but I suppose the path of a psyker is to suffer.
+*/
+/datum/power/psyker_root
+ name = "Abstract psyker root"
+ desc = "Oh god your psyker powers have gone haywire because they aren't CODED. This backlash event is not for you; its for the coders. Please report how this happened!"
+ abstract_parent_type = /datum/power/psyker_root
+ value = 0 // all roots should be free unless they are stronger than the defaults
+ power_flags = POWER_HUMAN_ONLY
+
+ archetype = POWER_ARCHETYPE_RESONANT
+ path = POWER_PATH_PSYKER
+ priority = POWER_PRIORITY_ROOT
+
+ /// Reference to the psyker's paracausal gland organ.
+ var/obj/item/organ/resonant/psyker/psyker_organ
+ /// The organ subtype this root installs.
+ var/organ_type = /obj/item/organ/resonant/psyker
+
+/datum/power/psyker_root/add(client/client_source)
+ psyker_organ = new organ_type
+ psyker_organ.Insert(power_holder, special = TRUE)
+
+/datum/power/psyker_root/remove(client/client_source)
+ if(psyker_organ)
+ qdel(psyker_organ)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm
new file mode 100644
index 00000000000000..274ec0542ff6a4
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm
@@ -0,0 +1,102 @@
+/datum/power/psyker_power/levitate
+ name = "Levitate"
+ desc = "Grants the ability to levitate yourself above surfaces and letting you propel yourself in zero-gravity. Passively drains stress while in use."
+ security_record_text = "Subject can levitate their body regardless of the current gravity."
+ value = 4
+ required_powers = list(/datum/power/psyker_root)
+ action_path = /datum/action/cooldown/power/psyker/levitate
+
+/datum/action/cooldown/power/psyker/levitate
+ name = "Levitate"
+ desc = "Toggles levitation, causing you to ignore the ground. Also allows for propulsion in zero-gravity. Passively drains stress while in use."
+ button_icon = 'icons/mob/actions/actions_minor_antag.dmi'
+ button_icon_state = "beam_up"
+
+ mental = FALSE
+ /// Overlay we add to the caster
+ var/mutable_appearance/caster_effect
+
+/datum/action/cooldown/power/psyker/levitate/use_action()
+ . = ..()
+ if(!active)
+ owner.AddElementTrait(TRAIT_PSYKER_LEVITATE_FLIGHT, REF(src), /datum/element/forced_gravity, 0)
+ owner.AddElementTrait(TRAIT_PSYKER_LEVITATE_FLIGHT, REF(src), /datum/element/simple_flying)
+ to_chat(owner, span_boldnotice("Your body gently floats in the air!"))
+ START_PROCESSING(SSfastprocess, src)
+ active = TRUE
+ //visual fx
+ caster_effect = mutable_appearance(
+ icon = 'icons/effects/effects.dmi',
+ icon_state = "psychic",
+ layer = owner.layer - 0.1,
+ alpha = 100,
+ appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART
+ )
+ owner.add_overlay(caster_effect)
+ playsound(owner, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ else
+ REMOVE_TRAIT(owner, TRAIT_PSYKER_LEVITATE_FLIGHT, REF(src))
+ to_chat(owner, span_boldnotice("You let yourself gently drop the ground."))
+ STOP_PROCESSING(SSfastprocess, src)
+ active = FALSE
+ // visual fx
+ if(caster_effect)
+ owner.cut_overlay(caster_effect)
+ caster_effect = null
+ playsound(owner, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+
+ return TRUE
+
+/datum/action/cooldown/power/psyker/levitate/process(seconds_per_tick)
+ if(!owner)
+ STOP_PROCESSING(SSfastprocess, src)
+ return
+ //Faceplant if you get KO'd
+ if(HAS_TRAIT(owner, TRAIT_INCAPACITATED))
+ on_dispel(owner, src)
+ // Passive stress cost
+ if(active)
+ var/mob/living/carbon/human/psyker = owner
+ var/cost = PSYKER_STRESS_TRIVIAL * 1.5
+ if(psyker.get_quirk(/datum/quirk/paraplegic)) // paraplegic gets it better
+ cost = PSYKER_STRESS_TRIVIAL * 0.5
+ modify_stress(cost * seconds_per_tick)
+
+// Dispel function; basically off-switch and possibly comedic faceplant
+/datum/action/cooldown/power/psyker/levitate/Grant(mob/granted_to)
+ . = ..()
+ if(resonant)
+ RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+/datum/action/cooldown/power/psyker/levitate/Remove(mob/removed_from)
+ . = ..()
+ if(resonant)
+ UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
+
+/// Ends the effect; makes them splat if they can't catch themselves.
+/datum/action/cooldown/power/psyker/levitate/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+
+ var/mob/living/carbon/human/victim = owner
+ if(active)
+ REMOVE_TRAIT(owner, TRAIT_PSYKER_LEVITATE_FLIGHT, REF(src))
+ STOP_PROCESSING(SSfastprocess, src)
+ active = FALSE
+ // visual fx
+ if(caster_effect)
+ owner.cut_overlay(caster_effect)
+ caster_effect = null
+ playsound(owner, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+
+ // Do you have anything to brace your fall? Or do you possibly manage to get lucky?
+ var/obj/item/organ/wings/gliders = owner.get_organ_by_type(/obj/item/organ/wings)
+ if(HAS_TRAIT(owner, TRAIT_FREERUNNING) || gliders?.can_soften_fall() || prob(30))
+ to_chat(owner, span_warning("You drop to the ground, but manage to catch yourself!"))
+ else
+ to_chat(owner, span_userdanger("You drop to the ground!"))
+ playsound(owner, 'sound/effects/desecration/desecration-02.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) // Research (vibes) shows desecration-02 is the best "hit the ground"-type splat; so we're using it instead of a random desecration.
+ victim.adjustBruteLoss(5)
+ victim.Knockdown(3 SECONDS)
+ return DISPEL_RESULT_DISPELLED
+
+ return NONE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm
new file mode 100644
index 00000000000000..8c4df5eb5a4950
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm
@@ -0,0 +1,133 @@
+/*
+ I bestow upon thee my attempt to emulate telekines remoter interactions. Allows you to interact with objects from a limited distance.
+ This required three nonmodular edits:
+ - code\modules\mob\living\living.dm line 1384 to bypass the range gate.
+ - code\modules\tgui\states.dm line 128 to bypass the UI closing
+ - code\modules\mob\mob.dm line 110 to bypass the interaction gate.
+ I condensed it into TRAIT_NO_UI_DISTANCE && TRAIT_REMOTE_INTERACT so that if someone else wants to do something similar, they can.
+*/
+
+/datum/power/psyker_power/manipulate
+ name = "Manipulate"
+ desc = "Allows you to interact with machinery and various other structures within line of sight as if it were next to you. Having UIs open from a distance using this power causes stress build-up."
+ security_record_text = "Subject can psychically interact with objects from a distance."
+ security_threat = POWER_THREAT_MAJOR
+ value = 2
+ power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES
+ action_path = /datum/action/cooldown/power/psyker/manipulate
+ required_powers = list(/datum/power/psyker_power/telekinesis) //given this lets you grab items from a distance this is basically a fluff requirement to explain why you can grab objects from a distance.
+
+// Normally the golden rule is to let your action handle everything in powers; but in this case we need to actually make it so that we only have TRAIT_NO_UI_DISTANCE while we have a TK'd interface.
+/datum/power/psyker_power/manipulate/process(seconds_per_tick)
+ if(!power_holder)
+ return
+ var/datum/action/cooldown/power/psyker/manipulate/manipulate_action = action_path
+ var/ui_count = manipulate_action ? length(manipulate_action.ui_filters) : 0
+ if(ui_count)
+ ADD_TRAIT(power_holder, TRAIT_NO_UI_DISTANCE, src)
+ manipulate_action.modify_stress((PSYKER_STRESS_TRIVIAL / 2) * seconds_per_tick * ui_count) // ticks up 0.5 stress per second per ui open.
+ else
+ REMOVE_TRAIT(power_holder, TRAIT_NO_UI_DISTANCE, src)
+
+
+/datum/action/cooldown/power/psyker/manipulate
+ name = "Manipulate"
+ desc = "Allows you to interact with machinery and various other structures within line of sight as if it were next to you."
+ button_icon = 'icons/mob/actions/actions_mime.dmi'
+ button_icon_state = "invisible_box"
+
+ target_type = /obj
+ click_to_activate = TRUE
+ target_range = 12
+
+ /// Saves if its a right click so that all click interactions are routed through use_action.
+ var/right_click
+
+ /// Saved glow effects on UI elements
+ var/list/ui_filters = list()
+ /// Whitelist of types allowed to be manipulated.
+ var/static/list/target_whitelist = typecacheof(list(
+ /obj/machinery,
+ /obj/structure,
+ /obj/item/radio/intercom,
+ ))
+ /// UI blacklist for targets that should never open a UI via Manipulate.
+ var/static/list/ui_blacklist = typecacheof(list(
+ /obj/machinery/door/airlock, // opens the AI interface instead
+ ))
+
+// We're manipulating click-on to A distnguish between obj machinery and obj structure and B to distinguish between left and right hand clicks.
+/datum/action/cooldown/power/psyker/manipulate/InterceptClickOn(mob/living/clicker, params, atom/target)
+ if(!is_type_in_typecache(target, target_whitelist))
+ return FALSE
+
+ var/list/mods = params2list(params)
+ // Right click functionality.
+ if(LAZYACCESS(mods, RIGHT_CLICK))
+ right_click = TRUE
+ ..()
+
+// We use TRAIT_REMOTE_INTERACT (temporarily) as to bypass /mob/living/can_perform_action
+/datum/action/cooldown/power/psyker/manipulate/use_action(mob/living/user, atom/target)
+ ADD_TRAIT(user, TRAIT_REMOTE_INTERACT, src) // this is specifically for allowing us to bypass the range interaction gate.
+ new /obj/effect/temp_visual/telekinesis(get_turf(target))
+ if(right_click) // rmb
+ target.attack_hand_secondary(user)
+ else // lmb
+ target.attack_hand(user)
+
+ // interact with UI if present and not blacklisted.
+ var/allow_ui_interact = (target.interaction_flags_atom & INTERACT_ATOM_UI_INTERACT) && !is_type_in_typecache(target, ui_blacklist)
+ if(allow_ui_interact)
+ ADD_TRAIT(user, TRAIT_NO_UI_DISTANCE, origin_power) // we give it early so that the we count as being 'valid' before we reach the process.
+ target.ui_interact(user)
+
+ // We save the ui so we can add a filter to show it is being interacted with.
+ var/datum/tgui/ui = SStgui.get_open_ui(user, target)
+ // Some UIs (usually older computers) have different UI logic; in this case we fallback to looking at all the open UIs and trying to find it by comparing the source object.
+ if(!ui)
+ for(var/datum/tgui/candidate in user.tgui_open_uis)
+ if(!candidate?.src_object)
+ continue
+ if(candidate.src_object == target || candidate.src_object.ui_host(user) == target)
+ ui = candidate
+ break
+ if(ui)
+ var/filter_id = "manipulate_glow"
+ target.add_filter(filter_id, 1, list(type = "outline", color = "#ff66cc", size = 2))
+ var/filter = target.get_filter(filter_id)
+ if(filter)
+ animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1)
+ animate(alpha = 40, time = 2.5 SECONDS)
+ ui_filters[ui] = list(target, filter_id)
+
+ RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+ RegisterSignal(ui, COMSIG_QDELETING, PROC_REF(on_ui_closed))
+
+ REMOVE_TRAIT(user, TRAIT_REMOTE_INTERACT, src)
+ right_click = FALSE
+ modify_stress(PSYKER_STRESS_TRIVIAL * 2)
+ return TRUE
+
+/// Ends the ongoing glow effect when the UI is closed.
+/datum/action/cooldown/power/psyker/manipulate/proc/on_ui_closed(datum/tgui/ui)
+ SIGNAL_HANDLER
+ var/list/entry = ui_filters[ui]
+ if(entry)
+ var/atom/target = entry[1]
+ var/filter_id = entry[2]
+ target?.remove_filter(filter_id)
+ ui_filters -= ui
+ UnregisterSignal(target, COMSIG_ATOM_DISPEL)
+
+/// Closes any open UIs on a manipulated object.
+/datum/action/cooldown/power/psyker/manipulate/proc/on_dispel(atom/source, atom/dispeller)
+ SIGNAL_HANDLER
+ var/list/uis_to_close = list()
+ for(var/datum/tgui/ui as anything in ui_filters)
+ var/list/entry = ui_filters[ui]
+ if(entry && entry[1] == source)
+ uis_to_close += ui
+
+ for(var/datum/tgui/ui as anything in uis_to_close)
+ ui?.close()
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm
new file mode 100644
index 00000000000000..9e81992eeb9f24
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm
@@ -0,0 +1,346 @@
+// Mirage mode constants
+#define MIRAGE_MODE_STATIONARY 1
+#define MIRAGE_MODE_AGGRESSIVE 2
+#define MIRAGE_MODE_FLEE 3
+
+/*
+ Create duplicates of yourself with varying AI behaviors.
+*/
+/datum/power/psyker_power/mirage
+ name = "Mirage"
+ desc = "Creates an illusory copy of yourself for 20 seconds; it has one health and draws aggression from creatures, but doesn't deal damage and can be walked through.\
+ \n Right click with the power selected to change its behavior between stationary, aggressive and flee. Creatures immune to mental and resonant effects disbelieve the illusion, making them see-through and pass-through. \
+ \n Creating the illusion creates a moderate amount of stress."
+ security_record_text = "Subject can create illusory duplicates of themselves."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+ required_powers = list(/datum/power/psyker_root)
+ action_path = /datum/action/cooldown/power/psyker/mirage
+
+/datum/action/cooldown/power/psyker/mirage
+ name = "Mirage"
+ desc = "Creates an illusory copy of yourself for 20 seconds; it has one health and draws aggression from creatures, but doesn't deal damage and can be walked through.\
+ \n Right click with the power selected to change its behavior between stationary, aggressive and flee. Creatures immune to mental and resonant effects disbelieve the illusion, making them see-through and pass-through."
+ button_icon = 'icons/mob/actions/actions_minor_antag.dmi'
+ button_icon_state = "chrono_phase"
+ click_to_activate = TRUE
+ unset_after_click = FALSE
+
+ /// Active mirage instances
+ var/list/active_mirages = list()
+ /// Mirage behavior mode
+ var/mode = MIRAGE_MODE_STATIONARY
+ /// Stress cost
+ var/stress_cost = PSYKER_STRESS_MODERATE * 1.5
+
+// WE get the right click behavior to cycle behavior.
+/datum/action/cooldown/power/psyker/mirage/InterceptClickOn(mob/living/clicker, params, atom/target)
+ if(clicker != owner)
+ return FALSE
+
+ var/list/mods = params2list(params)
+ if(LAZYACCESS(mods, RIGHT_CLICK))
+ cycle_mode()
+ return TRUE
+
+ return ..()
+
+/datum/action/cooldown/power/psyker/mirage/use_action(mob/living/user, atom/target)
+ . = ..()
+ if(!owner)
+ return FALSE
+
+ cleanup_mirages()
+
+ var/turf/spawn_turf = get_turf(target) || get_turf(owner)
+ if(!spawn_turf)
+ return FALSE
+
+ // Creates a new instance of the mirrage
+ var/mob/living/basic/resonant_mirage/new_mirage = new(spawn_turf)
+ new_mirage.Copy_Parent(owner, 20 SECONDS)
+ new_mirage.set_action_ref(src)
+ new_mirage.apply_mode(mode)
+ active_mirages += new_mirage
+
+ // Causes it to act immediately.
+ new_mirage.taunt_nearest_hostile(5)
+ new_mirage.wake_ai()
+
+ modify_stress(stress_cost)
+ playsound(new_mirage, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+
+ return TRUE
+
+/// Changes behavior of the spawned illusion.
+/datum/action/cooldown/power/psyker/mirage/proc/cycle_mode()
+ if(mode == MIRAGE_MODE_STATIONARY)
+ mode = MIRAGE_MODE_AGGRESSIVE
+ owner?.balloon_alert(owner, "set to Aggressive")
+ else if(mode == MIRAGE_MODE_AGGRESSIVE)
+ mode = MIRAGE_MODE_FLEE
+ owner?.balloon_alert(owner, "set to Flee")
+ else
+ mode = MIRAGE_MODE_STATIONARY
+ owner?.balloon_alert(owner, "set to Stationary")
+
+/datum/action/cooldown/power/psyker/mirage/Remove(mob/removed_from)
+ . = ..()
+ for(var/mob/living/basic/resonant_mirage/mirage as anything in active_mirages)
+ if(!QDELETED(mirage))
+ qdel(mirage)
+ active_mirages.Cut()
+
+/// Removes all active mirages.
+/datum/action/cooldown/power/psyker/mirage/proc/cleanup_mirages()
+ for(var/mob/living/basic/resonant_mirage/mirage as anything in active_mirages.Copy())
+ if(QDELETED(mirage))
+ active_mirages -= mirage
+
+
+/*
+ Mirage mob: basic mob used for aggro, but with per-viewer masking.
+*/
+/mob/living/basic/resonant_mirage
+ name = "illusion"
+ desc = "It's a fake!"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "static"
+ icon_living = "static"
+ icon_dead = "null"
+ mob_biotypes = NONE
+ faction = list(FACTION_ILLUSION)
+ basic_mob_flags = DEL_ON_DEATH
+ death_message = "vanishes into thin air! It was a fake!"
+
+ health = 1
+ maxHealth = 1
+ environment_smash = ENVIRONMENT_SMASH_NONE
+
+ /// Weakref to what we're copying
+ var/datum/weakref/parent_mob_ref
+ /// Weakref to the power action
+ var/datum/weakref/action_ref
+ /// the mode that was used to summon this creature
+ var/last_mode = MIRAGE_MODE_STATIONARY
+ /// ref for the alt apperance
+ var/alt_appearance_key
+
+/// Copies stats from the parent entity that summoned it, if any.
+/mob/living/basic/resonant_mirage/proc/Copy_Parent(mob/living/original, life = 5 SECONDS)
+ appearance = original.appearance
+ gender = original.gender
+ parent_mob_ref = WEAKREF(original)
+ setDir(original.dir)
+ transform = initial(transform)
+ pixel_x = base_pixel_x
+ pixel_y = base_pixel_y
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/mob/living, death)), life)
+
+/mob/living/basic/resonant_mirage/examine(mob/user)
+ var/mob/living/parent_mob = parent_mob_ref?.resolve()
+ if(parent_mob)
+ return parent_mob.examine(user)
+ return ..()
+
+/// imposes the caster onto the mob
+/mob/living/basic/resonant_mirage/proc/set_action_ref(datum/action/cooldown/power/psyker/mirage/action)
+ action_ref = WEAKREF(action)
+ if(!alt_appearance_key)
+ alt_appearance_key = "mirage_alpha_[REF(src)]"
+ var/image/appearance_image = image(loc = src)
+ appearance_image.appearance = appearance
+ appearance_image.dir = dir
+ add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/mirage_alpha, alt_appearance_key, appearance_image, action, action?.owner)
+ RegisterSignal(src, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_mirage_dir_change))
+ RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_mirage_dispel))
+
+/// Draw a nearby hostile's aggro to sell the illusion.
+/mob/living/basic/resonant_mirage/proc/taunt_nearest_hostile(range_limit = 5)
+ var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve()
+ var/mob/living/nearest_mob
+ var/nearest_dist
+
+ // Validation, cause taunting mobs is COMPLICATED
+ for(var/mob/living/living_mob in range(range_limit, src))
+ if(living_mob == src || QDELETED(living_mob)) // no self taunting
+ continue
+ if(istype(living_mob, /mob/living/simple_animal/hostile/illusion) || istype(living_mob, /mob/living/basic/resonant_mirage)) // no taunting other illusions
+ continue
+ if(living_mob.mind) // no sentient taunting
+ continue
+ if(!islist(living_mob.faction) || (!(FACTION_HOSTILE in living_mob.faction) && !(FACTION_MINING in living_mob.faction))) // has to be in the hostile mob faction or the mining faction
+ continue
+ if(FACTION_BOSS in living_mob.faction) // "There is no aggro reset. (...) There is some shit about an aggro reset when people don't know how to manage their aggro."
+ continue
+ if(action && !action.can_affect_mental(living_mob)) // can't be immune to mental shit
+ continue
+ if(!istype(living_mob, /mob/living/simple_animal/hostile) && !living_mob.ai_controller) // either a hostile mob or has to have an ai controler
+ continue
+ var/distance = get_dist(src, living_mob)
+ if(isnull(nearest_dist) || distance < nearest_dist) // get the nearest mob in range
+ nearest_mob = living_mob
+ nearest_dist = distance
+
+ if(nearest_mob)
+ if(istype(nearest_mob, /mob/living/simple_animal/hostile)) // hostile mobs forced target
+ var/mob/living/simple_animal/hostile/hostile_mob = nearest_mob
+ hostile_mob.GiveTarget(src)
+ else if(nearest_mob.ai_controller) // otherwise we just force the blackboard to use a different target.
+ nearest_mob.ai_controller.CancelActions()
+ nearest_mob.ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET)
+ nearest_mob.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, src)
+ nearest_mob.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, src)
+
+/// Applies the selection AI mode. Have your illusions act as you please :D
+/mob/living/basic/resonant_mirage/proc/apply_mode(new_mode)
+ last_mode = new_mode
+
+ switch(new_mode)
+ if(MIRAGE_MODE_STATIONARY)
+ set_ai_controller_type(null)
+ if(MIRAGE_MODE_AGGRESSIVE)
+ set_ai_controller_type(/datum/ai_controller/basic_controller/simple/simple_hostile)
+ if(MIRAGE_MODE_FLEE)
+ set_ai_controller_type(/datum/ai_controller/basic_controller/simple/simple_fearful)
+
+/// Sets the behavior type on the AI.
+/mob/living/basic/resonant_mirage/proc/set_ai_controller_type(controller_type)
+ if(isnull(controller_type))
+ QDEL_NULL(ai_controller)
+ return
+ if(istype(ai_controller, controller_type))
+ ai_controller.reset_ai_status()
+ ai_controller.set_blackboard_key(BB_TARGETING_STRATEGY, /datum/targeting_strategy/basic/mirage)
+ return
+ QDEL_NULL(ai_controller)
+ ai_controller = new controller_type(src)
+ ai_controller.set_blackboard_key(BB_TARGETING_STRATEGY, /datum/targeting_strategy/basic/mirage)
+
+/// 'Waking it up'. When this was a simple animal this wasn't as big of a problem, but /basic/ mobs are just more sluggish and with mirrages being meant to divert aggro, we want them reacting asap.
+/mob/living/basic/resonant_mirage/proc/wake_ai()
+ if(!ai_controller)
+ return
+ ai_controller.set_ai_status(AI_STATUS_ON)
+ ai_controller.SelectBehaviors(0.1)
+ for(var/datum/ai_behavior/current_behavior as anything in ai_controller.current_behaviors)
+ ai_controller.ProcessBehavior(0.1, current_behavior)
+
+/mob/living/basic/resonant_mirage/Destroy()
+ if(alt_appearance_key)
+ remove_alt_appearance(alt_appearance_key)
+ alt_appearance_key = null
+ UnregisterSignal(src, COMSIG_ATOM_DIR_CHANGE)
+ UnregisterSignal(src, COMSIG_ATOM_DISPEL)
+ action_ref = null
+ return ..()
+
+/// On dispel, poofs the mirrage.
+/mob/living/basic/resonant_mirage/proc/on_mirage_dispel(datum/source, atom/dispeller)
+ SIGNAL_HANDLER
+ qdel(src)
+ return DISPEL_RESULT_DISPELLED
+
+/// We need to tell the alt appearance variant to turn.
+/mob/living/basic/resonant_mirage/proc/on_mirage_dir_change(datum/source, old_dir, new_dir)
+ SIGNAL_HANDLER
+ var/image/appearance_image = hud_list?[alt_appearance_key]
+ if(appearance_image)
+ appearance_image.dir = new_dir
+
+/// If you have disbelieved the illusion (immune to mental) you can just walk through them.
+/mob/living/basic/resonant_mirage/CanAllowThrough(atom/movable/mover, border_dir)
+ if(should_ignore_target(mover))
+ return TRUE
+ return ..()
+
+/// Basically we check if they're our owner, are affected by mental or are an illusion of the same mob.
+/mob/living/basic/resonant_mirage/proc/should_ignore_target(atom/target)
+ var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve()
+ if(!action || !ismob(target) || !isliving(target))
+ return FALSE
+ var/mob/living/living_target = target
+ var/mob/living/owner = action.owner
+ if(owner && living_target == owner) // owner
+ return TRUE
+ if(!action.can_affect_mental(living_target)) // magic immune
+ return TRUE
+ if(istype(living_target, /mob/living/basic/resonant_mirage))
+ var/mob/living/basic/resonant_mirage/illusion_target = living_target
+ if(illusion_target.parent_mob_ref?.resolve() == owner)
+ return TRUE
+ return FALSE
+
+/// We basically do a fake attack to sell the 'illusion'. We don't want it to actually deal damage, or people will have hissyfit arguments that these are 'harmful' and should be 'illegal'
+/mob/living/basic/resonant_mirage/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE)
+ if(!isliving(target))
+ return FALSE
+ if(should_ignore_target(target))
+ return FALSE
+ if(!early_melee_attack(target, modifiers, ignore_cooldown))
+ return FALSE
+
+ var/mob/living/living_target = target
+ do_attack_animation(living_target, ATTACK_EFFECT_PUNCH)
+
+ var/verb_continuous = attack_verb_continuous || "attacks"
+ var/verb_simple = attack_verb_simple || "attack"
+
+ visible_message(
+ span_danger("[src] [verb_continuous] [living_target]!"),
+ span_userdanger("[src] [verb_continuous] you!"),
+ null,
+ COMBAT_MESSAGE_RANGE,
+ src
+ )
+ to_chat(src, span_danger("You [verb_simple] [living_target]!"))
+
+ if(attacked_sound)
+ playsound(loc, attacked_sound, 25, TRUE, -1)
+
+ SEND_SIGNAL(src, COMSIG_HOSTILE_POST_ATTACKINGTARGET, target, TRUE)
+ return TRUE
+
+/// Targeting strategy: never pick targets the mirage should ignore.
+/datum/targeting_strategy/basic/mirage/can_attack(mob/living/living_mob, atom/target, vision_range)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!istype(living_mob, /mob/living/basic/resonant_mirage))
+ return .
+ var/mob/living/basic/resonant_mirage/mirage = living_mob
+ if(mirage.should_ignore_target(target))
+ return FALSE
+ return TRUE
+
+
+/// Alternate appearance for mirage: semi-transparent for owner and mental-immune viewers.
+/datum/atom_hud/alternate_appearance/basic/mirage_alpha
+ /// Reference to the power action
+ var/datum/weakref/action_ref
+ /// Reference to the power action owner
+ var/datum/weakref/owner_ref
+ /// Alpha percentage on the mob alt appearance
+ var/alpha_override = 80
+
+/datum/atom_hud/alternate_appearance/basic/mirage_alpha/New(key, image/appearance_image, datum/action/cooldown/power/psyker/mirage/action, mob/living/owner, options = AA_TARGET_SEE_APPEARANCE)
+ action_ref = WEAKREF(action)
+ owner_ref = WEAKREF(owner)
+ if(appearance_image)
+ appearance_image.alpha = alpha_override
+ appearance_image.override = TRUE
+ . = ..(key, appearance_image, options)
+
+/// Who is ALLOWED to see us for who we truly are?
+/datum/atom_hud/alternate_appearance/basic/mirage_alpha/mobShouldSee(mob/viewer)
+ var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve()
+ if(!action || !ismob(viewer) || !isliving(viewer))
+ return FALSE
+ var/mob/living/owner = owner_ref?.resolve()
+ if(owner && viewer == owner)
+ return TRUE
+ return !action.can_affect_mental(viewer)
+
+#undef MIRAGE_MODE_STATIONARY
+#undef MIRAGE_MODE_AGGRESSIVE
+#undef MIRAGE_MODE_FLEE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm
new file mode 100644
index 00000000000000..8f4e10152d9bd1
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm
@@ -0,0 +1,144 @@
+/**
+ Old wife tale of sneezing when your name is mentioned.
+**/
+/datum/power/psyker_power/premonition
+ name = "Premonition"
+ desc = "You are aware when a particular something is mentioned; a hunch as it were.\
+ \n Select a specific word or phrase; anytime someone mentions it (no matter where they are), you will trigger the chosen emote. Has a cooldown of 10 seconds."
+ security_record_text = "Subject has strange bodily reactions whenever a certain keyphrase is mentioned."
+ value = 2
+
+ /// Trakcs the component
+ var/datum/component/beetlejuice/premonition/premonition_component
+
+// Adds the special beetlejuice component, gets the prefernece components.
+/datum/power/psyker_power/premonition/post_add()
+ if(!power_holder)
+ return
+
+ // Gets the holder and component
+ var/mob/living/holder = power_holder
+ var/datum/component/beetlejuice/premonition/component = holder.GetComponent(/datum/component/beetlejuice/premonition)
+ if(!component)
+ component = holder.AddComponent(/datum/component/beetlejuice/premonition)
+
+ premonition_component = component
+
+ // Sets the word of the day.
+ var/keyword = holder.client?.prefs?.read_preference(/datum/preference/text/premonition_keyword)
+ if(!keyword)
+ var/datum/preference/text/premonition_keyword/pref_entry = GLOB.preference_entries[/datum/preference/text/premonition_keyword]
+ keyword = pref_entry?.create_default_value() || "Beetlejuice"
+
+ component.keyword = keyword
+ component.update_regex()
+
+ // Sets the emote key.
+ var/emote_choice = holder.client?.prefs?.read_preference(/datum/preference/choiced/premonition_emote)
+ var/datum/preference/choiced/premonition_emote/pref_entry = GLOB.preference_entries[/datum/preference/choiced/premonition_emote]
+ component.emote_key = pref_entry?.validate_premonition_emote_choice(emote_choice) || "sneeze"
+ . = ..()
+
+/datum/power/psyker_power/premonition/remove()
+ . = ..()
+ if(premonition_component)
+ QDEL_NULL(premonition_component)
+
+// Custom beetlejuice component for Premonition.
+/datum/component/beetlejuice/premonition
+ min_count = 1
+ cooldown = 10 SECONDS
+ var/emote_key = "sneeze"
+
+// When the phrase is mentioned.
+/datum/component/beetlejuice/premonition/apport(atom/target)
+ var/atom/movable/triggered = parent
+ if(!ismob(triggered))
+ return
+ var/mob/living/living_triggered = triggered
+ if(HAS_TRAIT(living_triggered, TRAIT_RESONANCE_SILENCED))
+ return
+ if(!emote_key)
+ return
+ living_triggered.emote(emote_key, intentional = FALSE)
+ active = FALSE
+ addtimer(VARSET_CALLBACK(src, active, TRUE), cooldown)
+
+// Preference choice for Premonition keyword selection.
+/datum/preference/text/premonition_keyword
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "premonition_keyword"
+ savefile_identifier = PREFERENCE_CHARACTER
+ can_randomize = FALSE
+ maximum_value_length = 32
+
+/datum/preference/text/premonition_keyword/create_default_value()
+ return "Beetlejuice"
+
+/datum/preference/text/premonition_keyword/is_valid(value)
+ if(!istext(value))
+ return FALSE
+ if(length(value) < 1 || length(value) >= maximum_value_length)
+ return FALSE
+ // Allow only ASCII letters, numbers, and spaces.
+ var/quoted = REGEX_QUOTE(value)
+ var/static/regex/allowed_regex = regex("^" + ascii2text(91) + "A-Za-z0-9 " + ascii2text(93) + "+$")
+ allowed_regex.next = 1
+ return !!allowed_regex.Find(quoted)
+
+/datum/preference/text/premonition_keyword/deserialize(input, datum/preferences/preferences)
+ var/value = ..()
+ if(!is_valid(value))
+ return null
+ return value
+
+/datum/preference/text/premonition_keyword/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+// Preference choice for Premonition emote selection.
+/datum/preference/choiced/premonition_emote
+ category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
+ savefile_key = "premonition_emote"
+ savefile_identifier = PREFERENCE_CHARACTER
+
+/datum/preference/choiced/premonition_emote/create_default_value()
+ return validate_premonition_emote_choice("sneeze")
+
+/datum/preference/choiced/premonition_emote/init_possible_values()
+ return get_premonition_emote_choices()
+
+/datum/preference/choiced/premonition_emote/is_accessible(datum/preferences/preferences)
+ if (!..(preferences))
+ return FALSE
+ return TRUE
+
+/datum/preference/choiced/premonition_emote/apply_to_human(mob/living/carbon/human/target, value)
+ return
+
+/// Gets the list of emotes available for preminition using the global emote list.
+/datum/preference/choiced/premonition_emote/proc/get_premonition_emote_choices()
+ var/list/choices = list()
+ for(var/key in GLOB.emote_list)
+ for(var/datum/emote/emote_action in GLOB.emote_list[key])
+ if(emote_action.key == key)
+ choices += key
+ break
+ if(!length(choices))
+ return list("sneeze")
+ return sort_list(choices)
+
+/// Makes sure that the chosen emote is actually in the emote list and not just some random-thing you made up.
+/datum/preference/choiced/premonition_emote/proc/validate_premonition_emote_choice(value)
+ if(!istext(value))
+ value = null
+ var/list/choices = get_premonition_emote_choices()
+ if(value && (value in choices))
+ return value
+ return choices[1]
+
+/datum/power_constant_data/premonition
+ associated_typepath = /datum/power/psyker_power/premonition
+ customization_options = list(
+ /datum/preference/text/premonition_keyword,
+ /datum/preference/choiced/premonition_emote
+ )
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm
new file mode 100644
index 00000000000000..0311a548c066c9
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm
@@ -0,0 +1,31 @@
+// Psyker Events happen when your stress reaches the threshold. Specifically, 1x the stress_threshold, 1.5x for severe and 2x for catastrohpic.
+// There is a 20% of substituting a catastrophic event for a special event. These aren't necessarily always better, just a lot weirder.
+// Any psyker_event you define is added to the lists unless it is abstract.
+
+/datum/psyker_event
+ // Remember to set abstracts to this.
+ abstract_type = /datum/psyker_event
+ /// check defines for weights.
+ var/weight = PSYKER_EVENT_RARITY_COMMON
+ /// For events that continue for a while, this skips the qdel step. MAKE SURE YOU QDEL IT YOURSELF LATER INSIDE THE CODE.
+ var/lingering = FALSE
+
+/// Are there any special prerequisites?
+/datum/psyker_event/proc/can_execute(mob/living/carbon/human/psyker)
+ return TRUE
+
+/// Return TRUE if the event actually happens, FALSE if it doesnt and should be skipped
+/datum/psyker_event/proc/execute(mob/living/carbon/human/psyker)
+ return FALSE
+
+/// Milds generally want to not take you out of the flow but be noticeable enough that someone paying attention will notice they're pushing the line.
+/datum/psyker_event/mild
+ abstract_type = /datum/psyker_event/mild
+
+/// Severe are the very clear warning to stop. These should be obvious and detrimental, with a clear goal of making it so that you stop and meditate or face the consequences.
+/datum/psyker_event/severe
+ abstract_type = /datum/psyker_event/severe
+
+/// The consequences of your actions. Usually things that demand an immediate medbay visit or leave lingering consequences for the Psyker.
+/datum/psyker_event/catastrophic
+ abstract_type = /datum/psyker_event/catastrophic
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm
new file mode 100644
index 00000000000000..47eab50fa034a9
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm
@@ -0,0 +1,11 @@
+// Gives deep-rooted normal traumas.
+/datum/psyker_event/catastrophic/brain_trauma
+ weight = PSYKER_EVENT_RARITY_UNCOMMON
+
+/datum/psyker_event/catastrophic/brain_trauma/execute(mob/living/carbon/human/psyker)
+ if(!psyker.gain_trauma_type(BRAIN_TRAUMA_SEVERE, TRAUMA_RESILIENCE_LOBOTOMY))
+ // If we somehow fail to give them the trauma
+ return FALSE
+ //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them.
+ to_chat(psyker, span_userdanger(PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE))
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm
new file mode 100644
index 00000000000000..db01856688d720
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm
@@ -0,0 +1,11 @@
+/datum/psyker_event/catastrophic/heart_attack
+
+/datum/psyker_event/catastrophic/heart_attack/execute(mob/living/carbon/human/psyker)
+ if(!psyker.can_heartattack() && !psyker.undergoing_cardiac_arrest()) // Can the target have a heartattack? And if so, are they already undergoing a heartattack?
+ return FALSE
+ psyker.apply_status_effect(/datum/status_effect/heart_attack)
+ //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them.
+ to_chat(psyker, span_userdanger(PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE))
+
+ return TRUE
+
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/magic_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/magic_trauma.dm
new file mode 100644
index 00000000000000..f97d6eda38f96e
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/magic_trauma.dm
@@ -0,0 +1,20 @@
+// Gives one of the wizard's magical traumas.
+/datum/psyker_event/catastrophic/magic_trauma
+ weight = PSYKER_EVENT_RARITY_RARE
+
+/datum/psyker_event/catastrophic/magic_trauma/execute(mob/living/carbon/human/psyker)
+ var/datum/brain_trauma/magic/trauma
+ if(prob(65)) // Poltergeists are a bit more thematic so they're a tad more common.
+ trauma = new /datum/brain_trauma/magic/poltergeist
+ else // Gets you the stalker, which is even spookier (and bothersome)
+ trauma = new /datum/brain_trauma/magic/stalker
+ // We are also not going to tell them they got a trauma.
+ trauma.gain_text = null
+
+ if(!psyker.gain_trauma(trauma))
+ // If we somehow fail to give them the trauma
+ QDEL_NULL(trauma)
+ return FALSE
+ //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them.
+ to_chat(psyker, span_userdanger(PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE))
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm
new file mode 100644
index 00000000000000..6937f1ac8e018e
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm
@@ -0,0 +1,193 @@
+/// Summons copies of yourself to beat the snot out of you; or harrass others if you dare to run away. Unlike normal mirrages, these do hurt you. What hurts more is the social fuax pas of copies of yourself beating someoen else up.
+/datum/psyker_event/catastrophic/mirage_gangup
+ lingering = TRUE
+ weight = PSYKER_EVENT_RARITY_RARE // this shouldn't be too common since this inconveniences others as well
+ /// How many mirages to spawn
+ var/spawn_count = 8
+ /// Range around the psyker to spawn, in tiles
+ var/spawn_range = 3
+ /// Lifetime of each mirage
+ var/mirage_lifetime = 20 SECONDS
+
+/datum/psyker_event/catastrophic/mirage_gangup/execute(mob/living/carbon/human/psyker)
+ to_chat(psyker, span_userdanger("Your Resonant powers send your mind spiraling; everyone is looking like you, and at you!"))
+ psyker.cause_hallucination(/datum/hallucination/delusion/psyker_gangup, "psyker mirage gangup", duration = mirage_lifetime, psyker_owner = psyker)
+
+ // Spawn a large semblence of illusions to heckle and harass us.
+ for(var/iteration = 0; iteration < spawn_count; iteration++)
+ addtimer(CALLBACK(src, PROC_REF(_spawn_gangup_mirage), psyker), iteration SECONDS)
+
+ return TRUE
+
+/// Creates a mirrage specific to gang-up, copies the parent mob and tells it to GO GET THE FUKKEN PSYKER
+/datum/psyker_event/catastrophic/mirage_gangup/proc/_spawn_gangup_mirage(mob/living/carbon/human/psyker)
+ if(!psyker || QDELETED(psyker))
+ return
+
+ var/turf/spawn_turf = pick_spawn_turf(psyker)
+ if(!spawn_turf)
+ return
+
+ var/mob/living/basic/mirage_gangup/new_mirage = new(spawn_turf)
+ new_mirage.Copy_Parent(psyker, mirage_lifetime)
+ new_mirage.aggro_on(psyker)
+
+/// Find an appropriate space to create our mirrages
+/datum/psyker_event/catastrophic/mirage_gangup/proc/pick_spawn_turf(mob/living/psyker)
+ var/list/valid_turfs = list()
+ for(var/turf/turf_candidate in view(spawn_range, psyker))
+ if(!isopenturf(turf_candidate))
+ continue
+ if(turf_candidate.is_blocked_turf(exclude_mobs = TRUE))
+ continue
+ valid_turfs += turf_candidate
+
+ if(length(valid_turfs))
+ return pick(valid_turfs)
+
+ return get_turf(psyker)
+
+
+/// Mirage mob used by the gangup event. These are tougher and hit harder. We don't subtype the basic mirage because it has so much overhead with taunting
+/mob/living/basic/mirage_gangup
+ name = "mirage"
+ desc = "An illusory copy turned deadly."
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "static"
+ icon_living = "static"
+ icon_dead = "null"
+ mob_biotypes = NONE
+ faction = list(FACTION_ILLUSION)
+ basic_mob_flags = DEL_ON_DEATH
+ death_message = "dissipates into thin air!"
+
+ health = 50
+ maxHealth = 50
+ melee_damage_lower = 10
+ melee_damage_upper = 10
+ environment_smash = ENVIRONMENT_SMASH_NONE
+ attack_sound = 'sound/items/weapons/punch1.ogg'
+ ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile // WHAT DO YOU MEAN THERE'S NO STANDARD AI CONTROLLER?
+
+ /// Weakref to what we're copying
+ var/datum/weakref/parent_mob_ref
+ /// ref for the alt appearance
+ var/alt_appearance_key
+
+/// Copies stats from the parent entity that summoned it, if any.
+/mob/living/basic/mirage_gangup/proc/Copy_Parent(mob/living/original, life = 20 SECONDS)
+ appearance = original.appearance
+ name = original.name
+ real_name = original.real_name
+ gender = original.gender
+ parent_mob_ref = WEAKREF(original)
+ setDir(original.dir)
+ transform = initial(transform)
+ pixel_x = base_pixel_x
+ pixel_y = base_pixel_y
+ _setup_alt_appearance(original)
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/mob/living, death)), life)
+
+/// Force this mirage to focus the psyker.
+/mob/living/basic/mirage_gangup/proc/aggro_on(mob/living/target)
+ if(!target || QDELETED(target) || !ai_controller)
+ return
+ if(ispath(ai_controller))
+ ai_controller = new ai_controller(src)
+ ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, target)
+ ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, target)
+ ai_controller.set_ai_status(AI_STATUS_ON)
+ ai_controller.SelectBehaviors(0.1)
+
+/// Set up the alternate appearance so the psyker and mental-immune viewers see through it. Also sneaks in the dispel signaler.
+/mob/living/basic/mirage_gangup/proc/_setup_alt_appearance(mob/living/owner)
+ if(alt_appearance_key)
+ return
+ alt_appearance_key = "mirage_gangup_static_[REF(src)]"
+ var/image/appearance_image = image('icons/effects/effects.dmi', src, "static")
+ appearance_image.dir = dir
+ add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/mirage_gangup_static, alt_appearance_key, appearance_image, owner)
+ RegisterSignal(src, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_mirage_dir_change))
+ RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_mirage_dispel))
+
+/mob/living/basic/mirage_gangup/Destroy()
+ if(alt_appearance_key)
+ remove_alt_appearance(alt_appearance_key)
+ alt_appearance_key = null
+ UnregisterSignal(src, COMSIG_ATOM_DIR_CHANGE)
+ UnregisterSignal(src, COMSIG_ATOM_DISPEL)
+ return ..()
+
+/// Poofs the mob on dispel
+/mob/living/basic/mirage_gangup/proc/on_mirage_dispel(datum/source, atom/dispeller)
+ SIGNAL_HANDLER
+ qdel(src)
+ return DISPEL_RESULT_DISPELLED
+
+/// Actually makes mirage sprites rotate.
+/mob/living/basic/mirage_gangup/proc/on_mirage_dir_change(datum/source, old_dir, new_dir)
+ SIGNAL_HANDLER
+ var/image/appearance_image = hud_list?[alt_appearance_key]
+ if(appearance_image)
+ appearance_image.dir = new_dir
+
+/// Alternate appearance for mirage gangup: static outline for mental-immune viewers to show that they are in fact hostile mobs.
+/datum/atom_hud/alternate_appearance/basic/mirage_gangup_static
+ var/datum/weakref/owner_ref
+
+/datum/atom_hud/alternate_appearance/basic/mirage_gangup_static/New(key, image/appearance_image, mob/living/owner, options = AA_TARGET_SEE_APPEARANCE)
+ owner_ref = WEAKREF(owner)
+ if(appearance_image)
+ appearance_image.override = TRUE
+ . = ..(key, appearance_image, options)
+
+/// Who is ALLOWED to see us for who we truly are?
+/datum/atom_hud/alternate_appearance/basic/mirage_gangup_static/mobShouldSee(mob/viewer)
+ if(!ismob(viewer) || !isliving(viewer))
+ return FALSE
+ var/mob/living/owner = owner_ref?.resolve()
+ if(owner && viewer == owner)
+ return FALSE
+ return !can_affect_mental(viewer)
+
+/// Validates if the target is affected by mental effects.
+/datum/atom_hud/alternate_appearance/basic/mirage_gangup_static/proc/can_affect_mental(mob/living/target)
+ if(!target)
+ return FALSE
+ if(target.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0))
+ return FALSE
+ if(target.can_block_magic(MAGIC_RESISTANCE, charge_cost = 0))
+ return FALSE
+ if(target.can_block_resonance(0))
+ return FALSE
+ if(HAS_TRAIT(target, TRAIT_DUMB))
+ return FALSE
+ return TRUE
+
+/// Delusion: everyone looks like the psyker (for the psyker only).
+/datum/hallucination/delusion/psyker_gangup
+ random_hallucination_weight = 0
+ affects_us = FALSE
+ affects_others = TRUE
+ delusion_name = "psyker"
+ /// Who we're copying
+ var/datum/weakref/psyker_ref
+
+/datum/hallucination/delusion/psyker_gangup/New(mob/living/hallucinator, duration, mob/living/psyker_owner)
+ if(psyker_owner)
+ psyker_ref = WEAKREF(psyker_owner)
+ delusion_name = psyker_owner.name
+ return ..(hallucinator, duration)
+
+// override just to pass along psyker_owner
+/datum/hallucination/delusion/psyker_gangup/make_delusion_image(mob/over_who)
+ var/image/funny_image = image(loc = over_who)
+ var/mob/living/psyker_owner = psyker_ref?.resolve()
+ if(psyker_owner)
+ funny_image.appearance = psyker_owner.appearance
+ else
+ funny_image.appearance = over_who.appearance
+ funny_image.name = delusion_name
+ funny_image.override = TRUE
+ SET_PLANE_EXPLICIT(funny_image, ABOVE_GAME_PLANE, over_who)
+ return funny_image
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm
new file mode 100644
index 00000000000000..3c7a6932bf868d
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm
@@ -0,0 +1,15 @@
+// Gives a special deep-rooted trauma that silences Resonance powers all-together.
+/datum/psyker_event/catastrophic/silence_trauma
+ weight = PSYKER_EVENT_RARITY_RARE
+
+/datum/psyker_event/catastrophic/silence_trauma/execute(mob/living/carbon/human/psyker)
+ var/datum/brain_trauma/magic/trauma = new /datum/brain_trauma/magic/resonance_silenced
+ trauma.gain_text = null
+ if(!psyker.gain_trauma(trauma))
+ // If we somehow fail to give them the trauma
+ QDEL_NULL(trauma)
+ return FALSE
+ // We replicate the trauma message just in a different span.
+ to_chat(psyker, span_userdanger("You feel like you're no longer in touch with your own Resonant powers."))
+ return TRUE
+
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm
new file mode 100644
index 00000000000000..d773c0021d5cf7
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm
@@ -0,0 +1,95 @@
+// Resonant forces batter and wound your body. This one will always return TRUE, and is probably the deadliest.
+/datum/psyker_event/catastrophic/telekinetic_backlash
+ lingering = TRUE
+ /// I guess we'll have a pity system.
+ var/max_ticks = 6
+
+ /// Brute damage on a moderete severity roll
+ var/moderate_brute = 5
+ /// Damage on a severe severity roll
+ var/severe_brute = 10
+ /// Damage on a critical severity roll
+ var/critical_brute = 20
+
+ weight = PSYKER_EVENT_RARITY_UNCOMMON
+
+/datum/psyker_event/catastrophic/telekinetic_backlash/execute(mob/living/carbon/human/psyker)
+ to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you feel yourself wracked by pain, as your skin, bones and flesh are pulled in all manner of directions!"))
+
+ // Start the chain after ~1 second
+ addtimer(CALLBACK(src, PROC_REF(_backlash_tick), psyker, 0), 1 SECONDS)
+
+ return TRUE
+
+/// Every tick we do horrible things to the mob; then check if we should do another tick.
+/datum/psyker_event/catastrophic/telekinetic_backlash/proc/_backlash_tick(mob/living/carbon/human/psyker, tick_count)
+ if(!psyker || QDELETED(psyker))
+ qdel(src)
+ return
+
+ if(tick_count >= max_ticks)
+ qdel(src)
+ return
+
+ var/obj/item/bodypart/target_limb = pick_wound_bodypart(psyker)
+ if(!target_limb)
+ qdel(src)
+ return
+
+ //What wound type we apply for this instance.
+ var/wound_type = pick(WOUND_SLASH, WOUND_PIERCE, WOUND_BLUNT)
+
+ // Roll which effect happens this tick (65/20/10/5)
+ var/roll = rand(1, 100)
+
+ if(roll <= 65)
+ to_chat(psyker, span_warning("Your body lurches as invisible forces wrench at your flesh!"))
+ psyker.apply_damage(moderate_brute, BRUTE, def_zone = target_limb.body_zone)
+ psyker.cause_wound_of_type_and_severity(wound_type, target_limb, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_MODERATE)
+ else if(roll <= 85)
+ to_chat(psyker, span_danger("You feel something tear inside you as the force twists harder!"))
+ psyker.apply_damage(severe_brute, BRUTE, def_zone = target_limb.body_zone)
+ psyker.cause_wound_of_type_and_severity(wound_type, target_limb, WOUND_SEVERITY_SEVERE, WOUND_SEVERITY_CRITICAL)
+ else if(roll <= 95)
+ to_chat(psyker, span_userdanger("Agony spikes through you as feel your body being ripped apart!"))
+ psyker.apply_damage(critical_brute, BRUTE, def_zone = target_limb.body_zone)
+ psyker.cause_wound_of_type_and_severity(wound_type, target_limb, WOUND_SEVERITY_CRITICAL, WOUND_SEVERITY_CRITICAL)
+ psyker.emote("scream")
+ else
+ // MY LEG!
+ var/obj/item/bodypart/part = pick_wound_bodypart(psyker, FALSE)
+ if(part)
+ part.dismember()
+ to_chat(psyker, span_userdanger("Something gives way—your body can't hold together!"))
+ psyker.emote("scream")
+
+ // 75% chance to continue applying effects
+ if(!prob(75))
+ qdel(src)
+ return
+
+ // Schedule next tick in ~1 second
+ addtimer(CALLBACK(src, PROC_REF(_backlash_tick), psyker, tick_count + 1), 1 SECONDS)
+
+/// Picks a bodypart to wound.
+/datum/psyker_event/catastrophic/telekinetic_backlash/proc/pick_wound_bodypart(mob/living/carbon/human/psyker, allow_vital = TRUE)
+ if(!psyker || !length(psyker.bodyparts))
+ return null
+
+ var/list/candidates = list()
+ for(var/obj/item/bodypart/bodypart as anything in psyker.bodyparts)
+ // Skip missing/destroyed parts if your fork tracks those (optional safety)
+ if(QDELETED(bodypart))
+ continue
+
+ // Avoid vital zones unless explicitly allowed
+ if(!allow_vital)
+ if(bodypart.body_zone == BODY_ZONE_HEAD || bodypart.body_zone == BODY_ZONE_CHEST)
+ continue
+
+ candidates += bodypart
+
+ if(!length(candidates))
+ return null
+
+ return pick(candidates)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm
new file mode 100644
index 00000000000000..c29600a9cacb87
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm
@@ -0,0 +1,124 @@
+/// Tosses you around physically into various dangerous objects.
+/datum/psyker_event/catastrophic/tossed_around
+ lingering = TRUE
+ weight = PSYKER_EVENT_RARITY_UNCOMMON
+
+ /// Pity system
+ var/max_ticks = 20
+
+ /// The throw range
+ var/throw_range = 10
+ /// The throw speed
+ var/throw_speed = 3
+
+ /// Hand-made list of objects we prefer to smash people into, and will default to when throwing. Should only contain items with funny effects when thrown into them.
+ var/list/special_object_types = list(
+ /turf/open/chasm, // this one's evil
+ /turf/open/space,
+ /turf/open/lava, // how cooked do you want your spaceman
+ /turf/open/floor/tram/plate, // THE TRAM CALLS
+ /obj/structure/table/glass,
+ /obj/structure/window,
+ /obj/structure/grille,
+ /obj/machinery/teleport/hub,
+ /obj/machinery/vending, // we have a special interaction where there is a chance they're knocked over.
+ /obj/structure/musician, // they make funny noises
+ /obj/machinery/disposal, // flushes you too
+ /obj/machinery/power/supermatter_crystal, // if you break down next to a SM crystal you deserve this
+ /mob/living,
+
+ )
+
+ /// typecach for reference sake
+ var/static/list/special_object_typecache
+ /// Track impact handling for this event
+ var/mob/living/carbon/human/impact_owner
+ /// Are we expecting the mob to impact a surface.
+ var/expecting_impact = FALSE
+
+/datum/psyker_event/catastrophic/tossed_around/execute(mob/living/carbon/human/psyker)
+ to_chat(psyker, span_userdanger("Your Resonant powers send you hurling through the air!"))
+ RegisterSignal(psyker, COMSIG_MOVABLE_IMPACT, PROC_REF(on_toss_impact))
+ impact_owner = psyker
+ addtimer(CALLBACK(src, PROC_REF(_toss_tick), psyker, 0), 1 SECONDS)
+ return TRUE
+
+/// Every tick, we try to fling the mob at dangerous things; or in random directs, and then determine if we want to do it AGAIN
+/datum/psyker_event/catastrophic/tossed_around/proc/_toss_tick(mob/living/carbon/human/psyker, tick_count)
+ if(!psyker || QDELETED(psyker))
+ qdel(src)
+ return
+ if(tick_count >= max_ticks)
+ qdel(src)
+ return
+
+ // no escape
+ psyker.Knockdown(3 SECONDS)
+
+ if(!special_object_typecache)
+ special_object_typecache = typecacheof(special_object_types)
+
+ var/list/nearby_specials = typecache_filter_list(oview(throw_range, psyker), special_object_typecache)
+ var/list/valid_specials = list()
+
+ for(var/atom/special as anything in nearby_specials)
+ if(special == psyker) // makes it so mob/living doesnt throw the psyker at themselves
+ continue
+ if(can_see(psyker, special, throw_range))
+ valid_specials += special
+
+ var/turf/target_turf
+ var/atom/target_special
+ if(length(valid_specials)) // if we have special things to throw people at
+ target_special = pick(valid_specials)
+ target_turf = get_turf(target_special)
+ else // if we don't: just toss them somewhere random
+ target_turf = get_ranged_target_turf(psyker, pick(GLOB.alldirs), throw_range)
+
+ var/datum/callback/throw_callback
+ if(target_turf) // YEET!
+ psyker.throw_at(target_turf, range = throw_range, speed = throw_speed, thrower = psyker, spin = TRUE, callback = throw_callback)
+
+ // 95% chance to continue applying effects
+ if(!prob(95))
+ qdel(src)
+ return
+
+ addtimer(CALLBACK(src, PROC_REF(_toss_tick), psyker, tick_count + 1), 1 SECONDS)
+
+/// Forcefully flushes disposals on impact
+/datum/psyker_event/catastrophic/tossed_around/proc/flush_disposal(mob/living/carbon/human/psyker, obj/machinery/disposal/target_disposal)
+ if(!psyker || QDELETED(psyker) || !target_disposal || QDELETED(target_disposal))
+ return
+ target_disposal.flush()
+ return
+
+/// Runs a variety of on-hit effects when tossed into a surface.
+/datum/psyker_event/catastrophic/tossed_around/proc/on_toss_impact(atom/movable/source, atom/hit_atom, datum/thrownthing/throwingdatum)
+ SIGNAL_HANDLER
+
+ var/mob/living/carbon/human/psyker = source
+ if(!psyker || QDELETED(psyker))
+ return
+
+ // At least 5 brute on any impact
+ psyker.apply_damage(5, BRUTE)
+
+ // If we hit a disposal bin, force the mob into it and flush.
+ if(istype(hit_atom, /obj/machinery/disposal))
+ var/obj/machinery/disposal/target_disposal = hit_atom
+ if(psyker.loc != target_disposal)
+ psyker.forceMove(target_disposal)
+ target_disposal.update_appearance()
+ target_disposal.flush = TRUE
+ // If we hit a vending machine, give it a chance to knock over onto the psyker.
+ else if(istype(hit_atom, /obj/machinery/vending))
+ if(prob(50))
+ var/obj/machinery/vending/vendor = hit_atom
+ vendor.tilt(psyker)
+
+/datum/psyker_event/catastrophic/tossed_around/Destroy()
+ if(impact_owner)
+ UnregisterSignal(impact_owner, COMSIG_MOVABLE_IMPACT)
+ impact_owner = null
+ return ..()
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/dizziness.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/dizziness.dm
new file mode 100644
index 00000000000000..576c0f9784feb7
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/dizziness.dm
@@ -0,0 +1,7 @@
+// A mild dizzy, but enough to be noticed.
+/datum/psyker_event/mild/dizziness
+
+/datum/psyker_event/mild/dizziness/execute(mob/living/carbon/human/psyker)
+ psyker.set_dizzy_if_lower(15 SECONDS)
+ to_chat(psyker, span_danger("Overusing your powers has made you dizzy!"))
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm
new file mode 100644
index 00000000000000..140cb11f005773
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm
@@ -0,0 +1,11 @@
+/datum/psyker_event/mild/headache
+
+/datum/psyker_event/mild/headache/execute(mob/living/carbon/human/psyker)
+ psyker.add_mood_event("headache", /datum/mood_event/psyker_headache)
+ to_chat(psyker, span_danger("Overusing your powers has given you a splitting headache!"))
+ return TRUE
+
+/datum/mood_event/psyker_headache
+ description = "Overusing my powers has given me a splitting headache!"
+ mood_change = -15
+ timeout = 1 MINUTES // I wish my headaches went away that fast.
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm
new file mode 100644
index 00000000000000..28cd8e49d027a9
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm
@@ -0,0 +1,11 @@
+/datum/psyker_event/mild/nosebleed
+
+/datum/psyker_event/mild/nosebleed/execute(mob/living/carbon/human/psyker)
+ var/obj/item/bodypart/head = psyker.get_bodypart(BODY_ZONE_HEAD)
+ if(isnull(head))
+ return FALSE
+ if(!psyker.can_bleed())
+ return FALSE
+ head.adjustBleedStacks(5)
+ psyker.visible_message(span_notice("[psyker] gets a nosebleed."), span_danger("Overusing your powers has given you a nosebleed!"))
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/twitching.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/twitching.dm
new file mode 100644
index 00000000000000..8846dc42e94f9c
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/twitching.dm
@@ -0,0 +1,7 @@
+// Twitching, pretty mild.
+/datum/psyker_event/mild/twitching
+
+/datum/psyker_event/mild/twitching/execute(mob/living/carbon/human/psyker)
+ psyker.set_jitter_if_lower(15 SECONDS)
+ to_chat(psyker, span_danger("Overusing your powers has made you twitchy!"))
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/exhaustion.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/exhaustion.dm
new file mode 100644
index 00000000000000..f65c3a5471242f
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/exhaustion.dm
@@ -0,0 +1,11 @@
+// Head rings, myriad of minor effects and a big chunk of stamina damage.
+/datum/psyker_event/severe/exhaustion
+
+/datum/psyker_event/severe/exhaustion/execute(mob/living/carbon/human/psyker)
+ to_chat(psyker, span_userdanger("A loud ringing plays in your head, and you feel a wave of lethargy creep up on you."))
+ psyker.apply_damage(70, STAMINA)
+ psyker.adjustOrganLoss(ORGAN_SLOT_BRAIN, BRAIN_DAMAGE_MILD, BRAIN_DAMAGE_MILD)
+ psyker.set_jitter_if_lower(5 SECONDS)
+ psyker.playsound_local(psyker, 'sound/effects/screech.ogg', 50, FALSE)
+ psyker.flash_act(visual = TRUE, length = 1 SECONDS)
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/eyes_bleed.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/eyes_bleed.dm
new file mode 100644
index 00000000000000..e378d932d8d743
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/eyes_bleed.dm
@@ -0,0 +1,37 @@
+// Bleeding from the eyes and all that. Classic trope.
+// Deals a bunch of eye damage, makes you bleed and gives a temporary red overlay.
+/datum/psyker_event/severe/eyes_bleed
+ lingering = TRUE // Needs to linger to apply the blind remove.
+
+/datum/psyker_event/severe/eyes_bleed/execute(mob/living/carbon/human/psyker)
+ var/obj/item/organ/eyes/eyes = psyker.get_organ_slot(ORGAN_SLOT_EYES)
+ var/obj/item/bodypart/head/head = psyker.get_bodypart(BODY_ZONE_HEAD)
+ if(isnull(eyes))
+ return FALSE
+ if(!psyker.can_bleed())
+ return FALSE
+ psyker.visible_message(span_notice("[psyker] begins to bleed from the eyes!"), span_userdanger("You feel blood begin to seep out from your eyes!"))
+ eyes.apply_organ_damage(15) // not enough to do anything bad unless it's already damaged.
+ head.adjustBleedStacks(10)
+ psyker.playsound_local(psyker, 'sound/effects/meatslap.ogg', 50, FALSE)
+
+ // visual effects
+ psyker.become_nearsighted(src)
+ psyker.add_client_colour(/datum/client_colour/psyker_eyes_bleed, REF(src))
+ addtimer(CALLBACK(src, PROC_REF(_remove_blind), psyker), 6 SECONDS)
+ return TRUE
+
+/// Callback that removes the red eyeblind
+/datum/psyker_event/severe/eyes_bleed/proc/_remove_blind(mob/living/carbon/human/psyker)
+ // remove visual effects
+ psyker.cure_nearsighted(src)
+ psyker.remove_client_colour(REF(src))
+ // lingering event so have to qdel self
+ qdel(src)
+ return
+
+/datum/client_colour/psyker_eyes_bleed
+ priority = CLIENT_COLOR_IMPORTANT_PRIORITY
+ color = COLOR_RED
+ fade_in = 1
+ fade_out = 1
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm
new file mode 100644
index 00000000000000..614c8dbdcfc3b4
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm
@@ -0,0 +1,20 @@
+/datum/psyker_event/severe/hallucinate
+ /// Mostly instant shock factor stuff.
+ var/static/list/initial_hallucinations = list(
+ /datum/hallucination/delusion,
+ /datum/hallucination/xeno_attack,
+ /datum/hallucination/oh_yeah,
+ /datum/hallucination/death,
+ /datum/hallucination/fire,
+ /datum/hallucination/ice,
+ /datum/hallucination/shock
+ )
+
+/datum/psyker_event/severe/hallucinate/execute(mob/living/carbon/human/psyker)
+ to_chat(psyker, span_userdanger("You begin to lose your grip on reality!"))
+ // Generaly speaking we don't want these to last too long.
+ psyker.set_hallucinations_if_lower(60 SECONDS)
+ // We do also want immediate hallucinations as feedback, as the psyker_events double as stress warnings.
+ psyker.cause_hallucination(pick(initial_hallucinations), src)
+ return TRUE
+
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm
new file mode 100644
index 00000000000000..f5f3db60431a79
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm
@@ -0,0 +1,7 @@
+/datum/psyker_event/severe/vomit
+
+/datum/psyker_event/severe/vomit/execute(mob/living/carbon/human/psyker)
+ to_chat(psyker, span_userdanger("A wave of psychic energy overwhelms you, making you vomit!"))
+ psyker.vomit(VOMIT_CATEGORY_DEFAULT, lost_nutrition = 10)
+ // Even though they may dryheave, the feedback is there from vomit(), so mission accomplished.
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organs/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organs/_psyker_organ.dm
new file mode 100644
index 00000000000000..96f958ca214786
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organs/_psyker_organ.dm
@@ -0,0 +1,218 @@
+/*
+ Abstract type of the psyker organ which handles most of the stress resource as well as backlash events.
+*/
+/obj/item/organ/resonant/psyker
+ name = "abstract psyker organ"
+ desc = "how did you get this?!"
+ healing_factor = STANDARD_ORGAN_HEALING
+ decay_factor = 5 * STANDARD_ORGAN_DECAY //about 12mins to fully decay.
+ slot = ORGAN_SLOT_PSYKER
+ zone = BODY_ZONE_CHEST
+
+ /// The psyker organ handles most of the stress to do with psyker abilities; which is their central currency. Without this organ, you can't use psyker abilities.
+ /// Stress is not correlated to organ damage, but organ damage does affect this gland.
+ var/stress = 0
+ /// Stress threshold is how much the psyker organ can handle before the bad events start befalling the user.
+ /// Usually, 1x is the minor events, 1.5x are the major events, and 2x are the catastrophic events.
+ var/stress_threshold = PSYKER_STRESS_STANDARD_THRESHOLD
+ /// The root subtype this organ is meant to work with at full efficiency.
+ var/matching_root_type = /datum/power/psyker_root
+ /// Base recovery per second.
+ var/recovery_per_second = 0
+ /// Time between repeat backlash events while above the stress threshold.
+ var/stress_backlash_cooldown = 90 SECONDS
+
+ /// Cooldown for mild stress events.
+ COOLDOWN_DECLARE(mild_stress_backlash_cooldown)
+ /// Cooldown for severe stress events.
+ COOLDOWN_DECLARE(severe_stress_backlash_cooldown)
+
+ ///The stress warning message
+ var/datum/status_effect/power/stress_warning
+
+/// Call to modify stress. Don't adjust directly.
+/obj/item/organ/resonant/psyker/proc/modify_stress(amount, override_cap)
+ if(!isnum(amount))
+ return
+ var/cap_to = isnum(override_cap) ? override_cap : PSYKER_STRESS_STANDARD_THRESHOLD * 2
+ stress = clamp(stress + amount, 0, cap_to)
+
+/// Returns how much stress should naturally recover each second.
+/obj/item/organ/resonant/psyker/proc/get_stress_recovery_per_second()
+ if(stress >= PSYKER_STRESS_STANDARD_THRESHOLD)
+ return 0
+
+ var/recovery_amount = max(recovery_per_second - (damage * 0.015), 0)
+ if(has_compatible_root() && !has_matching_root())
+ recovery_amount *= 0.5
+
+ return recovery_amount
+
+/// Returns TRUE if the host has any psyker root at all.
+/obj/item/organ/resonant/psyker/proc/has_compatible_root()
+ if(!owner?.powers)
+ return FALSE
+
+ for(var/datum/power/power as anything in owner.powers)
+ if(istype(power, /datum/power/psyker_root))
+ return TRUE
+
+ return FALSE
+
+/// Returns TRUE if the host has the specific root subtype that belongs to this organ
+/obj/item/organ/resonant/psyker/proc/has_matching_root()
+ if(!owner?.powers)
+ return FALSE
+
+ for(var/datum/power/power as anything in owner.powers)
+ if(istype(power, matching_root_type))
+ return TRUE
+
+ return FALSE
+
+/// Updates medscanner visibility flags after the organ is inserted.
+/obj/item/organ/resonant/psyker/Insert(mob/living/carbon/organ_owner, special, movement_flags)
+ . = ..()
+ if(.)
+ RegisterSignal(organ_owner, COMSIG_LIVING_POST_FULLY_HEAL, PROC_REF(on_owner_fully_healed))
+ update_medscan_flags()
+
+/// Clears the flags on the organ before removal
+/obj/item/organ/resonant/psyker/Remove(mob/living/carbon/organ_owner, special = FALSE, movement_flags)
+ UnregisterSignal(organ_owner, COMSIG_LIVING_POST_FULLY_HEAL)
+ update_medscan_flags(FALSE)
+ return ..()
+
+/// Resets psyker stress after a full heal.
+/obj/item/organ/resonant/psyker/proc/on_owner_fully_healed(datum/source, heal_flags)
+ SIGNAL_HANDLER
+ stress = 0
+
+/// Updates the flags on the medscanner in the event that the person with the organ is not a psyker and when the organ is killing them.
+/obj/item/organ/resonant/psyker/proc/update_medscan_flags()
+ if(has_compatible_root())
+ organ_flags &= ~ORGAN_HAZARDOUS
+ return
+
+ organ_flags |= ORGAN_HAZARDOUS
+
+// If the organ is dangerous, it shows. Otherwise, you need an advanced med-scanner.
+/obj/item/organ/resonant/psyker/get_status_appendix(advanced, add_tooltips)
+ if(organ_flags & ORGAN_HAZARDOUS)
+ return "Hazardous resonant organ detected"
+ if(advanced)
+ return "Unnatural resonant organ detected"
+
+ return ..()
+
+// Handles stress & backlash events
+/obj/item/organ/resonant/psyker/on_life(seconds_per_tick, times_fired)
+ . = ..()
+ update_medscan_flags()
+
+ // If you have the associated power. read; you are a psyker.
+ if(has_compatible_root())
+ if(stress <= 0)
+ stress = 0
+ return
+ stress = max(stress - (get_stress_recovery_per_second() * seconds_per_tick), 0)
+
+ // Check if we do stress backlash after stress reduction.
+ if(stress >= (stress_threshold * 2)) // Catastrophic event.
+ stress_backlash(PSYKER_EVENT_TIER_CATASTROPHIC)
+ owner.dispel(src) // ends most effects
+ stress = 0 // No CD, just a hard reset and the consequences of your actions.
+ COOLDOWN_RESET(src, mild_stress_backlash_cooldown)
+ COOLDOWN_RESET(src, severe_stress_backlash_cooldown)
+ // Severe event.
+ else if(stress >= (stress_threshold * 1.5) && COOLDOWN_FINISHED(src, severe_stress_backlash_cooldown))
+ COOLDOWN_START(src, severe_stress_backlash_cooldown, stress_backlash_cooldown)
+ stress_backlash(PSYKER_EVENT_TIER_SEVERE)
+ // Mild event.
+ else if(stress >= stress_threshold && COOLDOWN_FINISHED(src, mild_stress_backlash_cooldown))
+ COOLDOWN_START(src, mild_stress_backlash_cooldown, stress_backlash_cooldown)
+ stress_backlash(PSYKER_EVENT_TIER_MILD)
+
+ //Handle the warning status effect
+ if(stress >= stress_threshold && !stress_warning)
+ stress_warning = owner.apply_status_effect(/datum/status_effect/power/stress_warning)
+ else if(stress < stress_threshold && stress_warning)
+ owner.remove_status_effect(/datum/status_effect/power/stress_warning)
+ stress_warning = null
+
+ // In the event that you implant this into someone else.
+ // Currently placeholder til we settle on what it do on people that don't have it.
+ else
+ damage += 1
+ owner.apply_damage(damage * 0.1, TOX)
+
+// "The psyker is exploding and probably about to summon extradimensional demons."
+/// When psyker stress gets too high, it triggers bad events, this chooses said bad events.
+/obj/item/organ/resonant/psyker/proc/stress_backlash(degree)
+ var/mob/living/carbon/human/human = owner
+ if(!istype(human))
+ return FALSE
+
+ var/base_type
+ switch(degree)
+ if(PSYKER_EVENT_TIER_MILD)
+ base_type = /datum/psyker_event/mild
+ if(PSYKER_EVENT_TIER_SEVERE)
+ base_type = /datum/psyker_event/severe
+ if(PSYKER_EVENT_TIER_CATASTROPHIC)
+ base_type = /datum/psyker_event/catastrophic
+ else
+ return FALSE
+
+ pick_psyker_event(base_type, human)
+ return TRUE
+
+/// Picks the backlash event after a stress breakdown
+/obj/item/organ/resonant/psyker/proc/pick_psyker_event(base_type, mob/living/carbon/human/human)
+ var/list/candidates = list()
+
+ // We check for abstract types and assign the weights
+ for(var/subtype in subtypesof(base_type))
+ var/datum/psyker_event/event_type = subtype
+
+ if(initial(event_type.abstract_type) == subtype)
+ continue
+
+ var/weight = initial(event_type.weight)
+ candidates[subtype] = weight
+
+ // We check the candidates, pick one, try it. If it returns true, we end. If it returns false, we try another.
+ // In principle this should never fail because each category has one that will always return true.
+ while(length(candidates))
+ var/subtype = pick_weight(candidates)
+ candidates -= subtype
+
+ var/datum/psyker_event/event = new subtype
+
+ if(!event.can_execute(human, src))
+ qdel(event)
+ continue
+
+ // We check if it actually successfully executed. Qdel it under normal circumstances; if it lingers we don't.
+ if(event.execute(human))
+ if(!event.lingering)
+ qdel(event)
+ return
+
+ // Execution failed? We retry
+ qdel(event)
+
+ return
+
+
+// Warning message for high stress
+/datum/status_effect/power/stress_warning
+ id = "stress_warning"
+ tick_interval = STATUS_EFFECT_NO_TICK // This one's just a warning
+ alert_type = /atom/movable/screen/alert/status_effect/stress_warning
+
+/atom/movable/screen/alert/status_effect/stress_warning
+ icon = 'icons/mob/actions/actions_ecult.dmi'
+ name = "Stress Warning!"
+ desc = "Your stress is at the backlash threshold! You will suffer periodic negative events until you meditate, and continued use of your powers will only make things worse!"
+ icon_state = "mansus_link" // Placeholder
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organs/chemotropic.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organs/chemotropic.dm
new file mode 100644
index 00000000000000..15c4af9555e013
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organs/chemotropic.dm
@@ -0,0 +1,76 @@
+/*
+ You do not passively regenerate stress, but you gain drastically increased stress loss (including in combat) from substances.
+*/
+/datum/power/psyker_root/chemotropic
+ name = "Chemotropic Gland"
+ desc = "An unnatural organ that grows inside the chest-cavity of Psykers. Required as a catalyst to wield Psyker powers.\
+ \nThis gland particularly only functions through external chemical stimuli: particularly substances such as nicotine, ethanol and hard-drugs. You barely recover stress passively, but recover vast amounts from having any of the aforementioned\
+ substances inside your bloodstream, with hard-drugs yielding more reecovery than nicotine and ethanol.\
+ \nHaving matching negative quirks with the substance (such as the Smoker quirk with Nicotine) increases the stress recovery."
+ security_record_text = "Subject wields psionic abilities and recovers from it through substance consumption."
+ organ_type = /obj/item/organ/resonant/psyker/chemotropic
+
+/obj/item/organ/resonant/psyker/chemotropic
+ name = "chemotropic gland"
+ desc = "An intrusive organ that should not even be able to function in most bodies. It responds to chemical stimuli in the bloodstream, accelerating psychic recovery in exchange for unhealthy dependency."
+ icon = 'modular_doppler/modular_powers/icons/items/organs.dmi'
+ icon_state = "chemotropic"
+ recovery_per_second = PSYKER_STRESS_RECOVERY * 0.25
+ matching_root_type = /datum/power/psyker_root/chemotropic
+ stress_backlash_cooldown = 180 SECONDS // double duration between backlash events since you can be stuck on a tier from the lack of accessible recovery.
+
+ /// Base amount to recover from chemicals, before all multipliers
+ var/base_recovery_amount = PSYKER_STRESS_CHEMOTROPIC_POWER
+ /// Multiplier for nicotine recovery. Keep in mind that smoking drip-feeds it.
+ var/nicotine_multiplier = 0.5
+ /// Multiplier for ethanol
+ var/ethanol_multiplier = 0.5
+ /// Multiplier for hard drugs
+ var/hard_drugs_multiplier = 1
+
+ /// Multiplier on how much quirks increase the appropriate stress gain.
+ var/quirk_multiplier = 1.25
+
+/// Returns the stress recovery multiplier granted by active chemical stimuli.
+/obj/item/organ/resonant/psyker/chemotropic/proc/get_chemical_recovery_multiplier()
+ if(!owner?.reagents?.reagent_list)
+ return 0
+
+ var/recovery_multiplier = 0
+ for(var/datum/reagent/reagent as anything in owner.reagents.reagent_list)
+ // Nicotine is relatively common and less hamrful so its less the power.
+ if(istype(reagent, /datum/reagent/drug/nicotine))
+ var/nicotine_recovery = nicotine_multiplier
+ if(owner.has_quirk(/datum/quirk/item_quirk/addict/smoker))
+ nicotine_recovery *= quirk_multiplier
+ recovery_multiplier = max(recovery_multiplier, nicotine_recovery)
+ continue
+ // Alcoholism is still common but more directly hamrful so its a bit less power
+ if(istype(reagent, /datum/reagent/consumable/ethanol))
+ var/ethanol_recovery = ethanol_multiplier
+ if(owner.has_quirk(/datum/quirk/item_quirk/addict/alcoholic))
+ ethanol_recovery *= quirk_multiplier
+ recovery_multiplier = max(recovery_multiplier, ethanol_recovery)
+ continue
+ // Doing drugs gets you full power. Go do drugs, kids.
+ if(istype(reagent, /datum/reagent/drug))
+ var/hard_drug_recovery = hard_drugs_multiplier
+ if(owner.has_quirk(/datum/quirk/item_quirk/addict/junkie))
+ hard_drug_recovery *= quirk_multiplier
+ recovery_multiplier = max(recovery_multiplier, hard_drug_recovery)
+ break
+
+ return recovery_multiplier
+
+/// Returns stress recovery per second based on substances in the host's bloodstream.
+/obj/item/organ/resonant/psyker/chemotropic/get_stress_recovery_per_second()
+ var/recovery_multiplier = get_chemical_recovery_multiplier()
+ if(!recovery_multiplier)
+ return recovery_per_second
+
+ var/recovery_amount = base_recovery_amount * recovery_multiplier
+ // Wrong root? Recover only a third as much.
+ if(!has_matching_root())
+ recovery_amount *= PSYKER_MISMATCHED_ORGAN_EFFICIENCY
+
+ return recovery_amount + recovery_per_second
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organs/paracausal.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organs/paracausal.dm
new file mode 100644
index 00000000000000..d9612a230dc030
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organs/paracausal.dm
@@ -0,0 +1,63 @@
+/*
+ The vanilla option. Has passive-regen and the associated power grants it meditation.
+*/
+/datum/power/psyker_root/paracausal
+ name = "Paracausal Gland"
+ desc = "An unnatural organ that grows inside the chest-cavity of Psykers. Required as a catalyst to wield Psyker powers.\
+ \nYou passively recover stress, which can be boosted by using the Meditate power while holding still."
+ security_record_text = "Subject wields psionic abilities."
+ organ_type = /obj/item/organ/resonant/psyker/paracausal
+
+/obj/item/organ/resonant/psyker/paracausal
+ name = "paracausal gland"
+ desc = "An intrusive organ that should not even be able to function in most bodies. Commonly found in the bodies of Psykers. Though many would try to implement these into themselves to try and awaken psychic powers, \
+ its presence in those without such powers is often life-threatening."
+ icon = 'modular_doppler/modular_powers/icons/items/organs.dmi'
+ icon_state = "paracausal"
+ recovery_per_second = PSYKER_STRESS_RECOVERY
+ matching_root_type = /datum/power/psyker_root/paracausal
+
+ /// Meditation action owned by this organ.
+ var/datum/action/cooldown/power/resonant_meditate/meditate_action
+
+/// Cleans up the organ-owned meditation action.
+/obj/item/organ/resonant/psyker/paracausal/Destroy()
+ QDEL_NULL(meditate_action)
+ return ..()
+
+/// Grants or removes the meditation action when this organ is inserted.
+/obj/item/organ/resonant/psyker/paracausal/on_mob_insert(mob/living/carbon/organ_owner, special = FALSE, movement_flags)
+ . = ..()
+ update_meditate_action(organ_owner)
+
+/// Removes the meditation action when this organ leaves a host.
+/obj/item/organ/resonant/psyker/paracausal/on_mob_remove(mob/living/carbon/organ_owner, special = FALSE, movement_flags)
+ remove_meditate_action(organ_owner)
+ return ..()
+
+/// Keeps the organ-owned meditation action in sync with the current host.
+/obj/item/organ/resonant/psyker/paracausal/on_life(seconds_per_tick, times_fired)
+ . = ..()
+ update_meditate_action(owner)
+
+/// Ensures the current host only has meditation while they possess a compatible psyker root.
+/obj/item/organ/resonant/psyker/paracausal/proc/update_meditate_action(mob/living/carbon/organ_owner)
+ if(!organ_owner)
+ return
+
+ if(!meditate_action)
+ meditate_action = new(src)
+
+ if(has_compatible_root())
+ if(meditate_action.owner != organ_owner)
+ meditate_action.Grant(organ_owner)
+ return
+
+ remove_meditate_action(organ_owner)
+
+/// Removes this organ's meditation action from the given host.
+/obj/item/organ/resonant/psyker/paracausal/proc/remove_meditate_action(mob/living/carbon/organ_owner)
+ if(!meditate_action || !organ_owner)
+ return
+
+ meditate_action.Remove(organ_owner)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm
new file mode 100644
index 00000000000000..2e6216413d2353
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm
@@ -0,0 +1,585 @@
+/*
+ Lets you use blood to scry someone. Really potent for detectives and the likes; but has a massive laundry list of things that disable it.
+*/
+
+/datum/power/psyker_power/scrying
+ name = "Scrying"
+ desc = "Using a sample of a creature's blood, you can see the world through their eyes remotely. Creatures will be vague and hard to distinguish, but their environment will appear clear. \
+ In this state, you use their sight instead of your own; but you cannot target creatures that are immune to magic, scrying; or lack the brain activity required to be detectable (dumb). \
+ Passively builds up stress, with extended use causing escalating amounts of stress. The target sometimes gets preminations to indicate they are watched."
+ security_record_text = "Subject can psychically observe people's locations based on blood samples from extreme distances."
+ value = 10
+ action_path = /datum/action/cooldown/power/psyker/scrying
+
+/datum/action/cooldown/power/psyker/scrying
+ name = "Scrying"
+ desc = "Using a sample of a creature's blood, you can see the world through their eyes remotely."
+ button_icon = 'icons/mob/actions/actions_animal.dmi'
+ button_icon_state = "gaze"
+ click_to_activate = TRUE
+ target_range = 1
+
+ /// The target we are currently scrying
+ var/atom/movable/scry_target
+ /// Seconds spent maintaining the current scrying link
+ var/scry_duration = 0
+
+ // This thing is a MESS. We have split functionality into three datums.
+ /// Scrying Camera which handles imparting the sight of the target
+ var/datum/scrying_camera/scry_camera
+ /// Scrying Vision which handles vision traits on the user.
+ var/datum/scrying_vision/scry_vision
+ /// Scrying Immunity Mask which hides people into indistinct overlays.
+ var/datum/scrying_immunity_mask/immunity_mask
+ /// and Scrying Tracker which basically handles any and all things related to stress gain.
+ var/datum/psyker_scry_tracker/tracker
+
+/datum/action/cooldown/power/psyker/scrying/Trigger(mob/clicker, trigger_flags, atom/target)
+ if(active)
+ end_scrying()
+ to_chat(owner, span_notice("Your sight returns as you focus back on your own mind."))
+ else
+ . = ..()
+ return TRUE
+
+/*
+ Most of the delegation with scrying is handled by scry_vision. We simply verify here and build the datums used.
+*/
+/datum/action/cooldown/power/psyker/scrying/use_action(mob/living/user, atom/target)
+ var/list/dna_samples = get_blood_dna_list_from_target(target)
+ if(!length(dna_samples))
+ to_chat(user, span_warning("You need blood to focus your scrying."))
+ return FALSE
+
+ // If your list of dna samples has multiples then my man you gotta clean your samples. Chooses a random one.
+ var/selected_dna = pick(dna_samples)
+ var/mob/living/chosen_target = find_scry_target_from_dna(selected_dna)
+ if(!chosen_target)
+ to_chat(user, span_warning("No mind to link to."))
+ return FALSE
+
+ if(!can_affect_scrying(chosen_target))
+ to_chat(user, span_warning("Your sight cannot find purchase on that mind."))
+ return FALSE
+
+ active = TRUE
+ scry_duration = 0
+
+ scry_target = chosen_target
+ // We create the new datums which will immediately handle their effects.
+ scry_camera = new(user, scry_target, src)
+ scry_vision = new(user)
+ tracker = new(src, user)
+ immunity_mask = new(src, user, scry_camera.scry_eye)
+ immunity_mask.refresh_now()
+
+ // Bit of psyker stress on use ontop of the processing cost just to prevent too much spam peeking.
+ modify_stress(PSYKER_STRESS_MINOR * 1.5)
+
+ playsound(user, 'sound/effects/magic/swap.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+
+ // Adds listeners for dispelling on the target
+ RegisterSignal(scry_target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+ return TRUE
+
+// Dispel signalers
+/datum/action/cooldown/power/psyker/scrying/Grant(mob/granted_to)
+ . = ..()
+ if(resonant)
+ RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+/datum/action/cooldown/power/psyker/scrying/Remove(mob/removed_from)
+ . = ..()
+ if(resonant)
+ UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
+ end_scrying()
+
+/// Dispel proc ends the scrying
+/datum/action/cooldown/power/psyker/scrying/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+ if(active)
+ to_chat(owner, span_warning("Your scrying link was cut off!"))
+ end_scrying()
+
+/// Gets DNA from blood
+/datum/action/cooldown/power/psyker/scrying/proc/get_blood_dna_list_from_target(atom/target)
+ if(isnull(target))
+ return null
+
+ var/list/dna_list = list()
+
+ if(ismob(target))
+ return dna_list
+
+ // Gets dna from a blood decal.
+ if(istype(target, /obj/effect/decal/cleanable/blood))
+ var/list/blood = GET_ATOM_BLOOD_DNA(target)
+ for(var/dna in blood)
+ dna_list += dna
+ return dna_list
+
+ // Gets dna from blood from reagent containers. Note: There's a bug with scraping blood not saving DNA; so if it acts weirds its likely that (as of 20/02/26)
+ if(istype(target, /obj/item/reagent_containers))
+ for(var/datum/reagent/present_reagent as anything in target.reagents?.reagent_list)
+ if(!istype(present_reagent, /datum/reagent/blood))
+ continue
+ var/blood_dna = present_reagent.data?["blood_DNA"]
+ if(isnull(blood_dna))
+ continue
+ if(islist(blood_dna))
+ for(var/dna in blood_dna)
+ dna_list += dna
+ else
+ dna_list += blood_dna
+
+ // Any non-mob atom with forensics blood on it (e.g. clothes, tools)
+ var/list/blood = GET_ATOM_BLOOD_DNA(target)
+ if(length(blood))
+ for(var/dna in blood)
+ dna_list += dna
+
+ return dna_list
+
+/// Checks the blood for a dna match.
+/datum/action/cooldown/power/psyker/scrying/proc/find_scry_target_from_dna(selected_dna)
+ if(!selected_dna)
+ return null
+
+ for(var/mob/living/target in GLOB.mob_list)
+ if(isobserver(target))
+ continue
+ var/list/blood_dna = target.get_blood_dna_list()
+ if(blood_dna && blood_dna[selected_dna])
+ return target
+ return null
+
+/// Returns the current per-second stress upkeep for maintaining the scrying link.
+/datum/action/cooldown/power/psyker/scrying/proc/get_upkeep_stress_per_second()
+ var/stress_per_second = (PSYKER_STRESS_MINOR / 2) + (scry_duration * 0.1)
+ var/mob/living/action_owner = owner
+ if(action_owner?.has_quirk(/datum/quirk/item_quirk/blindness))
+ stress_per_second *= 0.5
+ return stress_per_second
+
+/// called by everything that ends scrying; removes all the datums and left over signalers.
+/datum/action/cooldown/power/psyker/scrying/proc/end_scrying()
+ if(!active)
+ return
+
+ active = FALSE
+ scry_duration = 0
+
+ QDEL_NULL(tracker)
+ QDEL_NULL(scry_vision)
+ QDEL_NULL(scry_camera)
+ QDEL_NULL(immunity_mask)
+
+ // removes dispel signal from target
+ UnregisterSignal(scry_target, COMSIG_ATOM_DISPEL)
+
+ scry_target = null
+
+
+
+/*
+ We bypass our own vision traits and see the world from the target's pov.
+ Handles the removal of vision traits and the application of the overlay.
+*/
+/datum/scrying_vision
+ /// Used to remove/re-add quirk blindness safely.
+ var/had_blind_quirk = FALSE
+ /// Weakref to the viewer mob
+ var/datum/weakref/viewer_ref
+
+/datum/scrying_vision/New(mob/living/viewer)
+ . = ..()
+ viewer_ref = WEAKREF(viewer)
+ apply()
+
+/datum/scrying_vision/Destroy()
+ clear()
+ viewer_ref = null
+ return ..()
+
+/// Applies vision modifiers such as removing blindness quirk vision, as well as adding thecurse overlay.
+/datum/scrying_vision/proc/apply()
+ var/mob/living/viewer = viewer_ref?.resolve()
+ if(!istype(viewer))
+ return
+
+ // If blindness is being enforced by the blind quirk, we temporarily remove it.
+ if(viewer.is_blind_from(QUIRK_TRAIT))
+ had_blind_quirk = TRUE
+ viewer.remove_status_effect(/datum/status_effect/grouped/blindness, QUIRK_TRAIT)
+
+ ADD_TRAIT(viewer, TRAIT_SIGHT_BYPASS, REF(src))
+
+ // Restrict vision partially.
+ viewer.overlay_fullscreen("curse", /atom/movable/screen/fullscreen/curse, 1)
+ viewer.update_sight()
+
+/// Removes the things applied in apply()
+/datum/scrying_vision/proc/clear()
+ var/mob/living/viewer = viewer_ref?.resolve()
+ if(!istype(viewer))
+ return
+
+ viewer.clear_fullscreen("curse", 50)
+
+ REMOVE_TRAIT(viewer, TRAIT_SIGHT_BYPASS, REF(src))
+
+ // Restore the blind quirk's blindness if we removed it.
+ if(had_blind_quirk)
+ viewer.become_blind(QUIRK_TRAIT)
+
+ had_blind_quirk = FALSE
+ viewer.update_sight()
+
+/*
+ This sets the player's perspective to a scry eye that follows the target.
+*/
+/datum/scrying_camera
+ /// Weakref to the viewer mob
+ var/datum/weakref/viewer_ref
+ /// Weakref to the target mob
+ var/datum/weakref/target_ref
+ /// Weakref to the power action
+ var/datum/weakref/action_ref
+ /// Weakref to the mob/eye that handles the vision
+ var/mob/eye/psyker_scry/scry_eye
+
+
+/datum/scrying_camera/New(mob/living/viewer, atom/movable/target, datum/action/cooldown/power/psyker/scrying/action)
+ . = ..()
+ viewer_ref = WEAKREF(viewer)
+ target_ref = WEAKREF(target)
+ action_ref = WEAKREF(action)
+
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ qdel(src)
+ return
+
+ scry_eye = new(target_turf)
+ scry_eye.set_target(target)
+ scry_eye.assign_user(viewer)
+
+ RegisterSignals(target, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(on_target_event))
+
+/datum/scrying_camera/Destroy()
+ var/atom/movable/target = target_ref?.resolve()
+ if(target)
+ UnregisterSignal(target, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING))
+
+ if(scry_eye)
+ scry_eye.assign_user(null)
+ QDEL_NULL(scry_eye)
+
+ viewer_ref = null
+ target_ref = null
+ return ..()
+
+/// Called by the moved and qdeleted signaler, updating the scrying eye's location or removing it if qdeled
+/datum/scrying_camera/proc/on_target_event(datum/source)
+ SIGNAL_HANDLER
+
+ if(!scry_eye || QDELETED(scry_eye))
+ qdel(src)
+ return
+
+ var/atom/movable/target = target_ref?.resolve()
+ if(QDELETED(target) || !ismovable(target))
+ qdel(src)
+ return
+
+ var/turf/target_turf = get_turf(target)
+ if(target_turf)
+ scry_eye.setLoc(target_turf)
+ var/datum/action/cooldown/power/psyker/scrying/action = action_ref?.resolve()
+ action?.immunity_mask?.refresh_now()
+
+
+
+/*
+ Tracker just adds stress and handles proccessing.
+*/
+/datum/psyker_scry_tracker
+ /// Weakref to the power action
+ var/datum/weakref/action_ref
+ /// Weakref to the power's owner
+ var/datum/weakref/owner_ref
+
+/datum/psyker_scry_tracker/New(datum/action/cooldown/power/psyker/scrying/action, mob/living/owner)
+ . = ..()
+ action_ref = WEAKREF(action)
+ owner_ref = WEAKREF(owner)
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/psyker_scry_tracker/Destroy()
+ STOP_PROCESSING(SSfastprocess, src)
+ return ..()
+
+/datum/psyker_scry_tracker/process(seconds_per_tick)
+ var/datum/action/cooldown/power/psyker/scrying/action = action_ref?.resolve()
+ if(!action || !action.active)
+ qdel(src)
+ return
+
+ var/mob/living/owner = owner_ref?.resolve()
+ if(!owner)
+ action.end_scrying()
+ qdel(src)
+ return
+
+ var/atom/movable/current_target = action.scry_target
+ if(current_target && !action.can_affect_scrying(current_target))
+ action.end_scrying()
+ to_chat(owner, span_warning("Your scrying link was cut off!"))
+ qdel(src)
+ return
+
+ // Random chance for the target to feel a chill down their spine.
+ if(ismob(current_target))
+ var/mob/target_mob = current_target
+ if(prob((seconds_per_tick / 30) * 100))
+ to_chat(target_mob, span_warning("A shudder runs down your spine, as if you're being watched."))
+
+ // Applies stress, increasing with link duration to prevent permanent upkeep.
+ action.modify_stress(action.get_upkeep_stress_per_second() * seconds_per_tick)
+ action.scry_duration += seconds_per_tick
+
+ // Re-apply in case other systems reassert blindness/quirk/etc.
+ if(action.scry_vision)
+ action.scry_vision.apply()
+
+/*
+ Used to mask mobs from the scrying eye.
+*/
+/datum/scrying_immunity_mask
+ /// Weakref to the viewer mob
+ var/datum/weakref/viewer_ref
+ /// Weakref to the mob eye
+ var/datum/weakref/eye_ref
+ /// Weakref to the power action
+ var/datum/weakref/action_ref
+
+ /// mob -> mask_image
+ var/list/masked_mobs = list()
+
+/datum/scrying_immunity_mask/New(datum/action/cooldown/power/psyker/scrying/action, mob/living/viewer, mob/eye/psyker_scry/eye)
+ . = ..()
+ action_ref = WEAKREF(action)
+ viewer_ref = WEAKREF(viewer)
+ eye_ref = WEAKREF(eye)
+
+ if(viewer)
+ viewer.mob_flags |= MOB_HAS_SCREENTIPS_NAME_OVERRIDE
+ RegisterSignal(viewer, COMSIG_MOB_REQUESTING_SCREENTIP_NAME_FROM_USER, PROC_REF(screentip_name_override))
+ RegisterSignal(viewer, COMSIG_LIVING_PERCEIVE_EXAMINE_NAME, PROC_REF(examine_name_override))
+
+ START_PROCESSING(SSfastprocess, src)
+
+/datum/scrying_immunity_mask/Destroy()
+ STOP_PROCESSING(SSfastprocess, src)
+ var/mob/living/viewer = viewer_ref?.resolve()
+ if(viewer)
+ UnregisterSignal(viewer, list(COMSIG_MOB_REQUESTING_SCREENTIP_NAME_FROM_USER, COMSIG_LIVING_PERCEIVE_EXAMINE_NAME))
+ clear_all()
+ return ..()
+
+/datum/scrying_immunity_mask/process(seconds_per_tick)
+ var/datum/action/cooldown/power/psyker/scrying/action = action_ref?.resolve()
+ var/mob/living/viewer = viewer_ref?.resolve()
+ var/mob/eye/psyker_scry/eye = eye_ref?.resolve()
+
+ if(!action || !action.active || !viewer || !viewer.client || !eye)
+ qdel(src)
+ return
+
+ update_masks(viewer, eye, action)
+
+/// Proc that signals update_masks() and forces a refresh of all the masks
+/datum/scrying_immunity_mask/proc/refresh_now()
+ var/datum/action/cooldown/power/psyker/scrying/action = action_ref?.resolve()
+ var/mob/living/viewer = viewer_ref?.resolve()
+ var/mob/eye/psyker_scry/eye = eye_ref?.resolve()
+ if(!action || !action.active || !viewer || !viewer.client || !eye)
+ return
+
+ update_masks(viewer, eye, action)
+
+/// Gets every mob in view and applies an alpha'd mask to all mobs.
+/datum/scrying_immunity_mask/proc/update_masks(mob/living/viewer, mob/eye/psyker_scry/eye, datum/action/cooldown/power/psyker/scrying/action)
+ var/list/current_mobs = list()
+ for(var/mob/living/seen_mob in view(viewer.client.view, eye))
+ current_mobs += seen_mob
+
+ // Remove masks for mobs no longer in view (or deleted)
+ for(var/mob/living/masked_mob as anything in masked_mobs.Copy())
+ if(QDELETED(masked_mob) || !(masked_mob in current_mobs))
+ unmask_mob(viewer, masked_mob)
+
+ // Apply masks for newly seen mobs (baseline: everyone)
+ for(var/mob/living/seen_mob as anything in current_mobs)
+ if(masked_mobs[seen_mob])
+ sync_mask_image(seen_mob)
+ continue
+
+ mask_mob(viewer, seen_mob)
+
+/// Keep silhouettes aligned with the target's current appearance (transform/pixel offsets/dir).
+/datum/scrying_immunity_mask/proc/sync_mask_image(mob/living/target_mob)
+ var/image/mask_image = masked_mobs[target_mob]
+ if(!mask_image)
+ return
+ // Copy the full appearance so transforms and pixel offsets stay in sync.
+ mask_image.appearance = target_mob.appearance
+ mask_image.override = TRUE
+ mask_image.name = "Unknown"
+ mask_image.color = "#000000"
+ mask_image.alpha = 180
+ mask_image.appearance_flags |= RESET_TRANSFORM
+ mask_image.dir = target_mob.dir
+ // Avoid double-applying mob pixel offsets; the image is already anchored to the mob.
+ mask_image.pixel_w = 0
+ mask_image.pixel_x = 0
+ mask_image.pixel_y = 0
+ mask_image.pixel_z = 0
+ SET_PLANE_EXPLICIT(mask_image, ABOVE_GAME_PLANE, target_mob)
+
+/// Applies the alpha mob mask, turning them into a see-trhrough silhouette
+/datum/scrying_immunity_mask/proc/mask_mob(mob/living/viewer, mob/living/target_mob)
+ if(!viewer?.client || QDELETED(target_mob))
+ return
+
+ // Delusion-style hallucination override: a client-only mask image that owns the click/name.
+ var/image/mask_image = image(loc = target_mob)
+ mask_image.appearance = target_mob.appearance
+ mask_image.override = TRUE
+ mask_image.name = "Unknown"
+ mask_image.color = "#000000"
+ mask_image.alpha = 180
+ mask_image.appearance_flags |= RESET_TRANSFORM
+ mask_image.dir = target_mob.dir
+ // Avoid double-applying mob pixel offsets; the image is already anchored to the mob.
+ mask_image.pixel_w = 0
+ mask_image.pixel_x = 0
+ mask_image.pixel_y = 0
+ mask_image.pixel_z = 0
+ SET_PLANE_EXPLICIT(mask_image, ABOVE_GAME_PLANE, target_mob)
+
+ viewer.client.images += mask_image
+ masked_mobs[target_mob] = mask_image
+
+ // Hides data about the mob with vague examines + no huds.
+ RegisterSignal(target_mob, COMSIG_ATOM_EXAMINE, PROC_REF(on_target_examine))
+ hide_data_huds(viewer, target_mob)
+
+/// Removes mob masking from a target
+/datum/scrying_immunity_mask/proc/unmask_mob(mob/living/viewer, mob/living/target_mob)
+ var/image/mask_image = masked_mobs[target_mob]
+ if(!mask_image)
+ return
+
+ if(viewer?.client)
+ viewer.client.images -= mask_image
+
+ UnregisterSignal(target_mob, COMSIG_ATOM_EXAMINE)
+ unhide_data_huds(viewer, target_mob)
+ masked_mobs -= target_mob
+
+/// Clears all mob masks on the target
+/datum/scrying_immunity_mask/proc/clear_all()
+ var/mob/living/viewer = viewer_ref?.resolve()
+ if(!viewer?.client)
+ masked_mobs.Cut()
+ return
+
+ for(var/mob/living/masked_mob as anything in masked_mobs.Copy())
+ unmask_mob(viewer, masked_mob)
+
+/// Overrides the examine text of the target to be vague.
+/datum/scrying_immunity_mask/proc/on_target_examine(datum/source, mob/user, list/examine_list)
+ SIGNAL_HANDLER
+
+ var/mob/living/viewer = viewer_ref?.resolve()
+ if(user != viewer)
+ return NONE
+
+ if(!istype(source, /mob/living) || !masked_mobs[source])
+ return NONE
+
+ examine_list.Cut()
+ examine_list += span_notice("It's too hazy to make out details.")
+ return NONE
+
+/// Hides all glasses HUDs from the target mob.
+/datum/scrying_immunity_mask/proc/hide_data_huds(mob/living/viewer, mob/living/target_mob)
+ if(!viewer || !target_mob)
+ return
+ for(var/datum/atom_hud/hud as anything in GLOB.huds)
+ hud.hide_single_atomhud_from(viewer, target_mob)
+
+/// Unhides all glasses HUDs from the target mob
+/datum/scrying_immunity_mask/proc/unhide_data_huds(mob/living/viewer, mob/living/target_mob)
+ if(!viewer || !target_mob)
+ return
+ for(var/datum/atom_hud/hud as anything in GLOB.huds)
+ hud.unhide_single_atomhud_from(viewer, target_mob)
+
+/// Forcefully overrides the examine name of the target.
+/datum/scrying_immunity_mask/proc/examine_name_override(datum/source, mob/living/examined, visible_name, list/name_override)
+ SIGNAL_HANDLER
+
+ if(!istype(examined) || !masked_mobs[examined])
+ return NONE
+
+ name_override[1] = "Unknown"
+ return COMPONENT_EXAMINE_NAME_OVERRIDEN
+
+/// Forcefully overrides the top portion screentip of the mob's name.
+/datum/scrying_immunity_mask/proc/screentip_name_override(datum/source, list/returned_name, obj/item/held_item, atom/hovered)
+ SIGNAL_HANDLER
+
+ if(!istype(hovered) || !masked_mobs[hovered])
+ return NONE
+
+ returned_name[1] = "Unknown"
+ return SCREENTIP_NAME_SET
+
+
+/*
+ Scry eye mob: purely perspective anchor.
+*/
+/mob/eye/psyker_scry
+ name = "scrying eye"
+ /// Weakref to the user that's seeing through the mob eye
+ var/datum/weakref/user_ref
+ /// Weakref to the target we're following
+ var/datum/weakref/target_ref
+
+/mob/eye/psyker_scry/Destroy()
+ assign_user(null)
+ return ..()
+
+/// Assigns the mob we're following.
+/mob/eye/psyker_scry/proc/assign_user(mob/living/new_user)
+ var/mob/living/old_user = user_ref?.resolve()
+ if(old_user)
+ old_user.reset_perspective(null)
+ name = initial(src.name)
+
+ user_ref = WEAKREF(new_user)
+
+ if(new_user)
+ new_user.reset_perspective(src)
+ name = "Scrying Eye ([new_user.name])"
+
+/// Sets our target weakref
+/mob/eye/psyker_scry/proc/set_target(atom/movable/target)
+ target_ref = WEAKREF(target)
+
+/// Updates the location of the mob eye.
+/mob/eye/psyker_scry/proc/setLoc(turf/destination, force_update = FALSE)
+ if(destination)
+ forceMove(destination)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm
new file mode 100644
index 00000000000000..9d306726175088
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm
@@ -0,0 +1,379 @@
+/*
+ Telekinesis. This is one of the earliest made powers and is a port of how the grab module from MODs do it. It's a bit messy as a consequence; even after this was cleaned up later in production.
+*/
+
+#define TK_CLICK_NONE 0
+#define TK_CLICK_TRIGGER 1
+#define TK_CLICK_MIDDLE 2
+#define TK_CLICK_RIGHT 3
+
+/datum/power/psyker_power/telekinesis
+ name = "Telekinesis"
+ desc = "Grants the ability to manipulate and move various objects. Generates stress based upon weight on pick-up and throw, as well as passively while holding an object."
+ security_record_text = "Subject can wield telekinesis to maneuver and fling objects."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+ required_powers = list(/datum/power/psyker_root)
+ action_path = /datum/action/cooldown/power/psyker/telekinesis
+
+/datum/action/cooldown/power/psyker/telekinesis
+ name = "Telekinesis"
+ desc = "Middle-click to grab an object, Right-Click to drop, Middle-Click again to punt!"
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "repulse"
+ click_to_activate = TRUE
+ target_self = FALSE
+
+ unset_after_click = FALSE
+ target_range = 255 // this is just for show.
+
+ mental = FALSE // We are lifting them with the mind but it doesn't affect the target's mind
+
+ /// Range of the kinesis grab.
+ var/grab_range = 8
+
+ /// Stat required for us to grab a mob.
+ var/stat_required = DEAD
+
+ /// Atom we grabbed with kinesis.
+ var/atom/movable/grabbed_atom
+
+ /// Overlay we add to each grabbed atom.
+ var/mutable_appearance/kinesis_icon
+ /// Overlay we add to the player when using this power.
+ var/mutable_appearance/player_icon
+
+ /// Mouse tracker overlay (telekinesis-specific)
+ var/atom/movable/screen/fullscreen/cursor_catcher/kinesis/psyker_tk/kinesis_catcher
+ /// Which mouse click is used in use_action
+ var/tk_click_type = TK_CLICK_NONE
+
+// Auto-clear the grab if we disable the power + a bit of UI feedback.
+/datum/action/cooldown/power/psyker/telekinesis/Trigger(mob/clicker, trigger_flags, atom/target)
+ . = ..()
+ if(grabbed_atom)
+ clear_grab(playsound = FALSE)
+ to_chat(owner, span_notice("You relax your telekinetic powers."))
+ else
+ to_chat(owner, span_notice("You focus your telekinetic powers... Middle-click: Grab/Punt | Right-click: Drop | Move mouse: to drag"))
+ return TRUE
+
+// We need to disseminate which mouse-press is done for our effects.
+/datum/action/cooldown/power/psyker/telekinesis/InterceptClickOn(mob/living/clicker, params, atom/target)
+ var/list/mods = params2list(params)
+ if(LAZYACCESS(mods, RIGHT_CLICK))
+ tk_click_type = TK_CLICK_RIGHT
+ else if(LAZYACCESS(mods, MIDDLE_CLICK))
+ tk_click_type = TK_CLICK_MIDDLE
+ else
+ return FALSE // do not consume the click on lefties.
+
+ . = ..()
+ if(!.)
+ tk_click_type = TK_CLICK_NONE
+ return TRUE // always return true in right and middle clicks.
+
+/datum/action/cooldown/power/psyker/telekinesis/use_action(mob/living/user, atom/target)
+ // gets the mouseclick and saves it; reverts for the next.
+ var/click_type = tk_click_type
+ tk_click_type = TK_CLICK_NONE
+
+ // Change effects depending on right and middel click.
+ switch(click_type)
+ // Drops the item.
+ if(TK_CLICK_RIGHT)
+ if(grabbed_atom)
+ clear_grab()
+ return TRUE
+ return FALSE
+
+ // Grabs if empty, or punts if holding.
+ if(TK_CLICK_MIDDLE)
+ if(INCAPACITATED_IGNORING(user, INCAPABLE_GRAB))
+ owner.balloon_alert(user, span_warning("Cannot grab target!"))
+ return FALSE
+ // Attempt to grab if we aren't holding anything.
+ if(!grabbed_atom)
+ if(!target)
+ owner.balloon_alert(user, span_warning("No target!"))
+ return FALSE
+ if(!range_check(user, target))
+ owner.balloon_alert(user, span_warning("Too far!"))
+ return FALSE
+ if(!can_grab(user, target))
+ owner.balloon_alert(user, span_warning("Cannot grab target!"))
+ return FALSE
+
+ grab_atom(target)
+ return TRUE
+ // Punt if we are holding something.
+ punt_held(user, target)
+ return TRUE
+
+ return FALSE
+
+/datum/action/cooldown/power/psyker/telekinesis/Grant(mob/granted_to)
+ . = ..()
+ if(resonant)
+ RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+/datum/action/cooldown/power/psyker/telekinesis/Remove(mob/removed_from)
+ . = ..()
+ if(resonant)
+ UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
+
+/// Calculates the stres cost of vairous interactions.
+/datum/action/cooldown/power/psyker/telekinesis/proc/get_stress_cost_for_atom(atom/target)
+ var/cost
+ // You shouldn't get as stressed from picking up a pen as a closet.
+ if(isitem(target))
+ var/obj/item/tk_item = target
+ switch(tk_item.w_class)
+ if(WEIGHT_CLASS_TINY)
+ cost = PSYKER_STRESS_TRIVIAL
+ if(WEIGHT_CLASS_SMALL)
+ cost = PSYKER_STRESS_TRIVIAL * 2
+ if(WEIGHT_CLASS_NORMAL)
+ cost = PSYKER_STRESS_TRIVIAL * 4
+ if(WEIGHT_CLASS_BULKY)
+ cost = PSYKER_STRESS_MINOR * 0.8
+ else
+ cost = PSYKER_STRESS_MINOR // structures, superheavy things, basically anything that goes beyond w_class.
+
+ return cost
+
+// Important note; because we use the action's proccess, we override cooldown processing.
+/datum/action/cooldown/power/psyker/telekinesis/process(seconds_per_tick)
+ var/mob/living/user = owner
+ if(!grabbed_atom || !user?.client)
+ STOP_PROCESSING(SSfastprocess, src)
+ return
+
+ if(INCAPACITATED_IGNORING(user, INCAPABLE_GRAB))
+ clear_grab()
+ return
+
+ if(!range_check(user, grabbed_atom))
+ to_chat(user, span_warning("Out of range!"))
+ clear_grab()
+ return
+
+ if(kinesis_catcher?.mouse_params)
+ kinesis_catcher.calculate_params()
+ if(!kinesis_catcher?.given_turf)
+ return
+
+ var/turf/target_turf = kinesis_catcher.given_turf
+ if(!target_turf)
+ return
+
+ // Dragging along the floor
+ if(grabbed_atom.loc != target_turf)
+ var/turf/next_turf = get_step_towards(grabbed_atom, target_turf)
+
+ if(grabbed_atom.Move(next_turf, get_dir(grabbed_atom, next_turf), 8))
+ // If the item is in our space, do we scoop it up?
+ if(isitem(grabbed_atom) && (user in next_turf))
+ var/obj/item/grabbed_item = grabbed_atom
+ clear_grab(playsound = FALSE)
+ grabbed_item.pickup(user)
+ user.put_in_hands(grabbed_item)
+ return
+
+
+ modify_stress(PSYKER_STRESS_TRIVIAL * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever.
+
+/// The fun part, punting shit.
+/datum/action/cooldown/power/psyker/telekinesis/proc/punt_held(mob/living/user, atom/target)
+ if(!grabbed_atom)
+ return
+
+ // Where are we throwing it?
+ var/turf/throw_turf = target ? get_turf(target) : null
+
+ // If target didn't resolve (common on middle click), derive turf from cursor catcher
+ if(!throw_turf && kinesis_catcher)
+ kinesis_catcher.calculate_params()
+ throw_turf = kinesis_catcher.given_turf
+
+ if(!throw_turf)
+ owner.balloon_alert(user, span_warning("No target!"))
+ return
+
+ var/atom/movable/launched = grabbed_atom
+
+ // Basically the same stress cost for picking it up.
+ modify_stress(get_stress_cost_for_atom(launched))
+
+ clear_grab(playsound = FALSE)
+ playsound(launched, 'sound/effects/magic/repulse.ogg', 75, TRUE)
+
+ launched.throw_at(
+ throw_turf,
+ range = grab_range,
+ speed = (launched.density ? 3 : 4),
+ thrower = user,
+ spin = isitem(launched)
+ )
+
+/// The proverbial leash.
+/datum/action/cooldown/power/psyker/telekinesis/proc/range_check(mob/living/user, atom/target)
+ if(!user || !isturf(user.loc))
+ return FALSE
+ if(ismovable(target) && !isturf(target.loc))
+ return FALSE
+ return (target in view(grab_range, user))
+
+/// Can we ACTUALLY grab it or will it just fizz out?
+/datum/action/cooldown/power/psyker/telekinesis/proc/can_grab(mob/living/user, atom/target)
+ if(user == target)
+ return FALSE
+ if(!ismovable(target))
+ return FALSE
+ if(iseffect(target))
+ return FALSE
+
+ var/atom/movable/movable_target = target
+ if(movable_target.anchored)
+ return FALSE
+ if(movable_target.throwing)
+ return FALSE
+ if(movable_target.move_resist >= MOVE_FORCE_OVERPOWERING)
+ return FALSE
+
+ if(ismob(movable_target))
+ if(!isliving(movable_target))
+ return FALSE
+ var/mob/living/living_target = movable_target
+ if(living_target.buckled)
+ return FALSE
+ if(living_target.stat < stat_required)
+ return FALSE
+ else if(isitem(movable_target))
+ var/obj/item/item_target = movable_target
+ if(item_target.w_class >= WEIGHT_CLASS_GIGANTIC)
+ return FALSE
+ if(item_target.item_flags & ABSTRACT)
+ return FALSE
+
+ return TRUE
+
+/// Attempts to grab a target atom
+/datum/action/cooldown/power/psyker/telekinesis/proc/grab_atom(atom/movable/target)
+ // If anything was already held, clear it first
+ if(grabbed_atom)
+ clear_grab(playsound = FALSE)
+ grabbed_atom = target
+ active = TRUE
+
+ // Mob handling like module_kinesis
+ if(isliving(grabbed_atom))
+ grabbed_atom.add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), REF(src))
+ RegisterSignal(grabbed_atom, COMSIG_MOB_STATCHANGE, PROC_REF(on_statchange))
+
+ ADD_TRAIT(grabbed_atom, TRAIT_NO_FLOATING_ANIM, REF(src))
+ RegisterSignal(grabbed_atom, COMSIG_MOVABLE_SET_ANCHORED, PROC_REF(on_setanchored))
+ RegisterSignal(grabbed_atom, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+ playsound(grabbed_atom, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ kinesis_icon = mutable_appearance(
+ icon = 'icons/effects/effects.dmi',
+ icon_state = "psychic",
+ layer = grabbed_atom.layer - 0.1,
+ appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART
+ )
+ player_icon = mutable_appearance(
+ icon = 'icons/effects/effects.dmi',
+ icon_state = "purplesparkles",
+ layer = owner.layer - 0.1,
+ appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART
+ )
+ grabbed_atom.add_overlay(kinesis_icon)
+ owner.add_overlay(player_icon)
+
+ // Even though the modsuit catcher is global, we want our own so we can tweak the visuals.
+ if(!kinesis_catcher)
+ kinesis_catcher = owner.overlay_fullscreen("psyker_tk", /atom/movable/screen/fullscreen/cursor_catcher/kinesis/psyker_tk, 0)
+ kinesis_catcher.assign_to_mob(owner)
+
+ // Amounts are in the get_stress_cost_for_atom
+ modify_stress(get_stress_cost_for_atom(target))
+
+ START_PROCESSING(SSfastprocess, src)
+
+/// Ends the currently ongoing grab on a target.
+/datum/action/cooldown/power/psyker/telekinesis/proc/clear_grab(playsound = TRUE)
+ active = FALSE
+ if(!grabbed_atom)
+ // Still ensure the fullscreen overlay is gone if we somehow desynced
+ if(owner)
+ owner.clear_fullscreen("psyker_tk")
+ kinesis_catcher = null
+ kinesis_icon = null
+ STOP_PROCESSING(SSfastprocess, src)
+ return
+
+ // Hold a stable ref so we can safely null grabbed_atom early
+ var/atom/movable/held = grabbed_atom
+ grabbed_atom = null
+
+ if(playsound)
+ playsound(held, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+
+ STOP_PROCESSING(SSfastprocess, src)
+
+ UnregisterSignal(held, list(COMSIG_MOB_STATCHANGE, COMSIG_MOVABLE_SET_ANCHORED, COMSIG_ATOM_DISPEL))
+
+ // Remove overlay BEFORE deleting vars
+ if(kinesis_icon)
+ held.cut_overlay(kinesis_icon)
+ kinesis_icon = null
+ if(player_icon)
+ owner.cut_overlay(player_icon)
+ player_icon = null
+
+ if(isliving(held))
+ held.remove_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), REF(src))
+
+ REMOVE_TRAIT(held, TRAIT_NO_FLOATING_ANIM, REF(src))
+
+ // Clear our telekinesis-specific screen overlay
+ if(owner)
+ owner.clear_fullscreen("psyker_tk")
+ kinesis_catcher = null
+
+/// Tells the grab that the mob's state has changed and ends the grab if it becomes invalid.
+/datum/action/cooldown/power/psyker/telekinesis/proc/on_statchange(mob/grabbed_mob, new_stat)
+ SIGNAL_HANDLER
+ if(new_stat < stat_required)
+ clear_grab()
+
+/// Tells the grab that the target has become anchored and to tend the grab
+/datum/action/cooldown/power/psyker/telekinesis/proc/on_setanchored(atom/movable/grabbed_atom_ref, anchorvalue)
+ SIGNAL_HANDLER
+ if(grabbed_atom_ref.anchored)
+ clear_grab()
+
+/// On dispel, drop the thing.
+/datum/action/cooldown/power/psyker/telekinesis/proc/on_dispel(atom/source, atom/dispeller)
+ SIGNAL_HANDLER
+ if(grabbed_atom)
+ clear_grab()
+ return DISPEL_RESULT_DISPELLED
+ return NONE
+
+
+/* ------------------------------------------------------------
+// Telekinesis-only screen edge
+// We do this so we can tweak the actual looks of the overlay.
+ ------------------------------------------------------------ */
+/atom/movable/screen/fullscreen/cursor_catcher/kinesis/psyker_tk
+ icon_state = "kinesis"
+ alpha = 180
+ color = "#8A2BE2"
+ mouse_opacity = MOUSE_OPACITY_OPAQUE
+
+#undef TK_CLICK_NONE
+#undef TK_CLICK_TRIGGER
+#undef TK_CLICK_MIDDLE
+#undef TK_CLICK_RIGHT
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm
new file mode 100644
index 00000000000000..2d8d343938e21f
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm
@@ -0,0 +1,235 @@
+
+// Telepathy, basically lifted from the mutation.
+
+#define TELE_CLICK_NONE 0
+#define TELE_CLICK_LEFT 1
+#define TELE_CLICK_RIGHT 2
+/datum/power/psyker_power/telepathy
+ name = "Telepathy"
+ desc = "Allows you to mentally communicate messages to targets. Generates a very small amount of stress. Has a speech-bubble that can be toggled on and off using middle click."
+ security_record_text = "Subject can initiate one-way communication with a target telepathically."
+ value = 1
+ required_powers = list(/datum/power/psyker_root)
+ action_path = /datum/action/cooldown/power/psyker/telepathy
+
+/datum/action/cooldown/power/psyker/telepathy
+ name = "Telepathy"
+ desc = "Allows you to mentally communicate messages to the target. Middle click to toggle speech-bubble while typing."
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "telepathy"
+ click_to_activate = TRUE
+ target_self = FALSE
+ target_range = 12
+ target_type = /mob/living
+
+ /// The message we send to the target.
+ var/message
+ /// The span surrounding the telepathy message
+ var/telepathy_span = "notice"
+ /// The bolded span surrounding the telepathy message
+ var/bold_telepathy_span = "boldnotice"
+ /// Whether access to area telepathy (right click) is enabled.
+ var/aoe_enabled = FALSE
+ /// Which mouse click is used in use_action
+ var/tele_click_type = 0
+ /// Whether to show a speech-bubble while typing a telepathy message.
+ var/show_typing_bubble = FALSE
+ /// Active telepathy typing bubble overlay.
+ var/tmp/mutable_appearance/telepathy_typing_bubble
+ /// Timer id for delayed bubble removal.
+ var/tmp/telepathy_remove_timer
+
+/datum/action/cooldown/power/psyker/telepathy/InterceptClickOn(mob/living/clicker, params, atom/target)
+ var/list/mods = params2list(params)
+ // toggles between telepathy bubble
+ if(LAZYACCESS(mods, MIDDLE_CLICK))
+ show_typing_bubble = !show_typing_bubble
+ if(show_typing_bubble)
+ clicker.balloon_alert(clicker, "Typing bubble enabled")
+ else
+ clicker.balloon_alert(clicker, "Typing bubble disabled")
+ stop_telepathy_typing_overlay(clicker, FALSE) // turns it off instantly if needed
+ return TRUE
+ if(LAZYACCESS(mods, RIGHT_CLICK))
+ if(!aoe_enabled)
+ return FALSE
+ tele_click_type = TELE_CLICK_RIGHT
+ // We need a valid living target to proceed, so we basically forcefully get any valid target in range.
+ target = get_aoe_dummy_target(clicker)
+ else
+ tele_click_type = TELE_CLICK_LEFT
+ . = ..()
+ if(!.)
+ tele_click_type = TELE_CLICK_NONE
+ return TRUE // always return true to consume the click
+
+/datum/action/cooldown/power/psyker/telepathy/use_action(mob/living/user, atom/target)
+ // Sets teh click type.
+ var/click_type = tele_click_type
+ tele_click_type = TELE_CLICK_NONE
+ if(click_type == TELE_CLICK_RIGHT)
+ return send_area_thought(user)
+
+ // define mob and set message
+ var/mob/living/cast_on = target
+ if(show_typing_bubble)
+ start_telepathy_typing_overlay(user)
+ message = tgui_input_text(user, "What do you wish to whisper to [cast_on]?", "[src]", max_length = MAX_MESSAGE_LEN)
+ // if anything happens before we finish typing the message.
+ if(QDELETED(src) || QDELETED(user) || QDELETED(cast_on))
+ stop_telepathy_typing_overlay(user, FALSE)
+ return FALSE
+
+ // out of range
+ if(target_range && get_dist(user, cast_on) > target_range)
+ user.balloon_alert(user, "they're too far!")
+ stop_telepathy_typing_overlay(user, FALSE)
+ return FALSE
+ // no message
+ if(!message)
+ stop_telepathy_typing_overlay(user, FALSE)
+ return FALSE
+
+ send_thought(user, cast_on, message)
+ stop_telepathy_typing_overlay(user, TRUE)
+ return TRUE
+
+/datum/action/cooldown/power/psyker/telepathy/on_action_success(mob/living/user, atom/target)
+ modify_stress(PSYKER_STRESS_TRIVIAL)
+ return ..()
+
+/// Picks a valid mob in view to satisfy target checks for area telepathy; doubles as a check to see if we even have anyone to telepathy to.
+/datum/action/cooldown/power/psyker/telepathy/proc/get_aoe_dummy_target(mob/living/user)
+ var/list/targets = list()
+ for(var/mob/living/target in view(user))
+ if(target == user)
+ continue
+ if(mental && !can_affect_mental(target))
+ continue
+ targets += target
+
+ if(!length(targets))
+ return null
+ return pick(targets)
+
+/// Singular transmission
+/datum/action/cooldown/power/psyker/telepathy/proc/send_thought(mob/living/caster, mob/living/target, message, disable_feedback = FALSE)
+ log_directed_talk(caster, target, message, LOG_SAY, name)
+
+ var/formatted_message = "[message]"
+ target.balloon_alert(target, "you hear a voice")
+ to_chat(target, "You hear a voice in your head... [formatted_message]")
+
+ if(!disable_feedback) // So that the AoE version doesnt spam your chat log.
+ to_chat(caster, "You transmit to [target]: [formatted_message]")
+ send_ghost_message(caster, target, formatted_message)
+
+
+/// AoE transmission
+/datum/action/cooldown/power/psyker/telepathy/proc/send_area_thought(mob/living/user)
+ if(show_typing_bubble)
+ start_telepathy_typing_overlay(user)
+ message = tgui_input_text(user, "What do you wish to whisper to everyone in view?", "[src]", max_length = MAX_MESSAGE_LEN)
+ if(QDELETED(src) || QDELETED(user))
+ stop_telepathy_typing_overlay(user, FALSE)
+ return FALSE
+ if(!message)
+ stop_telepathy_typing_overlay(user, FALSE)
+ return FALSE
+
+ // We need to revalidate targeting on each person; you shouldn't be able to whisper to mental or magic immune people
+ var/list/targets = list()
+ for(var/mob/living/target in view(user))
+ if(target == user)
+ continue
+ if(mental && !can_affect_mental(target))
+ continue
+ targets += target
+
+ if(!length(targets))
+ stop_telepathy_typing_overlay(user, FALSE)
+ user.balloon_alert(user, "no minds in view!")
+ return FALSE
+
+ var/formatted_message = "[message]"
+ to_chat(user, "You broadcast to everyone in view: [formatted_message]")
+ send_ghost_message(user, null, formatted_message, area_broadcast = TRUE)
+
+ // basically goes through send_thought for each target
+ for(var/mob/living/target as anything in targets)
+ send_thought(user, target, message, disable_feedback = TRUE)
+ stop_telepathy_typing_overlay(user, TRUE)
+ return TRUE
+
+/// Tells the ghosts that telepathy talk is happening.
+/datum/action/cooldown/power/psyker/telepathy/proc/send_ghost_message(mob/living/caster, mob/living/target, formatted_message, area_broadcast = FALSE)
+ for(var/mob/dead/ghost as anything in GLOB.dead_mob_list)
+ if(!isobserver(ghost))
+ continue
+
+ var/from_link = FOLLOW_LINK(ghost, caster)
+ var/from_mob_name = "[caster] [src]"
+ from_mob_name += ":"
+ var/to_link = ""
+ var/to_mob_name
+ if(area_broadcast)
+ to_mob_name = span_name("area")
+ else
+ to_link = FOLLOW_LINK(ghost, target)
+ to_mob_name = span_name("[target]")
+
+ to_chat(ghost, "[from_link] [from_mob_name] [formatted_message] [to_link] [to_mob_name]")
+
+/// Starts a separate typing bubble overlay while the telepathy prompt is open.
+/datum/action/cooldown/power/psyker/telepathy/proc/start_telepathy_typing_overlay(mob/living/user)
+ if(!user || QDELETED(user))
+ return FALSE
+ if(HAS_TRAIT(user, TRAIT_THINKING_IN_CHARACTER) || user.active_typing_indicator || user.active_thinking_indicator)
+ return FALSE
+ if(telepathy_remove_timer)
+ deltimer(telepathy_remove_timer)
+ telepathy_remove_timer = null
+ if(telepathy_typing_bubble)
+ user.cut_overlay(telepathy_typing_bubble) // cut the old to force a sprite update
+ telepathy_typing_bubble.icon_state = "default3"
+ telepathy_typing_bubble.color = COLOR_LIGHT_PINK
+ telepathy_typing_bubble.appearance_flags = RESET_COLOR | KEEP_APART
+ user.add_overlay(telepathy_typing_bubble)
+ return TRUE
+ telepathy_typing_bubble = mutable_appearance('icons/mob/effects/talk.dmi', "default3", MOB_LAYER + 1, appearance_flags = RESET_COLOR | KEEP_APART)
+ telepathy_typing_bubble.color = COLOR_LIGHT_PINK
+ user.add_overlay(telepathy_typing_bubble)
+ return TRUE
+
+/// Stops the separate typing bubble overlay.
+/datum/action/cooldown/power/psyker/telepathy/proc/stop_telepathy_typing_overlay(mob/living/user, sent_message)
+ if(!user || QDELETED(user))
+ return
+ if(!telepathy_typing_bubble)
+ return
+ if(!sent_message) // if we didnt send a message
+ if(telepathy_remove_timer)
+ deltimer(telepathy_remove_timer)
+ telepathy_remove_timer = null
+ user.cut_overlay(telepathy_typing_bubble)
+ telepathy_typing_bubble = null
+ return
+ // if we did send a message
+ user.cut_overlay(telepathy_typing_bubble) // cut the old to force a sprite update
+ telepathy_typing_bubble.icon_state = "default0"
+ telepathy_typing_bubble.color = COLOR_LIGHT_PINK
+ telepathy_typing_bubble.appearance_flags = RESET_COLOR | KEEP_APART
+ user.add_overlay(telepathy_typing_bubble) // reapply to update.
+ telepathy_remove_timer = addtimer(CALLBACK(src, PROC_REF(finalize_telepathy_typing_overlay), user, telepathy_typing_bubble), 2.5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE)
+
+/// Removes the telepathy typing bubble overlay after the linger delay, if still applicable.
+/datum/action/cooldown/power/psyker/telepathy/proc/finalize_telepathy_typing_overlay(mob/living/user, mutable_appearance/bubble)
+ telepathy_remove_timer = null
+ if(!user || QDELETED(user))
+ return
+ if(!telepathy_typing_bubble || telepathy_typing_bubble != bubble)
+ return
+ if(telepathy_typing_bubble.icon_state != "default0") // we've started typing a new message.
+ return
+ user.cut_overlay(telepathy_typing_bubble)
+ telepathy_typing_bubble = null
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm
new file mode 100644
index 00000000000000..69db8a9bf1f655
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm
@@ -0,0 +1,22 @@
+/datum/power/psyker_power/telepathy_area
+ name = "Telepathy Area"
+ desc = "Allows you to right click with your telepathy power to send the message to all creatures currently in view!"
+ security_record_text = "Subject can initiate one-way communication with all visible targets."
+ value = 1
+ required_powers = list(/datum/power/psyker_power/telepathy)
+
+/datum/power/psyker_power/telepathy_area/post_add()
+ . = ..()
+ var/datum/power/psyker_power/telepathy/telepathy_power = power_holder.get_power(/datum/power/psyker_power/telepathy)
+ var/datum/action/cooldown/power/psyker/telepathy/telepathy_action = telepathy_power?.action_path
+ if(telepathy_action)
+ telepathy_action.aoe_enabled = TRUE
+ telepathy_action.desc = "Allows you to mentally communicate messages to the target. Left click to send the message to one target, right click to all targets in view, middle click to toggle speech-bubble while typing. ."
+
+/datum/power/psyker_power/telepathy_area/remove()
+ . = ..()
+ var/datum/power/psyker_power/telepathy/telepathy_power = power_holder.get_power(/datum/power/psyker_power/telepathy)
+ var/datum/action/cooldown/power/psyker/telepathy/telepathy_action = telepathy_power?.action_path
+ if(telepathy_action)
+ telepathy_action.aoe_enabled = FALSE
+ telepathy_action.desc = initial(telepathy_action.desc)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/ward_mind.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/ward_mind.dm
new file mode 100644
index 00000000000000..206ed03e7b710b
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/ward_mind.dm
@@ -0,0 +1,119 @@
+/datum/power/psyker_power/ward_mind
+ name = "Ward Mind"
+ desc = "Temporarily strengthens your mind to block out mental assaults. You become immune to various abilities that use the mental trait as well as resonance-based detection, such as a large variety of Psyker powers but also a handful of other interactions.\
+ \n You gain Stress passively while active, and a moderate amount is gained whenever you block a mental ability."
+ security_record_text = "Subject can ward their mind against mental assault and scrying."
+ value = 5
+ required_powers = list(/datum/power/psyker_root)
+ action_path = /datum/action/cooldown/power/psyker/ward_mind
+
+/datum/action/cooldown/power/psyker/ward_mind
+ name = "Ward Mind"
+ desc = "Temporarily strengthens your mind to block out mental assaults. You become immune to various abilities that use the mental trait as well as resonance-based detection, such as a large variety of Psyker powers but also a handful of other interactions.\
+ \n You gain Stress passively while active, and a moderate amount is gained whenever you block a mental ability."
+ button_icon = 'icons/mob/actions/actions_elites.dmi'
+ button_icon_state = "magic_box"
+ cooldown_time = 15 SECONDS
+
+ /// The status effect on the caster.
+ var/datum/status_effect/power/ward_mind/active_effect
+
+/datum/action/cooldown/power/psyker/ward_mind/Remove(mob/removed_from)
+ . = ..()
+ if(active_effect)
+ qdel(active_effect)
+ active_effect = null
+ active = FALSE
+
+/datum/action/cooldown/power/psyker/ward_mind/use_action(mob/living/user, atom/target)
+ if(active_effect)
+ qdel(active_effect)
+ active_effect = null
+ active = FALSE
+ to_chat(user, span_notice("You let your mental guard down."))
+ else
+ active_effect = user.apply_status_effect(/datum/status_effect/power/ward_mind, src)
+ active = TRUE
+ to_chat(user, span_notice("Your mind wards against intrusion."))
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+ return TRUE
+
+/datum/status_effect/power/ward_mind
+ id = "ward_mind"
+ alert_type = /atom/movable/screen/alert/status_effect/ward_mind
+ duration = STATUS_EFFECT_PERMANENT
+ tick_interval = 1 SECONDS
+ processing_speed = STATUS_EFFECT_FAST_PROCESS
+
+ /// Stress per blocked antimagic charge.
+ var/stress_per_charge = PSYKER_STRESS_MODERATE
+ /// Per-second upkeep while active.
+ var/stress_per_second = PSYKER_STRESS_TRIVIAL * 1.5
+ /// Reference to the ward mind action.
+ var/datum/action/cooldown/power/psyker/ward_mind/source_action
+
+/atom/movable/screen/alert/status_effect/ward_mind
+ name = "Ward Mind"
+ desc = "You are immune to resonance-based detection and mental effects; but you passively generate stress."
+ icon = 'icons/mob/actions/actions_elites.dmi'
+ icon_state = "magic_box"
+
+
+/datum/status_effect/power/ward_mind/on_creation(mob/living/new_owner, datum/action/cooldown/power/psyker/ward_mind/passed_action)
+ . = ..()
+ source_action = passed_action
+
+/datum/status_effect/power/ward_mind/on_apply()
+ if(!owner)
+ return FALSE
+ ADD_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, REF(src))
+ RegisterSignal(owner, COMSIG_MOB_RECEIVE_MAGIC, PROC_REF(on_receive_magic), override = TRUE)
+ RegisterSignal(owner, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+ if(source_action)
+ source_action.active = TRUE
+ source_action.active_effect = src
+ source_action.build_all_button_icons(UPDATE_BUTTON_STATUS)
+ return TRUE
+
+/datum/status_effect/power/ward_mind/on_remove()
+ if(owner)
+ UnregisterSignal(owner, COMSIG_MOB_RECEIVE_MAGIC)
+ UnregisterSignal(owner, COMSIG_ATOM_DISPEL)
+ REMOVE_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, REF(src))
+ if(source_action)
+ source_action.active = FALSE
+ source_action.active_effect = null
+ source_action.build_all_button_icons(UPDATE_BUTTON_STATUS)
+ return
+
+/// When any magical effect attempts to interact with us, attempt to block it if its mental.
+/datum/status_effect/power/ward_mind/proc/on_receive_magic(mob/living/carbon/source, casted_magic_flags, charge_cost, list/antimagic_sources)
+ SIGNAL_HANDLER
+ if(!(casted_magic_flags & MAGIC_RESISTANCE_MIND))
+ return NONE
+ var/obj/item/organ/resonant/psyker/psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER)
+ if(!psyker_organ)
+ return NONE
+ adjust_stress_from_block(psyker_organ, charge_cost)
+ if(psyker_organ.stress >= psyker_organ.stress_threshold)
+ return NONE
+ antimagic_sources += owner
+ return COMPONENT_MAGIC_BLOCKED
+
+/// Stresses out the psyker based on what we blocked and its charge cost.
+/datum/status_effect/power/ward_mind/proc/adjust_stress_from_block(obj/item/organ/resonant/psyker/psyker_organ, charge_cost)
+ if(!isnum(charge_cost) || charge_cost <= 0)
+ return
+ psyker_organ.modify_stress(charge_cost * stress_per_charge)
+
+/// On dispel, end the status effect.
+/datum/status_effect/power/ward_mind/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+ qdel(src)
+ return DISPEL_RESULT_DISPELLED
+
+/datum/status_effect/power/ward_mind/tick(seconds_between_ticks)
+ if(!source_action || QDELETED(source_action))
+ qdel(src)
+ return
+ source_action.modify_stress(stress_per_second * seconds_between_ticks)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm
new file mode 100644
index 00000000000000..1499dba5e1014c
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm
@@ -0,0 +1,15 @@
+/datum/brain_trauma/magic/resonance_silenced
+ name = "Aresonaphasia"
+ desc = "Patient is unable to wield their own Resonant powers."
+ scan_desc = "resonance silence"
+ gain_text = span_notice("You feel like you're no longer in touch with your own Resonant powers.")
+ lose_text = span_notice("You begin to feel your Resonant Powers returning.")
+
+/datum/brain_trauma/magic/resonance_silenced/on_gain()
+ owner.dispel(src)
+ ADD_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAUMA_TRAIT)
+ . = ..()
+
+/datum/brain_trauma/magic/resonance_silenced/on_lose()
+ REMOVE_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAUMA_TRAIT)
+ ..()
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_chalks.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_chalks.dm
new file mode 100644
index 00000000000000..fb92488e995136
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_chalks.dm
@@ -0,0 +1,231 @@
+#define CAT_ENIGMATIST "Enigmatist"
+
+/**
+ * Base Enigmatist Chalk
+ */
+
+/* Though crass, I am commenting this out largely because this will get development later.
+
+// TODO: make a basic enigmatist chalk item.
+// TODO: using it sends a signal to the user and collects a list of powers there.
+// Signal probably includes the type of chalk.
+
+/obj/item/enigmatist_chalk
+ name = "enigmatist chalk"
+ desc = "An abstract chalk item. Looks tasty. Mmmm... \
+ Wait it's abstract in the coding sense. Quick, report it!"
+ icon = 'icons/obj/art/crayons.dmi'
+ icon_state = "crayonwhite"
+ worn_icon_state = "crayon"
+
+ // stats parallel to crayons
+ w_class = WEIGHT_CLASS_TINY
+ force = 0
+ throwforce = 0
+ throw_speed = 3
+ throw_range = 7
+ attack_verb_continuous = list("attacks", "colours")
+ attack_verb_simple = list("attack", "colour")
+ grind_results = list()
+ interaction_flags_atom = parent_type::interaction_flags_atom | INTERACT_ATOM_IGNORE_MOBILITY
+
+ /// Bitflag of which types of enigmatist powers this can invoke.
+ var/enigmatist_flags = NONE
+ /// Our current integrity. When this reaches <0, we break.
+ var/resonant_integrity = ENIGMATIST_CHALK_STANDARD_INTEGRITY
+ /// Our maximum integrity.
+ var/max_resonant_integrity = ENIGMATIST_CHALK_STANDARD_INTEGRITY
+ /// The currently selected enigmatist power, if any.
+ var/datum/weakref/current_selected_power_ref
+
+/obj/item/enigmatist_chalk/Initialize(mapload)
+ . = ..()
+ register_context()
+ register_item_context()
+
+/obj/item/enigmatist_chalk/Destroy(force)
+ current_selected_power_ref = null
+ return ..()
+
+
+/obj/item/enigmatist_chalk/examine(mob/user)
+ . = ..()
+ . += span_notice("It's at [EXAMINE_HINT("[integrity_percent()]%")] integrity.")
+
+/obj/item/enigmatist_chalk/add_context(
+ atom/source,
+ list/context,
+ obj/item/held_item,
+ mob/user,
+)
+ var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve()
+ if(isnull(current_selected_power))
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Select spell"
+ return CONTEXTUAL_SCREENTIP_SET
+
+ if(current_selected_power.power_holder == user)
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Reset selection"
+ else
+ context[SCREENTIP_CONTEXT_ALT_LMB] = "Select spell"
+ current_selected_power.chalk_add_context(src, context, held_item, user)
+ return CONTEXTUAL_SCREENTIP_SET
+
+/obj/item/enigmatist_chalk/add_item_context(
+ obj/item/source,
+ list/context,
+ atom/target,
+ mob/living/user,
+)
+ var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve()
+ if(isnull(current_selected_power))
+ return NONE
+ return current_selected_power.chalk_add_item_context(src, context, target, user)
+
+
+/obj/item/enigmatist_chalk/attack_self(mob/user, modifiers)
+ . = ..()
+ var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve()
+ if(isnull(current_selected_power))
+ return
+ return current_selected_power.chalk_attack_self(src, user, modifiers)
+
+/obj/item/enigmatist_chalk/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
+ var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve()
+ if(isnull(current_selected_power))
+ return NONE
+ return current_selected_power.chalk_interact_with_atom(src, interacting_with, user, modifiers)
+
+/obj/item/enigmatist_chalk/interact_with_atom_secondary(atom/interacting_with, mob/living/user, list/modifiers)
+ var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve()
+ if(isnull(current_selected_power))
+ return NONE
+ return current_selected_power.chalk_interact_with_atom_secondary(src, interacting_with, user, modifiers)
+
+
+/obj/item/enigmatist_chalk/click_alt(mob/user)
+ var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve()
+ if(current_selected_power)
+ current_selected_power_ref = null
+ current_selected_power.chalk_reset_spell(user, src)
+ // Act as if nothing happened only if our spell wasn't set by our previous user.
+ if(current_selected_power.power_holder == user)
+ balloon_alert(user, "selection reset")
+ return CLICK_ACTION_SUCCESS
+
+ var/list/spell_options = list()
+ SEND_SIGNAL(user, COMSIG_ENIGMATIST_CHALK_SELECTION, enigmatist_flags, spell_options)
+ if(!length(spell_options))
+ balloon_alert(user, "you know nothing!")
+ return CLICK_ACTION_BLOCKING
+
+ var/list/radial_items = list()
+ for(var/spell_name as anything in spell_options)
+ var/datum/power/enigmatist_spell/spell_option = spell_options[spell_name]
+ var/datum/radial_menu_choice/radial_option = new()
+ radial_option.name = spell_option.get_option_name()
+ radial_option.info = spell_option.get_option_desc()
+ radial_option.image = image(icon = spell_option.option_icon, icon_state = spell_option.option_icon_state)
+ radial_items[spell_option.name] = radial_option
+ sort_list(radial_items)
+
+ message_admins("click_alt PRE-RADIAL - radial_items: [radial_items] spell_options: [spell_options]")
+ for(var/radial_name as anything in radial_items)
+ message_admins("click_alt PRE-RADIAL-LOOP - radial_name: [radial_name] entry: [radial_items[radial_name]]")
+
+ var/chosen_option = show_radial_menu(user, src, radial_items, custom_check = CALLBACK(src, PROC_REF(check_selection_menu), user), require_near = TRUE, tooltips = TRUE)
+ message_admins("click_alt POST RADIAL - chosen_option: [chosen_option]")
+ if(isnull(chosen_option))
+ return CLICK_ACTION_BLOCKING
+ var/datum/power/enigmatist_spell/chosen_spell = spell_options[chosen_option]
+ if(isnull(chosen_spell))
+ return CLICK_ACTION_BLOCKING
+
+ current_selected_power_ref = WEAKREF(chosen_spell)
+ chosen_spell.chalk_selected_spell(user, src)
+ balloon_alert(user, "spell selected")
+ return CLICK_ACTION_SUCCESS
+
+/// C
+/obj/item/enigmatist_chalk/proc/check_selection_menu(mob/user)
+ if(QDELETED(src))
+ return FALSE
+ if(!istype(user))
+ return FALSE
+ if(user.incapacitated)
+ return FALSE
+ return TRUE
+
+
+/// Gets the percentage of its maximum our current integrity is at.
+/obj/item/enigmatist_chalk/proc/integrity_percent()
+ return PERCENT(resonant_integrity / max_resonant_integrity)
+
+/// Try to use a given amount of integrity. If we don't have enough, don't and return FALSE.
+/obj/item/enigmatist_chalk/proc/use_integrity(damage, user)
+ if(damage > resonant_integrity)
+ return FALSE
+ resonant_integrity -= damage
+ if(resonant_integrity <= 0)
+ break_chalk(user)
+ return TRUE
+
+/// Breaks the chalk! Sends feedback if given a user.
+/obj/item/enigmatist_chalk/proc/break_chalk(user)
+ if(user)
+ balloon_alert(user, "chalk shatters!")
+ // TODO: replace with remnants if possible.
+ // TODO: add sounds if possible.
+ qdel(src)
+
+/**
+ * Practical Chalk Items
+ */
+
+/obj/item/enigmatist_chalk/resonant
+ name = "resonant chalk"
+ desc = "A stark-white stick of chalk. \
+ Its texture shifts as you turn it."
+ icon_state = "crayonwhite"
+ enigmatist_flags = ENIGMATIST_RESONANT
+
+/obj/item/enigmatist_chalk/unsealed
+ name = "unsealed chalk"
+ desc = "A stick of chalk with an odd purple hue. \
+ It doesn't obscure what's behind it."
+ icon_state = "crayonpurple"
+ enigmatist_flags = ENIGMATIST_UNSEALED
+
+/obj/item/enigmatist_chalk/illuminated
+ name = "illuminated chalk"
+ desc = "A stick of chalk with an odd yellow hue. \
+ It seems well-lit regardless of lighting."
+ icon_state = "crayonyellow"
+ enigmatist_flags = ENIGMATIST_ILLUMINATED
+
+/obj/item/enigmatist_chalk/divided
+ name = "divided chalk"
+ desc = "A stick of chalk with an odd blue hue. \
+ Its edges look sharp no matter the angle."
+ icon_state = "crayonblue"
+ enigmatist_flags = ENIGMATIST_DIVIDED
+
+/**
+ * Resonant Chalk
+ */
+
+/datum/crafting_recipe/resonant_chalk
+ name = "Resonant Chalk"
+ result = /obj/item/enigmatist_chalk/resonant
+ reqs = list(
+ /obj/item/stack/sheet/mineral/plasma = 1,
+ /obj/item/toy/crayon = 1,
+ )
+ blacklist = list(
+ /obj/item/toy/crayon/spraycan,
+ )
+ time = 5 SECONDS
+ category = CAT_ENIGMATIST
+ crafting_flags = CRAFT_MUST_BE_LEARNED
+
+#undef CAT_ENIGMATIST
+*/
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm
new file mode 100644
index 00000000000000..95012bb78af8ae
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm
@@ -0,0 +1,10 @@
+
+/datum/power/enigmatist_root
+ name = "Coming Soon!"
+ desc = "Only time will tell."
+
+ value = 999
+
+ archetype = POWER_ARCHETYPE_SORCEROUS
+ path = POWER_PATH_ENIGMATIST
+ priority = POWER_PRIORITY_ROOT
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_spell.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_spell.dm
new file mode 100644
index 00000000000000..5f5a00cac63fca
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_spell.dm
@@ -0,0 +1,85 @@
+
+/* Enigmatist content is getting commented out for now and getting developed later.
+/datum/power/enigmatist_spell
+ name = "Abstract Enigmatist Spell"
+ desc = "The true art of seeing into the seventh dimension: \
+ seeing this debug code. Please report this!"
+ abstract_parent_type = /datum/power/enigmatist_spell
+
+ archetype = POWER_ARCHETYPE_SORCEROUS
+ path = POWER_PATH_ENIGMATIST
+ required_powers = list(/datum/power/enigmatist_root)
+
+ /// The type of Enigmatist spell this is. Use ENIGMATIST_RESONANT/X flags.
+ var/enigmatist_type = ENIGMATIST_RESONANT
+
+ /// The icon used for us in chalk radial selections.
+ var/option_icon = 'icons/obj/art/crayons.dmi'
+ /// The icon_state used for us in chalk radial selections.
+ var/option_icon_state = "crayonwhite"
+ /// The name to use in chalk radial selections instead of our basic name.
+ var/option_name
+ /// The description to use in chalk radial selections instead of our basic description.
+ var/option_desc
+
+/datum/power/enigmatist_spell/add(client/client_source)
+ RegisterSignal(power_holder, COMSIG_ENIGMATIST_CHALK_SELECTION, PROC_REF(get_spell_option))
+
+/datum/power/enigmatist_spell/remove()
+ UnregisterSignal(power_holder, COMSIG_ENIGMATIST_CHALK_SELECTION)
+
+
+/datum/power/enigmatist_spell/proc/get_option_name()
+ return option_name || name
+
+/datum/power/enigmatist_spell/proc/get_option_desc()
+ return option_desc || desc
+
+/datum/power/enigmatist_spell/proc/get_spell_option(datum/source, enigmatist_flags, list/spell_options)
+ SIGNAL_HANDLER
+ message_admins("get_spell_option - enigmatist_type: [enigmatist_type] enigmatist_flags: [enigmatist_flags] both: [enigmatist_type & enigmatist_flags]")
+ if(enigmatist_type & enigmatist_flags)
+ spell_options[name] = src
+ message_admins("get_spell_option TWO - spell_options length: [length(spell_options)]")
+
+
+/datum/power/enigmatist_spell/proc/chalk_add_context(
+ obj/item/enigmatist_chalk/held_chalk,
+ list/context,
+ obj/item/held_item,
+ mob/user,
+)
+ return NONE
+
+/datum/power/enigmatist_spell/proc/chalk_add_item_context(
+ obj/item/enigmatist_chalk/held_chalk,
+ list/context,
+ atom/target,
+ mob/living/user,
+)
+ return NONE
+
+
+/datum/power/enigmatist_spell/proc/chalk_selected_spell(mob/user, obj/item/enigmatist_chalk/used_chalk)
+ return
+
+/datum/power/enigmatist_spell/proc/chalk_reset_spell(mob/user, obj/item/enigmatist_chalk/used_chalk)
+ return
+
+
+/datum/power/enigmatist_spell/proc/chalk_attack_self(obj/item/enigmatist_chalk/used_chalk, mob/user, modifiers)
+ return
+
+/datum/power/enigmatist_spell/proc/chalk_interact_with_atom(obj/item/enigmatist_chalk/used_chalk, atom/interacting_with, mob/living/user, list/modifiers)
+ return NONE
+
+/datum/power/enigmatist_spell/proc/chalk_interact_with_atom_secondary(obj/item/enigmatist_chalk/used_chalk, atom/interacting_with, mob/living/user, list/modifiers)
+ return chalk_interact_with_atom(used_chalk, interacting_with, user, modifiers)
+
+
+/datum/power/enigmatist_spell/proc/damage_chalk(obj/item/enigmatist_chalk/used_chalk, mob/living/user, damage)
+ if(!used_chalk.use_integrity(damage, user))
+ used_chalk.balloon_alert(user, "too damaged!")
+ return FALSE
+ return TRUE
+*/
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm
new file mode 100644
index 00000000000000..6abbed8f24da71
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm
@@ -0,0 +1,27 @@
+
+/*/datum/power/enigmatist_spell/lodestone_legends
+ name = "Lodestone Legends"
+ desc = "Activate with any type of Chalk in hand to be told your GPS \
+ position. Causes minor damage to the Chalk"
+
+ value = 1
+ priority = POWER_PRIORITY_BASIC
+ // Any chalk can use us, whatsoever.
+ enigmatist_type = ENIGMATIST_ANY_ALL
+
+/datum/power/enigmatist_spell/lodestone_legends/chalk_add_context(
+ obj/item/enigmatist_chalk/held_chalk,
+ list/context,
+ obj/item/held_item,
+ mob/user,
+)
+ if(held_chalk != held_item)
+ return NONE
+ context[SCREENTIP_CONTEXT_LMB] = "Get GPS position"
+ return CONTEXTUAL_SCREENTIP_SET
+
+/datum/power/enigmatist_spell/lodestone_legends/chalk_attack_self(obj/item/enigmatist_chalk/used_chalk, mob/user, modifiers)
+ if(!damage_chalk(used_chalk, user, ENIGMATIST_CHALK_MINOR_DAMAGE))
+ return
+ var/turf/current_turf = get_turf(used_chalk)
+ to_chat(user, span_notice("Your current coordinates are... [current_turf.x]x, [current_turf.y]y, [current_turf.z]z..."))*/
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm
new file mode 100644
index 00000000000000..14311c06096314
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm
@@ -0,0 +1,218 @@
+/datum/action/cooldown/power/thaumaturge
+ name = "abstract thaumaturge power action - ahelp this"
+ background_icon_state = "bg_star"
+ overlay_icon_state = "bg_default_border"
+ button_icon = 'icons/mob/actions/backgrounds.dmi'
+
+ // We generally don't dabble with cooldowns but a cooldown of 0.5 seconds is kinda handy to prevent you from blowing your load on all your charges by accident.
+ cooldown_time = 5
+ // hides the cooldown text cause we contest the ui element location.
+ text_cooldown = FALSE
+ /// Unlike normal spells, we have charges. More of that explained below at check_if_valid()
+ var/charges = 0
+ /// The cap on charges; you can't prepare more than these. If you leave this null, the spell will not interact with the charges system.
+ var/max_charges = THAUMATURGE_MAX_CHARGES_BASE
+ /// How many charges does it consume on use?
+ var/charges_to_use = 1
+ /// How much 'mana' does it cost to prepare this per charge?
+ var/prep_cost = 0
+ /// If TRUE, this power has a chance to refund its resource use on successful cast.
+ var/power_refunds = FALSE
+ /// Base chance (percent) for refunding on cast success.
+ var/power_refund_chance = 0
+ /// Additional refund chance per affinity.
+ var/power_refund_affinity_bonus = 0
+ /// Snapshotted per-cast result of refund logic.
+ var/last_power_refunded = FALSE
+ /// If TRUE, this action uses the default charges system.
+ var/charge_mechanics = TRUE
+ /// Thaumaturge component tied to this action's owner. Used for mechanics settings.
+ var/datum/component/thaumaturge/thaumaturge_component
+
+ /// Overlay that shows the number of charges
+ var/mutable_appearance/charge_overlay
+
+ /// How much affinity is currently affecting the action. It is deliberate we snap-shot this on cast.
+ var/affinity
+ /// How much affinity is required to use the action.
+ var/required_affinity
+
+/datum/action/cooldown/power/thaumaturge/New()
+ if(max_charges)
+ disable() // prep your spells first
+ update_charges_overlay()
+
+/datum/action/cooldown/power/thaumaturge/Grant(mob/grant_to)
+ . = ..()
+ ValidateThaumaturgeComponent()
+ check_if_valid(grant_to)
+ return .
+
+/// Checks if we have our referenced thaumaturge component for mechanics/display behavior.
+/datum/action/cooldown/power/thaumaturge/proc/ValidateThaumaturgeComponent()
+ if(owner && !thaumaturge_component)
+ thaumaturge_component = owner.GetComponent(/datum/component/thaumaturge)
+ if(!thaumaturge_component)
+ return FALSE
+ return TRUE
+
+/datum/action/cooldown/power/thaumaturge/try_use(mob/living/user, atom/target)
+ ValidateThaumaturgeComponent()
+ if(!check_if_valid(user))
+ return FALSE
+ if(ishuman(user)) // We're not checking for clothes on cats
+ affinity = get_affinity(user)
+ if(affinity < required_affinity) // Do we have the minimal required affinity
+ owner.balloon_alert(user, "requires [required_affinity] affinity!")
+ return FALSE
+ // Ensures extra anti-magic flags get properly added retroactively to powers.
+ magic_resistance_types = thaumaturge_component.additional_magic_resistance_flags
+ . = ..()
+
+// The charge deduction is handled on_action_success and thusly gains override_charges as an arg.
+// If your spell does anything unusual with charges such as refunds or costing multiples, this is where you would handle that.
+// You can otherwise use on_action_success as normal, just make sure to call parent.
+/datum/action/cooldown/power/thaumaturge/on_action_success(mob/living/user, atom/target, override_charges)
+ SHOULD_CALL_PARENT(TRUE)
+ . = ..()
+ ValidateThaumaturgeComponent()
+ last_power_refunded = handle_power_refund()
+ if(last_power_refunded)
+ override_charges = 0
+ to_chat(owner, span_notice("Your [name] spell did not consume a charge!"))
+
+ charge_mechanics = isnull(thaumaturge_component?.charge_mechanics) ? charge_mechanics : thaumaturge_component.charge_mechanics
+ if(charge_mechanics)
+ adjust_charges(isnull(override_charges) ? -charges_to_use : -override_charges)
+ check_if_valid(user)
+ return
+
+/// Handles refund chance for powers that support it.
+/datum/action/cooldown/power/thaumaturge/proc/handle_power_refund()
+ if(!power_refunds)
+ return FALSE
+ // Hemomancy-style roots do not use charge mechanics, so they should never roll refunds.
+ if(!isnull(thaumaturge_component?.charge_mechanics) && !thaumaturge_component.charge_mechanics)
+ return FALSE
+ var/refund_chance = clamp(power_refund_chance + (power_refund_affinity_bonus * max(affinity, 0)), 0, THAUMATURGE_REFUND_MAX)
+ if(prob(refund_chance))
+ return TRUE
+ return FALSE
+
+/// Adjusts the charge counts up to the cap and not below 0 unless overriden.
+/datum/action/cooldown/power/thaumaturge/proc/adjust_charges(amount, override_cap)
+ if(!isnum(amount))
+ return
+ var/cap_to = isnum(override_cap) ? override_cap : max_charges
+ charges = clamp(charges + amount, 0, cap_to)
+
+/*
+ Affinity system stuff here. Dress like a mage, get bonuses.
+*/
+/// Gets and reutrns a mob's current highest affinity number.
+/datum/action/cooldown/power/thaumaturge/proc/get_affinity(mob/living/user)
+ ValidateThaumaturgeComponent()
+ var/highest_affinity = 0
+
+ // If the root power benefits from dressing like a wizard.
+ if(thaumaturge_component?.affinity_benefits_from_items)
+ // Checks if you're wearing items with affinity. This has to be clothing; wearing your staff does not count.
+ var/list/equipped_items = user.get_equipped_items()
+ for(var/obj/item/equipped_item as anything in equipped_items)
+ if(!equipped_item)
+ continue
+ if(!istype(equipped_item, /obj/item/clothing) || equipped_item.affinity_worn_override)
+ continue
+
+ if(equipped_item.affinity > highest_affinity)
+ highest_affinity = equipped_item.affinity
+
+ // Checks if you're holding items with affinity.
+ for(var/obj/item/held_item as anything in user.held_items)
+ if(!held_item)
+ continue
+
+ // Holding clothing shouldn't contribute
+ if(istype(held_item, /obj/item/clothing))
+ continue
+
+ if(held_item.affinity > highest_affinity)
+ highest_affinity = held_item.affinity
+
+ // Seed affinity and let external listeners influence affinity.
+ // Generally speaking you should only add and subtract to this number.
+ affinity = highest_affinity
+ SEND_SIGNAL(user, COMSIG_THAUMATURGE_AFFINITY_QUERY, src)
+ return max(0, affinity)
+
+/*
+ Deviating massively from the original cooldown system, thaumaturge has charges they have to prepare and plan for in advance, just like the classic vanician spellcasting system.
+ Mechanically, we check if charges are 0. If so we Disable(). Otherwise, we deduct a charge and go on a short cooldown.
+*/
+
+/// Checks if we have charges to use.
+/datum/action/cooldown/power/thaumaturge/proc/check_if_valid(mob/living/user = owner)
+ ValidateThaumaturgeComponent()
+ update_charges_overlay()
+ if(!thaumaturge_component?.charge_mechanics)
+ enable()
+ return TRUE
+
+ if(charges <= 0 && max_charges) // If charges are 0 or less and it has a max_charges set.
+ disable()
+ return FALSE
+ else
+ enable()
+ return TRUE
+
+/// Handles the UI stuff.
+/datum/action/cooldown/power/thaumaturge/proc/update_charges_overlay()
+ var/atom/movable/ui_element = get_atom_moveable()
+ if(!ui_element)
+ return
+ ValidateThaumaturgeComponent()
+ var/used_mode = thaumaturge_component?.resource_display_mode || THAUMATURGE_RESOURCE_DISPLAY_CHARGES
+
+ // Usually we only show a resource number for charge-based actions.
+ // Hemomancy-style roots can still request prep_cost display even with null max_charges.
+ if(!max_charges && used_mode != THAUMATURGE_RESOURCE_DISPLAY_PREP_COST)
+ return
+
+ ui_element.cut_overlay(charge_overlay)
+ charge_overlay = new/mutable_appearance
+ charge_overlay.maptext_width = 32
+ charge_overlay.maptext_height = 16
+
+ // Bottom-left-ish
+ charge_overlay.maptext_x = 4
+ charge_overlay.maptext_y = 0
+
+ var/used_color = thaumaturge_component?.charges_color || "#ff69b4"
+ var/display_value = get_resource_display_value()
+ // Don't render "0" for prep-cost displays; zero-cost powers should look costless.
+ if(used_mode == THAUMATURGE_RESOURCE_DISPLAY_PREP_COST && display_value <= 0)
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+ return
+ charge_overlay.maptext = MAPTEXT("[display_value]")
+ ui_element.add_overlay(charge_overlay)
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+
+/// Gets the numeric value shown in the resource overlay.
+/datum/action/cooldown/power/thaumaturge/proc/get_resource_display_value()
+ ValidateThaumaturgeComponent()
+ var/used_mode = thaumaturge_component?.resource_display_mode || THAUMATURGE_RESOURCE_DISPLAY_CHARGES
+ var/used_mult = thaumaturge_component?.resource_display_multiplier || 1
+ // Prep cost a la hemomancy
+ if(used_mode == THAUMATURGE_RESOURCE_DISPLAY_PREP_COST)
+ return prep_cost * max(used_mult, 1)
+ // Charges a la spell preperation.
+ return charges * max(used_mult, 1)
+
+/// Get the moveable atom specifically for adjusting the number.
+/datum/action/cooldown/power/thaumaturge/proc/get_atom_moveable()
+ for(var/datum/hud/hud_instance as anything in viewers)
+ var/atom/movable/screen/movable/action_button/action_button_instance = viewers[hud_instance]
+ if(istype(action_button_instance, /atom/movable/screen/movable/action_button))
+ return action_button_instance
+
+
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_component.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_component.dm
new file mode 100644
index 00000000000000..75cc56428e334e
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_component.dm
@@ -0,0 +1,42 @@
+/datum/component/thaumaturge
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+
+ /// The mob we're attached to is always `parent`.
+ var/mob/living/attached_mob
+
+ /// If TRUE, thaumaturge actions use the default charges system.
+ var/charge_mechanics = TRUE
+ /// If TRUE, affinity can be gained from equipped/held items.
+ var/affinity_benefits_from_items = TRUE
+ /// Color of charges/resource text on thaumaturge actions.
+ var/charges_color = "#ffffff"
+ /// What value thaumaturge actions should render on their resource overlay.
+ var/resource_display_mode = THAUMATURGE_RESOURCE_DISPLAY_CHARGES
+ /// Multiplier used on the display, e.g if you are using a resource to cast.
+ var/resource_display_multiplier = 1
+ /// Extra flags applied to thaumaturge action anti-magic checks while this component exists.
+ var/additional_magic_resistance_flags = NONE
+
+/datum/component/thaumaturge/Initialize(mob/living/new_attached_mob)
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ attached_mob = new_attached_mob || parent
+ apply_action_magic_flags(TRUE)
+
+/datum/component/thaumaturge/Destroy(force)
+ apply_action_magic_flags(FALSE)
+ return ..()
+
+/// Adds additional antimagic flags to existing powers. Mostly use for hemomancy being UNHOLY.
+/datum/component/thaumaturge/proc/apply_action_magic_flags(add_flags = TRUE)
+ if(!attached_mob || !additional_magic_resistance_flags)
+ return
+ for(var/datum/action/action as anything in attached_mob.actions)
+ var/datum/action/cooldown/power/thaumaturge/thaumaturge_action = action
+ if(!istype(thaumaturge_action))
+ continue
+ var/base_flags = initial(thaumaturge_action.magic_resistance_types)
+ thaumaturge_action.magic_resistance_types = add_flags ? (base_flags | additional_magic_resistance_flags) : base_flags
+
+
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_hemomancy.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_hemomancy.dm
new file mode 100644
index 00000000000000..cba9d7974ac465
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_hemomancy.dm
@@ -0,0 +1,161 @@
+/datum/component/thaumaturge/hemomancy
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+
+ charge_mechanics = FALSE
+ affinity_benefits_from_items = FALSE
+ charges_color = "#c72222"
+ resource_display_mode = THAUMATURGE_RESOURCE_DISPLAY_PREP_COST
+ additional_magic_resistance_flags = MAGIC_RESISTANCE_HOLY
+ resource_display_multiplier = THAUMATURGE_HEMOMANCY_BLOOD_COST_MULTIPLIER
+
+ /// HUD element that shows current blood amount.
+ var/atom/movable/screen/hemophage/blood/blood_tracker
+ /// Multiplier for converting prep_cost into blood cost.
+ var/blood_cost_multiplier = THAUMATURGE_HEMOMANCY_BLOOD_COST_MULTIPLIER
+
+
+/datum/component/thaumaturge/hemomancy/Initialize(mob/living/new_attached_mob)
+ . = ..(new_attached_mob)
+ // Power use hook for overcast affinity rider.
+ RegisterSignal(attached_mob, COMSIG_POWER_ACTION_USED, PROC_REF(on_mob_power_action_used))
+ // Power success hook that hadnles costs
+ RegisterSignal(attached_mob, COMSIG_POWER_ACTION_SUCCESS, PROC_REF(on_mob_power_action_success))
+
+ // Procs & trackers for blood UI
+ // I'd prefer to tie it to blood volume changes but since there's no signaler for that (and unlike powers resources which auto-update UIs when incremented) we'll have to settle for life.
+ RegisterSignal(attached_mob, COMSIG_LIVING_LIFE, PROC_REF(on_owner_life))
+ RegisterSignal(attached_mob, COMSIG_MOB_LOGIN, PROC_REF(on_owner_login))
+
+ // Updates to the blood tracker
+ ensure_blood_tracker()
+ update_blood_tracker()
+
+// Removes all signalers.
+/datum/component/thaumaturge/hemomancy/Destroy(force)
+ if(attached_mob)
+ UnregisterSignal(attached_mob, list(COMSIG_POWER_ACTION_USED, COMSIG_POWER_ACTION_SUCCESS, COMSIG_LIVING_LIFE, COMSIG_MOB_LOGIN))
+ clear_blood_tracker()
+ return ..()
+
+/// Ensures the blood HUD is visible on our UI.
+/datum/component/thaumaturge/hemomancy/proc/ensure_blood_tracker()
+ // Stop if mob has no HUD.
+ if(!attached_mob?.hud_used)
+ return
+ var/datum/hud/hud_used = attached_mob.hud_used
+
+ // Always delete competing hemophage trackers so ours stays authoritative.
+ for(var/atom/movable/screen/hemophage/blood/existing_tracker as anything in hud_used.infodisplay)
+ if(existing_tracker == blood_tracker)
+ continue
+ hud_used.infodisplay -= existing_tracker
+ qdel(existing_tracker)
+
+ // If our tracker was qdeleted externally, clear stale ref.
+ if(QDELETED(blood_tracker))
+ blood_tracker = null
+
+ // Create ours if missing.
+ if(!blood_tracker)
+ blood_tracker = new /atom/movable/screen/hemophage/blood(null, hud_used)
+
+ // Ensure ours is in infodisplay.
+ if(!(blood_tracker in hud_used.infodisplay))
+ hud_used.infodisplay += blood_tracker
+
+ attached_mob.hud_used.show_hud(attached_mob.hud_used.hud_version)
+
+/// Updates the blood tracker with new numbers +
+/datum/component/thaumaturge/hemomancy/proc/update_blood_tracker()
+ if(!attached_mob || !blood_tracker)
+ return
+ var/hud_color = "#FFDDDD"
+ // Turn green if we can 'overcast'
+ if(attached_mob.blood_volume >= BLOOD_VOLUME_NORMAL * THAUMATURGE_HEMOMANCY_OVERCAST_THRESHOLD)
+ hud_color = "#83d46f"
+ // Turns red if we are in the danger zone.
+ else if(attached_mob.blood_volume <= BLOOD_VOLUME_NORMAL * THAUMATURGE_HEMOMANCY_LOW_BLOOD_THRESHOLD)
+ hud_color = "#eb6b6b"
+ blood_tracker.maptext = MAPTEXT("
[trunc(attached_mob.blood_volume)]
")
+
+/// Removes the blood tracker UI element
+/datum/component/thaumaturge/hemomancy/proc/clear_blood_tracker()
+ if(!blood_tracker)
+ return
+ if(attached_mob?.hud_used)
+ attached_mob.hud_used.infodisplay -= blood_tracker
+ QDEL_NULL(blood_tracker)
+
+/// Updates on mob life
+/datum/component/thaumaturge/hemomancy/proc/on_owner_life(mob/living/source, seconds_per_tick, times_fired)
+ SIGNAL_HANDLER
+ ensure_blood_tracker()
+ update_blood_tracker()
+
+/// Updates on owner login.
+/datum/component/thaumaturge/hemomancy/proc/on_owner_login(mob/living/source)
+ SIGNAL_HANDLER
+ ensure_blood_tracker()
+ update_blood_tracker()
+
+/// Gets the blood cost for a thaumaturge action.
+/datum/component/thaumaturge/hemomancy/proc/get_blood_cost(datum/action/cooldown/power/thaumaturge/action)
+ if(!action)
+ return 0
+ return max(0, action.prep_cost * blood_cost_multiplier)
+
+/// Subtracts the appropriate amount of blood from the mob.
+/datum/component/thaumaturge/hemomancy/proc/consume_action_cost(datum/action/cooldown/power/thaumaturge/action, mob/living/user)
+ if(!action || !user)
+ return
+ action.ValidateThaumaturgeComponent()
+ if(action.thaumaturge_component?.charge_mechanics)
+ return
+ var/blood_cost = get_blood_cost(action)
+ if(blood_cost <= 0)
+ return
+ if(user.blood_volume < blood_cost)
+ user.balloon_alert(user, "not enough blood!")
+ return
+ user.blood_volume = max(user.blood_volume - blood_cost, 0)
+
+/// During action use, repeatedly consume spell cost above threshold to boost affinity for this cast.
+/datum/component/thaumaturge/hemomancy/proc/on_mob_power_action_used(mob/living/source, datum/action/cooldown/power/action, atom/target)
+ SIGNAL_HANDLER
+ // Definitions and validations
+ var/datum/action/cooldown/power/thaumaturge/thaum_action = action
+ if(!istype(thaum_action))
+ return
+ thaum_action.ValidateThaumaturgeComponent()
+ if(thaum_action.thaumaturge_component?.charge_mechanics)
+ return
+ // Overcasting is disabled for powers that use refund mechanics.
+ if(thaum_action.power_refunds)
+ return
+
+ // Gets the cost of the ability
+ var/spell_cost = get_blood_cost(thaum_action)
+ if(spell_cost <= 0)
+ return
+
+ // Sets the overcast threshold
+ var/overcast_threshold = BLOOD_VOLUME_NORMAL * THAUMATURGE_HEMOMANCY_OVERCAST_THRESHOLD
+ if(source.blood_volume < overcast_threshold) // if we aren't at the threshold, no overcasting.
+ return
+
+ // Overcasting
+ var/current_affinity = isnum(thaum_action.affinity) ? thaum_action.affinity : 0
+ // Attempts to overcast by repeatedly paying spell cost while above threshold.
+ while(source.blood_volume >= overcast_threshold && source.blood_volume >= spell_cost && current_affinity < THAUMATURGE_HEMOMANCY_MAX_AFFINITY)
+ source.blood_volume = max(source.blood_volume - spell_cost, 0)
+ current_affinity++
+
+ thaum_action.affinity = current_affinity
+
+/// Chargeless thaumaturge actions consume blood on successful use.
+/datum/component/thaumaturge/hemomancy/proc/on_mob_power_action_success(mob/living/source, datum/action/cooldown/power/action, atom/target)
+ SIGNAL_HANDLER
+ var/datum/action/cooldown/power/thaumaturge/thaum_action = action
+ if(!istype(thaum_action))
+ return
+ consume_action_cost(thaum_action, source)
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_power.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_power.dm
new file mode 100644
index 00000000000000..103e2383c35a3e
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_power.dm
@@ -0,0 +1,9 @@
+/datum/power/thaumaturge
+ name = "Thaumaturge Power"
+ desc = "Your Clairvoyance spell has succesfully revealed a most terrifying creature; an abstract parent type. Roll a DC18 Will save or become Confused for 1 minute. \
+ If you have expert proficeincy in Lore: Programming or are wearing programmer socks; improve your degree of success by one step."
+
+ archetype = POWER_ARCHETYPE_SORCEROUS
+ path = POWER_PATH_THAUMATURGE
+ priority = POWER_PRIORITY_BASIC
+ abstract_parent_type = /datum/power/thaumaturge
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm
new file mode 100644
index 00000000000000..fecc3cd09b7fbe
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm
@@ -0,0 +1,342 @@
+/datum/component/thaumaturge/preparation
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+
+ charges_color = "#ff69b4"
+
+ /// The 'mana' we have to allocate. This is basically the power value of the spell in the powers menu. Note that the spell's own mana cost need not be propertional to the value.
+ var/mana = 0
+
+ /// The mana that is currently being spend by spell preperation.
+ var/mana_spend = 0
+
+ /// Maximum amount of mana you can have.
+ var/max_mana = THAUMATURGE_MAX_MANA
+
+ /// List of spells available to the user.
+ var/list/spell_list = list()
+
+ /// Spells being prepared in the UI
+ var/list/prepared_charges = list()
+
+ /// Spells prepared post-preperation.
+ var/list/applied_prepared_charges = list()
+
+ /// If this is the first time preparing spells for the round.
+ var/first_time_preperation = TRUE
+
+ /// If they go to sleep, they'll recharge their actions. This is only set if it passes validation.
+ var/recharge_when_sleep = FALSE
+
+/datum/component/thaumaturge/preparation/Initialize()
+ . = ..()
+
+// We need to set these to interact with sleeping = gain charges
+/datum/component/thaumaturge/preparation/RegisterWithParent()
+ . = ..()
+ RegisterSignal(attached_mob, COMSIG_LIVING_STATUS_SLEEP, PROC_REF(on_sleep_set))
+
+/datum/component/thaumaturge/preparation/UnregisterFromParent()
+ UnregisterSignal(attached_mob, COMSIG_LIVING_STATUS_SLEEP)
+ . = ..()
+
+/// Gives the status effect responsible for charging spells when we go to sleep.
+/datum/component/thaumaturge/preparation/proc/on_sleep_set(mob/living/source, amount)
+ SIGNAL_HANDLER
+ // Only trigger on entering sleep (not waking, shortening, or extending existing sleep).
+ if(amount <= 0 || source.IsSleeping())
+ return
+ // Do we have queqed changes and is the flag that it passed validation on?
+ if(applied_prepared_charges && recharge_when_sleep)
+ //Do we have the focus on our person?
+ if(locate(/obj/item/spell_focus) in attached_mob.get_all_contents())
+ // apply the status effect which handles replenishment.
+ attached_mob.apply_status_effect(/datum/status_effect/power/thaumaturgic_sleep, src)
+ return
+ to_chat(attached_mob, span_warning("You cannot recharge your spells without a Spell Focus on your person!"))
+
+
+/// Validates mana and adds spells to the list.
+/datum/component/thaumaturge/preparation/proc/build_spells()
+ var/calculated_mana = 0
+ spell_list = list()
+ for(var/datum/power/power_instance as anything in attached_mob.powers)
+ if(!power_instance)
+ continue
+ if(power_instance.path != POWER_PATH_THAUMATURGE)
+ continue
+ if(check_if_can_prepare(power_instance.action_path))
+ spell_list.Add(power_instance)
+ calculated_mana += power_instance.value
+ mana = clamp(calculated_mana * THAUMATURGE_MANA_MULT, 0, max_mana)
+
+/// Checks if we can prepare the spell in our spellbook and if so adds it to the spell list.
+/datum/component/thaumaturge/preparation/proc/check_if_can_prepare(action_type)
+ if(!istype(action_type, /datum/action/cooldown/power/thaumaturge))
+ return FALSE
+ var/datum/action/cooldown/power/thaumaturge/cast_type = action_type
+ if(!cast_type.max_charges)
+ return FALSE
+
+ return TRUE
+
+/// Find the spell in the current spell_list and read its prep_cost.
+/datum/component/thaumaturge/preparation/proc/get_prep_cost_for_spell_ref(spell_ref)
+ for(var/datum/power/power_instance as anything in spell_list)
+ if("[power_instance.action_path.type]" == spell_ref)
+ var/datum/action/cooldown/power/thaumaturge/action_instance = power_instance.action_path
+ return max(0, action_instance?.prep_cost || 0)
+ return 0
+
+
+/// Starts the process of applying spells. Verification & all
+/datum/component/thaumaturge/preparation/proc/apply_preperation()
+ if(!check_valid_preperation())
+ recharge_when_sleep = FALSE
+ return
+ if(first_time_preperation)
+ if(apply_spell_charges())
+ first_time_preperation = FALSE
+ recharge_when_sleep = TRUE
+ to_chat(attached_mob, span_notice("Your spell preperation has been applied!"))
+ else
+ to_chat(attached_mob, span_warning("Something went wrong when applying spell charges; this shouldn't happen! Yell at a dev!"))
+ else
+ // For those curious how we trigger it, its the on_sleep_set() signaler at the top.
+ recharge_when_sleep = TRUE
+ to_chat(attached_mob, span_notice("Your changes have been saved! The next time you take the sleep action, the charges will be applied."))
+
+/// Applies the prepared spell charges.
+/datum/component/thaumaturge/preparation/proc/apply_spell_charges()
+ if(!length(applied_prepared_charges))
+ return FALSE
+
+ for(var/datum/power/power in attached_mob.powers)
+ // Thaumaturge powers only.
+ if(power.path != POWER_PATH_THAUMATURGE)
+ continue
+
+ var/datum/action/cooldown/power/thaumaturge/action = power.action_path
+ if(!action)
+ continue
+
+ var/charges = applied_prepared_charges["[action.type]"]
+ if(isnull(charges))
+ continue
+
+ action.charges = clamp(charges, 0, action.max_charges)
+
+ // Re-enable the power if it got charges, disable if it has 0 if it has max charges..
+ if(action.charges)
+ action.enable()
+ else if(action.max_charges)
+ action.disable()
+ action.update_charges_overlay()
+ return TRUE
+
+/// Reverifies that all the things picked for preperation are indeed valid.
+/datum/component/thaumaturge/preparation/proc/check_valid_preperation()
+ var/total_mana_cost = 0
+ build_spells()
+ for(var/prepared_key in applied_prepared_charges)
+ var/prepared_charges = applied_prepared_charges[prepared_key]
+
+ // find matching action instance on the mob
+ var/datum/action/cooldown/power/thaumaturge/matching_action = get_applied_charges_matching_power(attached_mob.powers, prepared_key)
+ if(!matching_action)
+ to_chat(attached_mob, span_warning("Prepared power '[prepared_key]' not found on you!"))
+ return FALSE
+
+ // Checks if the amount of charges are valid vs the max_charge
+ if(matching_action.max_charges < prepared_charges)
+ to_chat(attached_mob, span_warning("[matching_action]'s charges exceed the maximum!"))
+ return FALSE
+ total_mana_cost += (prepared_charges * matching_action.prep_cost)
+
+ // Checks if the total mana cost of all the charges
+ if(mana < total_mana_cost)
+ to_chat(attached_mob, span_warning("You have spend more mana than you have!"))
+ return FALSE
+ return TRUE
+
+/// Because TGUI gives it along as a string.
+/datum/component/thaumaturge/preparation/proc/get_applied_charges_matching_power(list/powers_list, prepared_key)
+ for(var/datum/power/power in powers_list)
+ var/datum/action/cooldown/power/thaumaturge/action = power.action_path
+ if(!action)
+ continue
+
+ // Becuase prepared key is a string we have to stringify action.type
+ if("[action.type]" == prepared_key)
+ return action
+
+ return null
+
+
+/* Below is responsible for all the TGUI stuff to do with spell preperation.
+ Save yourself if you need to touch this.
+*/
+/datum/component/thaumaturge/preparation/ui_interact(mob/living/user, datum/tgui/ui)
+ if(!user)
+ return
+
+ ui = SStgui.try_update_ui(user, src, ui)
+ if(ui)
+ return
+
+ // Draft starts from applied state
+ prepared_charges = applied_prepared_charges.Copy()
+
+ // Recalculate mana_spend from the draft
+ mana_spend = 0
+ for(var/spell_ref in prepared_charges)
+ var/charges = prepared_charges[spell_ref]
+ if(!isnum(charges) || charges <= 0)
+ continue
+ mana_spend += (charges * get_prep_cost_for_spell_ref(spell_ref))
+
+ ui = new(user, src, "ThaumaturgeSpellPrep", "Spell Preparation")
+ ui.open()
+
+
+/datum/component/thaumaturge/preparation/ui_state(mob/user)
+ return GLOB.always_state
+
+/datum/component/thaumaturge/preparation/ui_data(mob/living/user)
+ var/list/spells_payload = list()
+
+ for(var/datum/power/power_instance as anything in spell_list)
+ var/spell_ref = "[power_instance.action_path.type]"
+ var/current_charges = prepared_charges[spell_ref]
+ if(isnull(current_charges))
+ current_charges = 0
+
+ var/datum/action/cooldown/power/thaumaturge/action_instance = power_instance.action_path
+
+ var/prep_cost = action_instance?.prep_cost
+ if(isnull(prep_cost))
+ prep_cost = 1
+
+ spells_payload += list(list(
+ "key" = spell_ref,
+ "name" = action_instance?.name || "Unknown Spell",
+ "charges" = current_charges,
+ "max_charges" = action_instance?.max_charges || 0,
+ "prep_cost" = prep_cost,
+ "icon" = action_instance?.button_icon,
+ "icon_state" = action_instance?.button_icon_state,
+ ))
+
+ var/mana_remaining = max(mana - mana_spend, 0)
+
+ return list(
+ "mana_total" = mana,
+ "mana_max" = max_mana,
+ "mana_spend" = mana_spend,
+ "mana_remaining" = mana_remaining,
+ "spell_count" = length(spells_payload),
+ "spells" = spells_payload,
+ "first_time_preperation" = first_time_preperation,
+ )
+
+
+
+
+/datum/component/thaumaturge/preparation/ui_act(action, list/params, datum/tgui/ui)
+ . = ..()
+ if(.)
+ return
+
+ if(action == "inc" || action == "dec")
+ var/spell_ref = params["ref"]
+ if(!spell_ref)
+ return TRUE
+
+ // Validate spell exists (prevents spoofing)
+ var/found = FALSE
+ var/max_charges_local = 0
+
+ for(var/datum/power/power_instance as anything in spell_list)
+ if("[power_instance.action_path.type]" != spell_ref)
+ continue
+
+ var/datum/action/cooldown/power/thaumaturge/action_instance = power_instance.action_path
+ max_charges_local = action_instance?.max_charges || 0
+ found = TRUE
+ break
+
+ if(!found || max_charges_local <= 0)
+ return TRUE
+
+ var/current_charges = prepared_charges[spell_ref]
+ if(isnull(current_charges))
+ current_charges = 0
+
+ var/prep_cost = get_prep_cost_for_spell_ref(spell_ref)
+
+ if(action == "inc")
+ if(current_charges >= max_charges_local)
+ return TRUE
+
+ if(mana_spend + prep_cost > mana)
+ return TRUE
+
+ current_charges++
+ mana_spend += prep_cost
+
+ else
+ if(current_charges <= 0)
+ return TRUE
+
+ current_charges--
+ mana_spend = max(mana_spend - prep_cost, 0)
+
+ prepared_charges[spell_ref] = current_charges
+ return TRUE
+
+ if(action == "apply")
+ applied_prepared_charges = prepared_charges.Copy()
+ apply_preperation()
+ return TRUE
+
+ return FALSE
+
+// Status effect used for validating sleep
+/datum/status_effect/power/thaumaturgic_sleep
+ id = "thaumaturgic_sleep"
+ duration = THAUMATURGE_SLEEP_TIME // required amount of sleepytime
+ tick_interval = 1 SECONDS
+ show_duration = TRUE
+ alert_type = /atom/movable/screen/alert/status_effect/thaumaturgic_sleep
+
+ /// Has the sleep ended early?
+ var/ends_early = FALSE
+ /// Reference to the preperation component.
+ var/datum/component/thaumaturge/preparation/prep_component
+
+/datum/status_effect/power/thaumaturgic_sleep/on_creation(mob/living/new_owner, datum/component/thaumaturge/preparation/thaum_component)
+ prep_component = thaum_component
+ return ..()
+
+// Ticks every second, checks for focus and if we are asleep
+/datum/status_effect/power/thaumaturgic_sleep/tick(seconds_between_ticks)
+ var/has_focus = locate(/obj/item/spell_focus) in owner.get_all_contents()
+
+ if(!owner.IsSleeping() || !has_focus)
+ ends_early = TRUE
+ qdel(src)
+
+/datum/status_effect/power/thaumaturgic_sleep/on_remove()
+ // YOU GET NOTHING, YOU LOSE.
+ if(ends_early || QDELETED(owner))
+ return
+ if(!prep_component)
+ return
+ prep_component.apply_spell_charges()
+ to_chat(owner, span_notice("Your mind focuses on your spells, and through your dreams, you feel your Thaumaturge powers recharge!"))
+
+/atom/movable/screen/alert/status_effect/thaumaturgic_sleep
+ name = "Thaumaturgic Sleep"
+ desc = "You are manifesting your thaumaturgic power through your dreams; if you are asleep with your spell focus when this effect expires, you will recharge your spells. Waking up early yields nothing!"
+ icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ icon_state = "ice_1"
+
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm
new file mode 100644
index 00000000000000..2221a7cd965c77
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm
@@ -0,0 +1,10 @@
+
+/datum/power/thaumaturge_root
+ name = "Thaumaturge Root"
+ desc = "RAW, AWESOME MAGICAL POTENTIAL, UNREFINED CODE. ALL WAITING TO BE SHAPED INTO AN AWESOME POWER. That is to say, you are not meant to see this and should report this to a dev."
+
+ abstract_parent_type = /datum/power/thaumaturge_root
+
+ archetype = POWER_ARCHETYPE_SORCEROUS
+ path = POWER_PATH_THAUMATURGE
+ priority = POWER_PRIORITY_ROOT
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm
new file mode 100644
index 00000000000000..3d4fc75bad29e6
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm
@@ -0,0 +1,268 @@
+/obj/item
+ /// Used for the affinity system in the Powers system by Thaumaturge to determine their magical strengh.
+ var/affinity = 0
+ /// Item gets affinity from being worn instead of being held; useful for items that can be worn but arent obj/item/clothing
+ var/affinity_worn_override
+
+/* So, affinity is a system that applies a value to objects; and the amount of affinity is based on the item.
+We have to apply this retroactively to existing items, which is what this file is for. If you make something new, include it as a var instead.
+*/
+
+/*
+A lot of Affinity asignments are vibe-based depending on looks, visibility and rarirty, but the rule of thumb I tend to use is;
+- T1: If it goes in a weird slot (neck, mask, undersuit, shoes, gloves) OR if it has some association with 'magic' or pretending to be magical (e.g short capes) and isn't traditionally part of a classical caster archetype (Bard, Druid, Cleric, Wizard) it goes here.
+- T2: Magical headwear with bonus stats (armor). Handheld affinity items that fit in pockets+. T1 equipment that covers a lot of the sprite (capes, full-head masks, etc.)
+- T3: Magical headwear with NO bonus stats, Magical Bodywear with bonus stats. Rare magic-looking items in weird slots. Handheld affinity items that don't fit in pockets but do fit in the backpack.
+- T4: Magical bodywear with NO bonus stats. Handheld affinity items that don't fit in pocket or backpack but allow suit slots/belt slot.
+- T5: Handheld affinity items that don't fit anywhere (but the backpack). Most antag robes.
+- T6+: Go with your gut based on rarity, looks and antag. Wiz robes are usually T7.
+*/
+
+/*
+ Tier 1
+*/
+ // its not as pointy but itlll do
+/obj/item/clothing/head/costume/paper_hat
+ affinity = 1
+
+// the various neck-slot capes that cover too little of the sprite to pass. or are the poncho; because I cant recall a single magic-man wearing a poncho.
+/obj/item/clothing/neck/face_scarf
+ affinity = 1
+/obj/item/clothing/neck/mantle
+ affinity = 1
+/obj/item/clothing/neck/doppler_mantle
+ affinity = 1
+/obj/item/clothing/neck/basic_poncho
+ affinity = 1
+/obj/item/clothing/neck/ranger_poncho
+ affinity = 1
+/obj/item/clothing/neck/patterned_poncho
+ affinity = 1
+
+// we all loved to larp with bedsheet capes when we were younger
+/obj/item/bedsheet
+ affinity = 1
+ affinity_worn_override = TRUE
+
+// there's an argument to be made for plague doctors being mystical.
+/obj/item/clothing/mask/gas/plaguedoctor
+ affinity = 1
+
+// Animal masks are like a classic ritual in a lot of folklore so I am giving some leeway here. Small are T1, big are T2.
+/obj/item/clothing/mask/animal/small
+ affinity = 1
+
+// There's enough anime of magic maids to justify this.
+/obj/item/clothing/under/rank/civilian/janitor/maid
+ affinity = 1
+/obj/item/clothing/under/costume/maid
+ affinity = 1
+/obj/item/clothing/accessory/maidapron
+ affinity = 1
+
+/*
+ Tier 2:
+*/
+// Capes are about as caster as it gets and cover enough of the sprite to justify t2.
+/obj/item/clothing/neck/wide_cape
+ affinity = 2
+/obj/item/clothing/neck/robe_cape
+ affinity = 2
+/obj/item/clothing/neck/long_cape
+ affinity = 2
+
+
+/obj/item/staff // the base item is small
+ affinity = 2
+
+// Nullrods come in a lot of shapes and forms; by default we give it affinity 2 unless it fucks with slots and is clearly magical.
+/obj/item/nullrod
+ affinity = 2
+
+// Animal masks that arent small
+/obj/item/clothing/mask/animal
+ affinity = 2
+/*
+ Tier 3:
+*/
+// Jester hat
+/obj/item/clothing/head/costume/jester
+ affinity = 3
+
+// Clown Mitre
+/obj/item/clothing/head/chaplain/clownmitre
+ affinity = 4
+
+// Nun hood
+/obj/item/clothing/head/chaplain/habit_veil
+ affinity = 3
+
+// Shrine maiden wig
+/obj/item/clothing/head/costume/shrine_wig
+ affinity = 3
+
+// Gohei; this is apparently the asian equivelant of a staff. Regardless they lose points cause they fit in the backpack.
+/obj/item/gohei
+ affinity = 3
+
+// Narsie cult looks sufficiently magical, but they don't get the antag pass because you can get these from Lavaland and are already very robust.
+/obj/item/clothing/suit/hooded/cultrobes
+ affinity = 3
+
+// Rare enough cloak-slot dropped by Lavaland Elites.
+/obj/item/clothing/neck/cloak/herald_cloak
+ affinity = 3
+
+// It glows. That makes it more magical.
+/obj/item/bedsheet/cult
+ affinity = 3
+
+// Heretic focues arent too pronounced but theyre antag items so they get preferential treatment
+/obj/item/clothing/neck/heretic_focus
+ affinity = 3
+
+// You can teleport with it but given its megafauna loot we can be a bit lax
+/obj/item/hierophant_club
+ affinity = 3
+
+// Its the bible! Given we allow tomes, I'm sure some people will want to larp a cleric or otherwise have some magic religion. Print more bibles!
+/obj/item/book/bible
+ affinity = 3
+
+/*
+ Tier 4
+*/
+// Fits the criteria for wands but since its lavaland loot it gets a +1
+/obj/item/lava_staff
+ affinity = 4
+
+// Carp suit (magicarp)
+/obj/item/clothing/suit/hooded/carp_costume
+ affinity = 4
+
+// Cueball hat. It sparks and makes your head a big white orb.
+/obj/item/clothing/head/costume/cueball
+ affinity = 4
+
+// Owl Wings (pretty druidy)
+/obj/item/clothing/suit/toggle/owlwings // (includes griffon wings
+ affinity = 4
+
+// Dracula is pretty wizardy
+/obj/item/clothing/suit/costume/dracula
+ affinity = 4
+
+// Costumes that are basically wizard drip.
+/obj/item/clothing/suit/costume/imperium_monk
+ affinity = 4
+/obj/item/clothing/suit/hooded/mysticrobe
+ affinity = 4
+
+// Cleric/priest robes with no defenses.
+/obj/item/clothing/suit/chaplainsuit/whiterobe
+ affinity = 4
+/obj/item/clothing/suit/chaplainsuit/habit
+ affinity = 4
+/obj/item/clothing/suit/chaplainsuit/clownpriest
+ affinity = 4
+
+// I dont know what a touhou or a shrine maiden is but its magical apparently.
+/obj/item/clothing/suit/costume/shrine_maiden
+ affinity = 4
+
+// Banshees are valid.
+/obj/item/clothing/suit/costume/whitedress
+ affinity = 4
+/obj/item/clothing/under/dress/wedding_dress
+ affinity = 4
+
+// We should add frost powers tbh.
+/obj/item/clothing/suit/costume/drfreeze_coat
+ affinity = 4
+
+// Heretic void cloak. The hood actually gives T5 to reflect you can cast only with the hood up.
+/obj/item/clothing/suit/hooded/cultrobes/void
+ affinity = 4
+
+// The heretic book. Bonus points for being antag and heretic spell focus.
+/obj/item/codex_cicatrix
+ affinity = 4
+
+// Did you know the perceptomatrix lets you cast spells?
+/obj/item/clothing/head/helmet/perceptomatrix
+ affinity = 4
+
+/*
+ Tier 4: Wizrobes specifically.
+*/
+
+// Wizrobes (Fakes)
+/obj/item/clothing/suit/wizrobe/fake
+ affinity = 4
+/obj/item/clothing/suit/wizrobe/marisa/fake
+ affinity = 4
+/obj/item/clothing/suit/wizrobe/tape/fake
+ affinity = 4
+
+// Wizrobe hats (Fakes)
+/obj/item/clothing/head/wizard/fake
+ affinity = 4
+/obj/item/clothing/head/costume/witchwig
+ affinity = 4
+/obj/item/clothing/head/collectable/wizard
+ affinity = 4
+/obj/item/clothing/head/wizard/marisa/fake
+ affinity = 4
+/obj/item/clothing/head/wizard/tape/fake
+ affinity = 4
+/obj/item/clothing/head/wizard/chanterelle
+ affinity = 4
+
+/*
+ Tier 5+
+*/
+
+// Nullrods that are bulky and clearly magical
+/obj/item/nullrod/staff
+ affinity = 5
+/obj/item/nullrod/vibro/spellblade
+ affinity = 5
+/obj/item/vorpalscythe
+ affinity = 5
+/obj/item/nullrod/claymore/darkblade // its a cult sword and it glows thats good enough
+ affinity = 5
+/obj/item/nullrod/pitchfork
+ affinity = 5
+/obj/item/nullrod/pride_hammer
+ affinity = 5
+
+// Haunted blade, the heretic sword that gives you cool shit.
+/obj/item/melee/cultblade/haunted
+ affinity = 5
+
+// Heretic cloak with hood up: This is invisible, but also heretics can cast spells with it so this is fine.
+/obj/item/clothing/head/hooded/cult_hoodie/void
+ affinity = 5
+
+// Eldrich heretic robes with hood up.
+/obj/item/clothing/head/hooded/cult_hoodie/eldritch
+ affinity = 5
+
+// Slightly better staves, as a treat.
+/obj/item/storm_staff
+ affinity = 6
+/obj/item/rod_of_asclepius
+ affinity = 6
+
+// Real Wizrobes (antag only)
+/obj/item/clothing/head/wizard
+ affinity = 6
+/obj/item/clothing/suit/wizrobe
+ affinity = 7
+/obj/item/clothing/suit/wizrobe/paper // this ones a bit special since its space loot but rare space loot.
+ affinity = 6
+
+// This is the actual magnum opus of Wizardy; unless a Wizard item is made to delibaretely interact with thaumaturge, there shouldn't be anything exceeding this.
+/obj/item/mod/control/pre_equipped/enchanted
+ affinity = 8
+ affinity_worn_override = TRUE
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_robes.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_robes.dm
new file mode 100644
index 00000000000000..ccabd7c4f0f578
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_robes.dm
@@ -0,0 +1,59 @@
+/// Special job specific robes that give affinity.
+
+// Viszard; affinity 4 body, no bonus perks besides not being flammable.
+/obj/item/clothing/suit/wizrobe/viszard
+ name = "vizard robe"
+ desc = "Most people would think this is a high-vis raincoat, but those fools are WRONG. These are the proud garments of a thaumaturge. They look the same? Well, one is worn in rainy weather, and the other is the highly regarded robes of the great thaumaturges that can untangle the 'M6 Spaghetti Junction' with the flick of a wrist. "
+ icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi'
+ worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi'
+ icon_state = "hivizmob"
+ armor_type = /datum/armor/none
+ cold_protection = CHEST|GROIN|ARMS|HANDS|LEGS
+ heat_protection = CHEST|GROIN|ARMS|HANDS|LEGS
+ fishing_modifier = -6 // high vishing
+ affinity = 3
+ supported_bodyshapes = list(BODYSHAPE_HUMANOID, BODYSHAPE_DIGITIGRADE)
+ bodyshape_icon_files = list(BODYSHAPE_HUMANOID_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi',
+ BODYSHAPE_DIGITIGRADE_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi')
+
+// Viszard; affinity 3 head, no bonus perks besides not being flammable.
+/obj/item/clothing/head/wizard/viszard
+ name = "viszard hat"
+ desc = "An incredibly obvious wizard hat; as if the pointiness wasn't obvious enough. Despite being granted to the Engineering department, it does not pass as a helmet for workplace safety standards, so please beware falling objects. However, it is fireproof."
+ icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi'
+ worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi'
+ icon_state = "hivizhat"
+ armor_type = /datum/armor/none
+ fishing_modifier = -5 // high vishing
+ affinity = 3
+
+// Secrobe; affinity 3 armor. Has the stats of a secjacket and covers the legs, and also has affinity, but also has a slight amount of slowdown.
+/obj/item/clothing/suit/wizrobe/secwiz
+ name = "security thaumaturge robe"
+ desc = "The garments of a security-contracted Thaumaturge. The robes have been reinforced and provide a high amount of protection across a large degree of the body, at the cost of being bulkier to move in. The proportion of armor to robe has been fine-tuned for the most optimal results; it seems that armored wizards aren't particularly popular in the worldly zeitgeist, reducing the impact of armor on robes."
+ icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi'
+ worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi'
+ icon_state = "secrobemob"
+ armor_type = /datum/armor/armor_secjacket
+ cold_protection = CHEST|GROIN|ARMS|HANDS|LEGS
+ heat_protection = CHEST|GROIN|ARMS|HANDS|LEGS
+ resistance_flags = FLAMMABLE
+ affinity = 3
+ slowdown = 0.2
+ fishing_modifier = -3
+ supported_bodyshapes = list(BODYSHAPE_HUMANOID, BODYSHAPE_DIGITIGRADE)
+ bodyshape_icon_files = list(BODYSHAPE_HUMANOID_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi',
+ BODYSHAPE_DIGITIGRADE_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi')
+
+// Secrobe; affinity 3 head, no bonus perks besides not being flammable.
+/obj/item/clothing/head/wizard/secwiz
+ name = "security thaumaturge hat"
+ desc = "A wizard's hat, painted in the colors of the security department. Jokingly referred to as the Magic Police, Thaumaturges experience an unique skillset that is very useful to have as a Security Officer. Given their requirements to dress \
+ for their powers, security has commissioned these special hats."
+ icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi'
+ worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi'
+ icon_state = "sechat"
+ armor_type = /datum/armor/none
+ fishing_modifier = -2
+ resistance_flags = FLAMMABLE
+ affinity = 3
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm
new file mode 100644
index 00000000000000..91f5edc23d275c
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm
@@ -0,0 +1,84 @@
+/obj/item/spell_focus
+ name = "thaumaturge's spell focus"
+ desc = "An orb of raw thaumaturgic resonance, adjustable to take on any form of your choosing, one-time only. Needed to restore thaumaturgic powers."
+ icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ icon_state = "ice_1"
+ slot_flags = NONE
+ w_class = WEIGHT_CLASS_TINY
+ obj_flags = UNIQUE_RENAME
+ affinity = 2 // check thaumaturge_affinity.dm if you ever wonder what deserves what affinity
+ /// If FALSE, suppress belt sprite entirely (prevents missing belt sprites).
+ var/shows_on_belt = FALSE
+ /// Short description of what this item is capable of, for radial menu uses.
+ var/menu_description = "An orb of energy. Fits in pockets. Very convenient, gives affinity 2 and is not visible in your hands, but doesn't do much more than that."
+
+/obj/item/spell_focus/Initialize(mapload)
+ . = ..()
+
+ // Only the base focus should offer "picks". Subtypes are the end result.
+ if(type != /obj/item/spell_focus)
+ return
+
+ var/list/focuses = list()
+ for(var/obj/item/spell_focus/focus_type as anything in typesof(/obj/item/spell_focus))
+ focuses[focus_type] = initial(focus_type.menu_description)
+
+ AddComponent(/datum/component/subtype_picker, focuses, CALLBACK(src, PROC_REF(on_spell_focus_picked)))
+
+/obj/item/spell_focus/proc/on_spell_focus_picked(obj/item/spell_focus/new_focus, mob/living/picker)
+ if(!istype(new_focus))
+ return
+ new_focus.on_selected(src, picker)
+
+/obj/item/spell_focus/proc/on_selected(obj/item/spell_focus/old_focus, mob/living/picker)
+ // Preserve unique renames (if the old focus was renamed by a player).
+ if(old_focus.name != initial(old_focus.name))
+ name = old_focus.name
+
+/obj/item/spell_focus/tome
+ name = "thaumaturge's tome"
+ desc = "A tome! What secrets does it hold? Apparently long lines of jargon that only one specific person can understand; some people need to learn how to convey information."
+ icon = 'icons/obj/service/library.dmi'
+ icon_state = "bookcharge"
+ lefthand_file = 'icons/mob/inhands/items/books_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/items/books_righthand.dmi'
+ inhand_icon_state = "kojiki" // they have no inhands but affinity3 needs inhands so we borrow another blue book instead
+ throw_speed = 1
+ throw_range = 5
+ slot_flags = NONE
+ shows_on_belt = FALSE
+ w_class = WEIGHT_CLASS_NORMAL
+ attack_verb_continuous = list("bashes", "whacks", "educates")
+ attack_verb_simple = list("bash", "whack", "educate")
+ drop_sound = 'sound/items/handling/book_drop.ogg'
+ pickup_sound = 'sound/items/handling/book_pickup.ogg'
+ affinity = 3
+ menu_description = "An arcane tome. Fits in your backpack, and provides affinity 3; but does not fit in the pockets and is fairly conspicuous."
+
+/obj/item/spell_focus/wand
+ name = "thaumaturge's wand"
+ desc = "A pointy stick, attuned to work with thaumaturgic resonance. Capable of restoring thaumaturgic powers when resting."
+ icon = 'icons/obj/weapons/guns/magic.dmi'
+ icon_state = "nothingwand-drained"
+ inhand_icon_state = "wand"
+ lefthand_file = 'icons/mob/inhands/items_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/items_righthand.dmi'
+ slot_flags = NONE
+ w_class = WEIGHT_CLASS_NORMAL
+ affinity = 3
+ menu_description = "A classical magic wand. Fits in your backpack, and provides affinity 3; but does not fit in any pockets and is clearly visible when held."
+
+/obj/item/spell_focus/staff
+ name = "thaumaturge's staff"
+ desc = "A big ol' staff, attuned to work with thaumaturgic resonance. Makes for an excellent focus for thaumaturgic powers, and is capable of restoring thaumaturgic powers when resting."
+ icon = 'icons/obj/weapons/staff.dmi'
+ icon_state = "godstaff-blue"
+ inhand_icon_state = "godstaff-blue"
+ icon_angle = -45
+ lefthand_file = 'icons/mob/inhands/weapons/staves_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/weapons/staves_righthand.dmi'
+ w_class = WEIGHT_CLASS_HUGE
+ force = 7
+ slot_flags = ITEM_SLOT_BACK
+ affinity = 5
+ menu_description = "A staff with an orb on the end. Because it is bulky, it can only be stored in the back slot, but offers affinity 5 in return. As well as being very apt for whacking fools that can't comprehend your arcane knowledge."
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm
new file mode 100644
index 00000000000000..b88f28840b2bf9
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm
@@ -0,0 +1,167 @@
+// Will it blend?
+// Emulates the effects of a grinder on the target in your hand. Can be used offensively too through aggressive grabs.
+
+/datum/power/thaumaturge/blend_for_me
+ name = "Blend For Me"
+ desc = "Grinds the item in your hand as if it were inserted in a grinder, then conjures a glass to hold it (if you're grinding). Right-hand for grinding, left-hand for juicing. Can be used on people using an aggressive grab to inflict brute damage and bleeding. \
+ \nRequires Affinity 1. Affinity gives a chance to not consume charges."
+ security_record_text = "Subject can magically blend drinks, objects and people with their bare hands."
+ value = 2
+
+ action_path = /datum/action/cooldown/power/thaumaturge/blend_for_me
+ required_powers = list(/datum/power/thaumaturge_root)
+ required_allow_subtypes = TRUE
+
+/datum/action/cooldown/power/thaumaturge/blend_for_me
+ name = "Blend For Me"
+ desc = "The younger cousin of a remarkably wicked spell; grinds the item in your hand as if it were inserted in a grinder, then conjures a glass to hold it (if you're grinding). Right-hand for grinding, left-hand for juicing. Can be used on people using an aggressive grab to inflict brute damage and bleeding."
+ button_icon = 'icons/obj/machines/kitchen.dmi'
+ button_icon_state = "juicer"
+
+ cooldown_time = 50 // we don't want people spamming the blender noise. that's it. that's the whole reason why we force a 5 second cooldown.
+ required_affinity = 1
+ prep_cost = 2
+ power_refunds = TRUE
+ power_refund_chance = THAUMATURGE_REFUND_MULT_BASE
+ power_refund_affinity_bonus = THAUMATURGE_REFUND_MULT_AFFINITY
+
+ /// The grab damage per tick.
+ var/grab_blend_brute = 12.5
+ /// How many cycles can we blend a person.
+ var/grab_blend_duration = 4
+
+/datum/action/cooldown/power/thaumaturge/blend_for_me/use_action(mob/living/user, atom/target)
+ // Check first if we are pulling a person. If so we go for the grab_blend
+ if(person_blend_conditions(user, target))
+ return will_a_person_blend(user, owner.pulling)
+
+ // Are we grinding or juicing?
+ var/grinding
+ // What item is in our active hand?
+ var/obj/item/active_held_item = user.get_active_held_item()
+ if(!active_held_item)
+ return FALSE
+
+ // Is the item too big?
+ if(active_held_item.w_class >= WEIGHT_CLASS_BULKY)
+ user.balloon_alert(user, "Too large to blend!")
+ return FALSE
+
+ // Which hand is active?
+ var/held_index = user.get_held_index_of_item(active_held_item)
+ if(!held_index)
+ return FALSE
+ var/active_is_right_hand = IS_RIGHT_INDEX(held_index)
+ if(active_is_right_hand) // If its in the right hand we grind, otherwise we blend.
+ grinding = TRUE
+
+ return will_it_blend(user, active_held_item, grinding)
+
+/// Attempts to blend the item.
+/datum/action/cooldown/power/thaumaturge/blend_for_me/proc/will_it_blend(mob/living/user, obj/item/input_item, grinding)
+ // Start cooldown immediately (anti-spam)
+ StartCooldown()
+
+ // FX
+ user.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 40)
+ playsound(user, grinding ? 'sound/machines/blender.ogg' : 'sound/machines/juicer.ogg', 50, TRUE)
+
+ // Channel
+ if(!do_after(user, 4 SECONDS, target = user))
+ return FALSE
+
+ // Temporary buffer to house the results so we can neatly transfer it to the same hand.
+ var/turf/user_turf = get_turf(user)
+ if(!user_turf)
+ return FALSE
+
+ var/obj/effect/abstract/thaum_blend_buffer/buffer = new(user_turf, 30)
+
+ // We reject multiple stacks of items for now. Despite multiple attempts, it seems to just NOT WORK?!
+ // If you can figure out how, please do :)
+ if(istype(input_item, /obj/item/stack))
+ var/obj/item/stack/stack_item = input_item
+ if(stack_item.amount > 1)
+ user.balloon_alert(user, "Split the stack first!")
+ return FALSE
+
+ var/success
+ // The blending process
+ if(grinding)
+ success = input_item.grind(buffer.reagents, user)
+ else
+ success = input_item.juice(buffer.reagents, user)
+
+ if(!success) // If it somehow fails to grind/juice
+ user.balloon_alert(user, "[input_item] resists being processed!")
+ qdel(buffer)
+ return FALSE
+
+ if(buffer.reagents.total_volume <= 0) // If somehow we grind something but ntohing comes out.
+ user.balloon_alert(user, "Nothing useful comes out.")
+ qdel(buffer)
+ return FALSE
+
+ // Conjure bottle AFTER grind so hands are likely freed
+ var/obj/item/reagent_containers/cup/glass/bottle/small/result_bottle = new(user_turf)
+ user.put_in_hands(result_bottle)
+
+ // Transfer contents
+ buffer.reagents.trans_to(result_bottle, buffer.reagents.total_volume, transferred_by = user)
+ qdel(buffer)
+
+ return TRUE
+
+// We create a temporary buffer for holding the reagents, given that our 'blender' in this case isn't a conventional object.
+/obj/effect/abstract/thaum_blend_buffer
+ name = "resonant blender"
+ desc = "You think you're so fancy seeing invisible coder objects huh? Reaaal magician right here."
+ invisibility = INVISIBILITY_ABSTRACT
+ anchored = TRUE
+ density = FALSE
+
+ /// The reagent_buffer that holds all the reagents temporarily.
+ var/datum/reagents/reagent_buffer
+
+ /// Size of the buffer
+ var/buffer_volume = 50
+
+/obj/effect/abstract/thaum_blend_buffer/Initialize(mapload, new_buffer_volume)
+ . = ..()
+ if(isnum(new_buffer_volume) && new_buffer_volume > 0)
+ buffer_volume = new_buffer_volume
+ reagents = new /datum/reagents(buffer_volume, src)
+ reagents.flags = TRANSPARENT | DRAINABLE
+
+/// Check to see if we're allowed to blend people.
+/datum/action/cooldown/power/thaumaturge/blend_for_me/proc/person_blend_conditions(/mob/living/user, atom/target)
+ return owner.pulling && owner.grab_state <= GRAB_AGGRESSIVE && isliving(owner.pulling)
+
+/// Attemps to blend A PERSON.
+/// Keep in mind that if you try to blend an undersized person in your hand, it will use will_it_blend instead.
+/datum/action/cooldown/power/thaumaturge/blend_for_me/proc/will_a_person_blend(mob/living/user, mob/living/target)
+ // How many times has our do_while hurt the person?
+ var/blend_attacks = 0
+ owner.visible_message(span_danger("[owner] begins to magically grind [target]'s body to bits!"), span_notice("You begin to grind [target] into a pulp."))
+ playsound(user, 'sound/machines/blender.ogg' , 50, TRUE)
+ do
+ target.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 10)
+ if(do_after(owner, 10, target = target) && person_blend_conditions(user, target))
+ target.adjustBruteLoss(grab_blend_brute)
+ // Carbon mobs can receive wounds.
+ if(iscarbon(target))
+ var/mob/living/carbon/thatpoorguy = target
+ // 50% chance to receive a severe wound
+ if(prob(50))
+ thatpoorguy.cause_wound_of_type_and_severity(WOUND_SLASH, null, WOUND_SEVERITY_SEVERE, WOUND_SEVERITY_SEVERE)
+ else
+ thatpoorguy.cause_wound_of_type_and_severity(WOUND_SLASH, null, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_MODERATE)
+ // Scream for the first time cause this is HORRIFYING.
+ if(blend_attacks == 0)
+ target.emote("scream")
+ playsound(user, SFX_DESECRATION, 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+ blend_attacks++
+ else
+ break
+ while (blend_attacks < grab_blend_duration)
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm
new file mode 100644
index 00000000000000..6331b8e62c62d0
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm
@@ -0,0 +1,51 @@
+/*
+ To deal with those pesky resonant users that keep using their POWERS.
+*/
+
+/datum/power/thaumaturge/brazen_bindings
+ name = "Brazen Bindings"
+ desc = "Summons a set of manacles made from brass, capable of dispelling and disabling Resonant powers on the bound target. The magic that made them is fragile, causing them to break once someone escapes. \
+ \nRequires Affinity 1. Additional affinity increases the time it takes to break out."
+ security_record_text = "Subject can conjure anti-resonant manacles out of thin air."
+ security_threat = POWER_THREAT_MAJOR
+ value = 3
+
+ action_path = /datum/action/cooldown/power/thaumaturge/brazen_bindings
+ required_powers = list(/datum/power/thaumaturge_root)
+ required_allow_subtypes = TRUE
+
+/datum/action/cooldown/power/thaumaturge/brazen_bindings
+ name = "Brazen Bindings"
+ desc = "Summons a set of manacles made from brass, capable of dispelling and disabling Resonant powers on the bound target. The magic that made them is fragile, causing them to break once someone escapes."
+ button_icon = 'icons/obj/weapons/restraints.dmi'
+ button_icon_state = "brass_manacles"
+
+ required_affinity = 1
+ prep_cost = 3
+
+/datum/action/cooldown/power/thaumaturge/brazen_bindings/use_action(mob/living/user, atom/target)
+ if(user.get_active_held_item() && user.get_inactive_held_item())
+ user.balloon_alert(user, "hands are not empty!")
+ return FALSE
+
+ // Creates item, adds the special phantasmal tool properties, give to user.
+ var/obj/item/restraints/handcuffs/antiresonant/brazen/new_cuffs = new /obj/item/restraints/handcuffs/antiresonant/brazen
+ new_cuffs.breakouttime += (affinity - 1) * 5
+ user.put_in_hands(new_cuffs)
+ playsound(user, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return TRUE
+
+// the special ones conjured by thaumaturges.
+/obj/item/restraints/handcuffs/antiresonant/brazen/
+ name = "brazen manacles"
+ desc = "Bulky, enchanted and resonant manacles made out of brass and laden with (cheap) gemstones. They're held together using a sliver of resonant power, causing them to break into an unuseable mess once removed."
+ icon = 'icons/obj/weapons/restraints.dmi'
+ icon_state = "brass_manacles"
+ w_class = WEIGHT_CLASS_NORMAL
+ breakouttime = 30 SECONDS // default for 1affinity. For comparison, zipties are 30seconds and normal cuffs are 1min.
+ handcuff_time = 6 SECONDS
+ color = null // only til we get a proper sprite for the base cuffs, which are currrently colored red.
+
+/obj/item/restraints/handcuffs/antiresonant/brazen/on_uncuffed(datum/source, mob/living/wearer)
+ . = ..()
+ qdel(src)
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm
new file mode 100644
index 00000000000000..bec9461b3cfa3b
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm
@@ -0,0 +1,205 @@
+// bless my rains down with reagents.
+/datum/power/thaumaturge/conjure_rain
+ name = "Conjure Rain"
+ desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u water, up to a maximum of 60u spread out across all containers. Mobs are splashed with the same amount and don't count towards this limit. \
+ \nHolding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \
+ \nRequires Affinity 3. Higher affinity increases the max amount of spreadable reagents by 20u."
+ security_record_text = "Subject can conjure rains with varying chemical properties."
+ security_threat = POWER_THREAT_MAJOR
+ value = 4
+
+ action_path = /datum/action/cooldown/power/thaumaturge/conjure_rain
+ required_powers = list(/datum/power/thaumaturge_root)
+ required_allow_subtypes = TRUE
+
+/datum/action/cooldown/power/thaumaturge/conjure_rain
+ name = "Conjure Rain"
+ desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u water, up to a maximum of 60u spread out across all containers. Mobs are splashed with the same amount and don't count towards this limit. \
+ Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ "
+ button_icon = 'icons/effects/weather_effects.dmi'
+ button_icon_state = "rain_low"
+
+ required_affinity = 3
+ prep_cost = 4
+ click_to_activate = TRUE
+ anti_magic_on_target = FALSE
+
+ use_time_overlay_type = /obj/effect/temp_visual/conjure_rain
+ use_time = 1 SECONDS
+
+ /// the chem that the base rain uses
+ var/datum/reagent/rain_chem = /datum/reagent/water
+ /// the base conversion ratio of the chem.
+ var/base_chem_ratio = 1
+ /// the max amount we put in a single container
+ var/max_reagents_per_container = 20
+ /// max amount of reagents we can spread across containers (not including mobs)
+ var/max_reagents_dupe = 60
+ /// bonus to max reagents per affinity above 3.
+ var/affinity_max_reagents = 20
+ /// Max units of reagent to expose per turf when splashing on the ground.
+ var/ground_expose_cap = 10
+ /// If TRUE, only allow chems that can be synthesized (unless bypassed below).
+ var/require_synthesizable = TRUE
+ /// Chems that bypass synthesizable check.
+ var/list/synth_bypass_chems = list(/datum/reagent/blood) // blood is cool and has synergy with sanguine absorption
+
+/// Is the chem alloewd? If its synthesizable or is on the bypass list.
+/datum/action/cooldown/power/thaumaturge/conjure_rain/proc/is_allowed_rain_reagent(datum/reagent/reagent)
+ if(!reagent)
+ return FALSE
+ if(reagent.type in synth_bypass_chems)
+ return TRUE
+ if(!require_synthesizable)
+ return TRUE
+ return (reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED)
+
+// We piggyback into do_use_time to add a telegraph of the rain.
+/datum/action/cooldown/power/thaumaturge/conjure_rain/do_use_time(mob/living/user, atom/target)
+ if(use_time <= 0)
+ return TRUE
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ return FALSE
+
+ // we cheekily get the color of the held reagent container so we can color the rain even if we haven't calculated the buffer yet. May not be 100% accurate, but close enuff.
+ var/rain_color
+ var/obj/item/reagent_containers/held_container = user.get_active_held_item()
+ if(istype(held_container) && held_container.reagents?.reagent_list?.len)
+ // We need to make sure that the chems are synthesizable so that people aren't surprised that they can't blood rain
+ var/list/datum/reagent/synth_reagents = list()
+ for(var/datum/reagent/reagent in held_container.reagents.reagent_list)
+ if(is_allowed_rain_reagent(reagent))
+ synth_reagents += reagent
+ // If all succeeds, mix the rain color.
+ if(length(synth_reagents))
+ rain_color = mix_color_from_reagents(synth_reagents)
+ else // no reagent container, default to rain_chem
+ rain_color = initial(rain_chem.color)
+
+ // displays the telgraphed rain
+ for(var/turf/area_turf in range(1, target_turf))
+ new /obj/effect/temp_visual/thaum_rain_buildup(area_turf, rain_color)
+ return ..()
+
+/datum/action/cooldown/power/thaumaturge/conjure_rain/use_action(mob/living/user, atom/target)
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ return FALSE
+
+ // creatures the reagent buffer and adds water
+ var/obj/effect/abstract/thaum_rain_buffer/buffer = new(target_turf, 20)
+ buffer.reagents.add_reagent(rain_chem, buffer.buffer_volume)
+
+ // If we have a held container, convert some of the rain into that reagent.
+ var/obj/item/reagent_containers/held_container = user.get_active_held_item()
+ if(istype(held_container) && held_container.reagents?.total_volume)
+ var/synth_volume = 0
+ for(var/datum/reagent/reagent as anything in held_container.reagents.reagent_list)
+ if(is_allowed_rain_reagent(reagent))// Prevents us from duping SPECIAL CHEMS (unless bypassed).
+ synth_volume += reagent.volume
+ var/drain_amount = min(buffer.buffer_volume, synth_volume)
+ if(drain_amount > 0)
+ buffer.reagents.remove_reagent(rain_chem, drain_amount) // 1:1 water consumption
+ var/chem_ratio = base_chem_ratio
+ var/part = drain_amount / synth_volume
+ for(var/datum/reagent/reagent as anything in held_container.reagents.reagent_list)
+ if(!is_allowed_rain_reagent(reagent))
+ continue
+ var/transfer_amount = reagent.volume * part
+ if(transfer_amount > 0)
+ held_container.reagents.trans_to(buffer.reagents, transfer_amount, chem_ratio, target_id = reagent.type, transferred_by = user)
+
+ // sets the rain color and plays the noise
+ var/rain_color = mix_color_from_reagents(buffer.reagents.reagent_list)
+ playsound(target, 'sound/effects/splat.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+
+ var/list/obj/item/reagent_containers/area_containers = list()
+ for(var/turf/area_turf in range(1, target_turf))
+ for(var/obj/item/reagent_containers/target_container in area_turf)
+ if(target_container.reagents)
+ area_containers += target_container
+
+ var/bonus_affinity = max(0, affinity - 3)
+ var/max_spread = max_reagents_dupe + (bonus_affinity * affinity_max_reagents)
+ var/per_container = 0
+ var/ground_expose_modifier = 1
+ if(buffer.reagents.total_volume > 0)
+ ground_expose_modifier = min(1, ground_expose_cap / buffer.reagents.total_volume)
+
+ // Get every reagent container in range and calculate how we spread the rain.
+ if(length(area_containers))
+ per_container = min(max_reagents_per_container, max_spread / length(area_containers))
+
+ // every tile in range...
+ for(var/turf/area_turf in range(1, target_turf))
+ var/has_container = FALSE
+ for(var/obj/item/reagent_containers/target_container in area_turf)
+ has_container = TRUE
+ break
+ // splash it onto the space (skip if we're filling a container on that turf).
+ if(!has_container)
+ buffer.reagents.expose(area_turf, TOUCH, ground_expose_modifier)
+ // splashes it onto every mob in the area
+ for(var/mob/living/area_mob in area_turf)
+ buffer.reagents.expose(area_mob, TOUCH)
+
+ // rain fx
+ new /obj/effect/temp_visual/thaum_rain(area_turf, rain_color)
+
+ // Adds reagents to containers based on the calculated per_container.
+ if(per_container > 0)
+ for(var/obj/item/reagent_containers/target_container in area_containers)
+ buffer.reagents.trans_to(target_container, per_container, transferred_by = user, copy_only = TRUE)
+
+ qdel(buffer)
+ return TRUE
+
+// We create a temporary buffer for holding the reagents.
+/obj/effect/abstract/thaum_rain_buffer
+ name = "resonant beaker"
+ desc = "You caught me doing it again; I did it once with the blender, now I am doing it again. YES. This is NECESSARY for Reagents. Don't you judge the coder! You aren't even meant to see this, peasant!"
+ invisibility = INVISIBILITY_ABSTRACT
+ anchored = TRUE
+ density = FALSE
+
+ /// Holds reagents tempirarly.
+ var/datum/reagents/reagent_buffer
+ /// The size of our buffer; also affects how much our rain produces
+ var/buffer_volume = 20
+
+/obj/effect/abstract/thaum_rain_buffer/Initialize(mapload, new_buffer_volume)
+ . = ..()
+ if(isnum(new_buffer_volume) && new_buffer_volume > 0)
+ buffer_volume = new_buffer_volume
+ reagents = new /datum/reagents(buffer_volume, src)
+ reagents.flags = TRANSPARENT | DRAINABLE
+
+/obj/effect/temp_visual/thaum_rain
+ name = "magical rain"
+ icon = 'icons/effects/weather_effects.dmi'
+ icon_state = "rain_high"
+ duration = 1 SECONDS
+
+/obj/effect/temp_visual/thaum_rain_buildup
+ name = "light magical rain"
+ icon = 'icons/effects/weather_effects.dmi'
+ icon_state = "rain_low"
+ duration = 1 SECONDS
+
+// lets us recolor the rain
+/obj/effect/temp_visual/thaum_rain_buildup/Initialize(mapload, set_color)
+ if(set_color)
+ add_atom_colour(set_color, FIXED_COLOUR_PRIORITY)
+ return ..()
+
+/obj/effect/temp_visual/thaum_rain/Initialize(mapload, set_color)
+ if(set_color)
+ add_atom_colour(set_color, FIXED_COLOUR_PRIORITY)
+ return ..()
+
+// visual effect on the caster for casting rain
+/obj/effect/temp_visual/conjure_rain
+ icon_state = "blessed"
+ color = "#243fda"
+ duration = 1 SECONDS
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm
new file mode 100644
index 00000000000000..647e2ba133b8e6
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm
@@ -0,0 +1,181 @@
+/*
+ Shoots a blast of wind for various neat purposes; mostly just to push your co-workers around and the occasional fire.
+ Perfectly encapsulates the design philosophy of thaumaturge spells being big on util and not specifically good at one thing.
+*/
+
+// Maximum amount of items we can push with this spell.
+#define THAUMATURGE_GALE_BLAST_PUSH_LIMIT 20
+
+/datum/power/thaumaturge/gale_blast
+ name = "Gale Blast"
+ desc = "Shoots forth a blast of wind. The blast keeps traveling until it hits a solid structure, extinguishing any fires and dragging along any items with it. If it hits a creature, it knocks them back 3 spaces and extinguishes them. \
+ \nRequires Affinity 3. Extra affinity gives a chance to knockback further."
+ security_record_text = "Subject can create and shoot out strong, violent gusts of wind."
+ security_threat = POWER_THREAT_MAJOR
+ value = 3
+
+ action_path = /datum/action/cooldown/power/thaumaturge/gale_blast
+ required_powers = list(/datum/power/thaumaturge_root)
+ required_allow_subtypes = TRUE
+
+/datum/action/cooldown/power/thaumaturge/gale_blast
+ name = "Gale Blast"
+ desc = "Shoots forth a blast of wind. The blast keeps traveling until it hits a solid structure, extinguishing any fires and dragging along any items with it. If it hits a creature, it knocks them back 3 spaces and extinguishes them."
+ button_icon = 'icons/effects/effects.dmi'
+ button_icon_state = "smoke"
+
+ required_affinity = 3
+ prep_cost = 3
+ click_to_activate = TRUE
+ anti_magic_on_target = FALSE
+
+/datum/action/cooldown/power/thaumaturge/gale_blast/use_action(mob/living/user, atom/target)
+ if(fire_projectile(user, target, /obj/projectile/resonant/gale_blast))
+ playsound(user, 'sound/effects/podwoosh.ogg', 60, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+ return FALSE
+
+
+// The projectile itself
+/obj/projectile/resonant/gale_blast
+ name = "gale blast"
+ icon = 'icons/effects/effects.dmi'
+ icon_state = "smoke"
+
+ // Tweak as needed.
+ var/knockback_range = 3
+
+// Code for dragging along objects.
+/obj/projectile/resonant/gale_blast/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
+ . = ..()
+
+ var/turf/current_turf = get_turf(src)
+ if(!current_turf)
+ return
+ var/turf/old_turf = get_turf(old_loc)
+
+ // Handless moving objects along with it.
+ drag_along_movables(old_turf, current_turf)
+ // Extinguishes hotspots. Doesn't mess with atmos.
+ extinguish_hotspots_on_turf(current_turf)
+
+/// Drags along anything that's not nailed down on the floor.
+/// Testing note: This drags along ghosts. This is too funny to fix.
+/obj/projectile/resonant/gale_blast/proc/drag_along_movables(turf/from_turf, turf/to_turf)
+ if(!from_turf || !to_turf)
+ return
+
+ var/travel_dir = get_dir(from_turf, to_turf)
+ if(!travel_dir)
+ return
+
+ var/pushed_atoms = 0
+
+ // Checks if we're allowed to drag it and if the space can be passed through.
+ for(var/atom/movable/movable_instance as anything in from_turf)
+ // We cap the amount of items that can be moved similar to push brooms to prevent you from casting LAGIMUS MAXIMUS.
+ if(pushed_atoms >= THAUMATURGE_GALE_BLAST_PUSH_LIMIT)
+ break
+
+ if(!can_wind_drag(movable_instance, from_turf))
+ continue
+
+ if(!movable_instance.CanPass(movable_instance, to_turf, travel_dir))
+ continue
+
+ // Drags along the object.
+ movable_instance.Move(to_turf)
+ // Also extinguishes it.
+ movable_instance.extinguish()
+ pushed_atoms++
+
+/// Checks if we can drag along the target.
+/obj/projectile/resonant/gale_blast/proc/can_wind_drag(atom/movable/movable_instance, turf/current_turf)
+ if(!movable_instance)
+ return FALSE
+
+ // Core rule: anchored objects do not move
+ if(movable_instance.anchored)
+ return FALSE
+
+ // Do not drag living mobs; knockback is handled separately
+ if(isliving(movable_instance))
+ return FALSE
+
+ // Only drag things actually sitting on the turf
+ if(movable_instance.loc != current_turf)
+ return FALSE
+
+ return TRUE
+
+/// Extinguishes fire in the target space.
+/obj/projectile/resonant/gale_blast/proc/extinguish_hotspots_on_turf(turf/current_turf)
+ if(!current_turf)
+ return
+
+ for(var/obj/effect/hotspot/hotspot_instance as anything in current_turf)
+ if(hotspot_instance.type != /obj/effect/hotspot) // only delete fires!
+ continue
+ qdel(hotspot_instance)
+
+/*
+ On hit effects below
+*/
+
+// Helpers functions do most of the work here.
+/obj/projectile/resonant/gale_blast/on_hit(atom/target, blocked, pierce_hit)
+ . = ..()
+ extinguish_hit_target(target)
+ apply_knockback(target)
+
+
+/// Handles the knockback on hit
+/obj/projectile/resonant/gale_blast/proc/apply_knockback(atom/hit_atom)
+ var/atom/movable/movable_target = hit_atom
+ if(!istype(movable_target))
+ return
+
+ if(movable_target.anchored)
+ return
+
+ var/turf/target_turf = get_turf(movable_target)
+ if(!target_turf)
+ return
+
+ // Knockback direction = projectile travel direction at impact
+ var/knockback_dir = dir
+ if(!knockback_dir)
+ return
+
+ var/turf/destination_turf = target_turf
+ for(var/step_count in 1 to 3)
+ var/turf/next_turf = get_step(destination_turf, knockback_dir)
+ if(!next_turf)
+ break
+ destination_turf = next_turf
+
+ // chance to knockback slightly farther based on affinity.
+ // This is really ugly.
+ var/knockback_dist = knockback_range
+ var/datum/action/cooldown/power/thaumaturge/power = creating_power
+ var/affinity = power.affinity
+ var/extra_knockback_chance = clamp(25 * (affinity - 3), 0, 100) // Caps out at 50 for T5.
+
+ if(prob(extra_knockback_chance))
+ knockback_dist += 1
+
+ movable_target.safe_throw_at(destination_turf, knockback_dist, 2, firer)
+ playsound(movable_target, 'sound/effects/bamf.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+
+// Extinguishes the target we just hit.
+/obj/projectile/resonant/gale_blast/proc/extinguish_hit_target(atom/hit_atom)
+ if(!hit_atom)
+ return
+
+ if(isliving(hit_atom))
+ var/mob/living/living_target = hit_atom
+ living_target.extinguish_mob()
+ return
+
+ // Items / other atoms that can burn
+ hit_atom.extinguish()
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/hemomancy.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/hemomancy.dm
new file mode 100644
index 00000000000000..065906b3ddb854
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/hemomancy.dm
@@ -0,0 +1,336 @@
+/*
+ Variant of spell preperation. Rather than needing to choose and prepare spells, you have access to all your chosen spells (with an increased root cost to compensate).
+ You instead pay the spell's cost in blood, proportional to charge_cost.
+ Comes with an innate ability to drain blood from various things much like sanguine absorption. So you can aura-farm blood without needing to quite literally drink it.
+ Given the cultist stereotypes, your magic is also now affected by holy resistance.
+*/
+/datum/power/thaumaturge_root/hemomancy
+ name = "Hemomancy"
+ desc = "You cast spells by channeling your blood. All your spells drain your blood when wielding them, usually 4 * the power's allocation cost. \
+ Blood exceeding 110% of your natural blood threshold is consumed at higher rates to boost the affinity of your spells, empowering them to higher levels of Affinity, up to a maximum of 6. This does not trigger on spells that have a chance refund charges based on affinity. \
+ \nYou cannot gain Affinity from items, you do not benefit from random chance to refund charges on spells, and all your spells are now affected by holy resistance.\
+ \nYou also gain the Channel Blood action. Using it allows you to transfer blood from various sources back to you (and converts the blood-type to yours), and grants Affinity 3 (4 if you're a Hemophage) while the channel is active. Requires an empty hand."
+ security_record_text = "Subject is capable of wielding their blood to perform thaumaturgic magic."
+ action_path = /datum/action/cooldown/power/thaumaturge/channel_blood
+ species_blacklist = list(/datum/species/android, /datum/species/android/holosynth, /datum/species/golem, /datum/species/plasmaman, /datum/species/ethereal, /datum/species/jelly, /datum/species/pod, /datum/species/snail) // You can't do blood magic without blood, duh!
+ value = 5
+
+/datum/power/thaumaturge_root/hemomancy/post_add()
+ if(!power_holder) // So it doesn't runtime at init
+ return
+ // Spell preperation is so complicated we basically handle it all in a component, including the UI part.
+ power_holder.AddComponent(/datum/component/thaumaturge/hemomancy, power_holder)
+ . = ..()
+
+/datum/power/thaumaturge_root/hemomancy/remove()
+ . = ..()
+ if(!power_holder)
+ return
+ var/tobedel = power_holder.GetComponent(/datum/component/thaumaturge/hemomancy)
+ QDEL_NULL(tobedel)
+
+/datum/action/cooldown/power/thaumaturge/channel_blood
+ name = "Channel Blood"
+ desc = "Empowers your hand with the ability to absorb blood from the touched object or creature, and converting it to your own bloodtype. Whilst active, you have Affinity 3 with your Thaumaturge spells, but cannot use that hand until you cancel the ability."
+ button_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "manip"
+ max_charges = null
+ cooldown_time = 15
+
+ prep_cost = 0
+
+/datum/action/cooldown/power/thaumaturge/channel_blood/Grant(mob/granted_to)
+ . = ..()
+ RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+/datum/action/cooldown/power/thaumaturge/channel_blood/Remove(mob/removed_from)
+ . = ..()
+ UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
+
+/datum/action/cooldown/power/thaumaturge/channel_blood/use_action(mob/living/user, atom/target)
+ // Deletes the hand if active
+ if(active)
+ for(var/obj/item/melee/channel_blood/blood_hand in user.held_items)
+ qdel(blood_hand)
+ playsound(get_turf(user), 'sound/effects/magic/enter_blood.ogg', 30, TRUE, SILENCED_SOUND_EXTRARANGE)
+ to_chat(user, span_notice("You dissipate the blood around your hand back into your body."))
+ user.update_held_items()
+ active = FALSE
+ return TRUE
+
+ // Checks if the hand is occupied
+ if(user.get_active_held_item())
+ user.balloon_alert(user, "hand occupied!")
+ return FALSE
+
+ // Attempts to make a new blood hand
+ var/obj/item/melee/channel_blood/new_blood_hand = new(user)
+ if(!user.put_in_active_hand(new_blood_hand))
+ qdel(new_blood_hand)
+ return FALSE
+ playsound(user, 'sound/effects/magic/enter_blood.ogg', 30, TRUE, SILENCED_SOUND_EXTRARANGE)
+ active = TRUE
+ return TRUE
+
+/// When dispelled, the bloodied hand dissipates.
+/datum/action/cooldown/power/thaumaturge/channel_blood/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+ if(!active)
+ return NONE
+
+ for(var/obj/item/melee/channel_blood/blood_hand in owner.held_items)
+ qdel(blood_hand)
+ to_chat(owner, span_boldwarning("Your bloodied hand dissipates against your own volition!"))
+ owner.update_held_items()
+ break
+
+ active = FALSE
+ StartCooldownSelf(150)
+ return DISPEL_RESULT_DISPELLED
+
+/*
+ Touch-based hand that handles all the interactions.
+*/
+/obj/item/melee/channel_blood
+ name = "Channeled Blood"
+ desc = "A bloodied hand, entirely submerged in your own blood and sticking to it through supernatural means. It appears ready to burst as you channel your magic. \
+ Can be used to drain blood from objects and creatures."
+ icon = 'icons/obj/weapons/hand.dmi'
+ lefthand_file = 'icons/mob/inhands/items/touchspell_lefthand.dmi'
+ righthand_file = 'icons/mob/inhands/items/touchspell_righthand.dmi'
+ icon_state = "scream_for_me"
+ inhand_icon_state = "disintegrate"
+ item_flags = ABSTRACT | HAND_ITEM
+ w_class = WEIGHT_CLASS_HUGE
+ throwforce = 0
+ throw_range = 0
+ throw_speed = 0
+ affinity = 3 // this doesn't do anything by itself: though it alters the description to say it gives Affinity 3.
+ /// How much blood we drain per touch from objects.
+ var/drain_amount = 25
+ /// How much blood we drain from a mob per second.
+ var/mob_drain_amount = 10
+
+/obj/item/melee/channel_blood/Initialize(mapload)
+ . = ..()
+ ADD_TRAIT(src, TRAIT_NODROP, INNATE_TRAIT)
+
+// Adds a listener to the affinity check
+/obj/item/melee/channel_blood/equipped(mob/user, slot, initial = FALSE)
+ . = ..()
+ RegisterSignal(user, COMSIG_THAUMATURGE_AFFINITY_QUERY, PROC_REF(on_thaumaturge_affinity_query))
+ // Sets the affinity higher if we're a hemophage.
+ if(ishemophage(user))
+ affinity = 4
+
+// Removes the affinity check listener.
+/obj/item/melee/channel_blood/dropped(mob/user, silent = FALSE)
+ . = ..()
+ if(!isliving(user))
+ return
+ UnregisterSignal(user, COMSIG_THAUMATURGE_AFFINITY_QUERY)
+
+// Removes the affinity check listener.
+/obj/item/melee/channel_blood/Destroy(force)
+ if(isliving(loc))
+ var/mob/living/holder = loc
+ UnregisterSignal(holder, COMSIG_THAUMATURGE_AFFINITY_QUERY)
+ return ..()
+
+/// While this hand is held, it contributes +3 affinity to thaumaturge actions.
+/obj/item/melee/channel_blood/proc/on_thaumaturge_affinity_query(mob/living/source, datum/action/cooldown/power/thaumaturge/action)
+ SIGNAL_HANDLER
+ if(!action)
+ return
+ action.affinity += 3
+
+ // Checks if we're a hemophage and if so give a +1 bonus to affinity ontop of that.
+ if(ishemophage(source))
+ action.affinity += 1
+
+// Handles blood draining and blocks default melee attack behavior.
+/obj/item/melee/channel_blood/pre_attack(atom/target, mob/living/user, list/modifiers, list/attack_modifiers)
+ if(!isliving(user) || !target)
+ return TRUE
+ if(get_dist(user, target) > 1)
+ return TRUE
+
+ // If the target is a living mob, we start a beam effect.
+ if(isliving(target))
+ var/mob/living/target_mob = target
+ if(start_blood_channel(target_mob, user))
+ to_chat(user, span_notice("You begin channeling blood from [target_mob]."))
+ else
+ to_chat(user, span_warning("You failed to channel blood from [target_mob]!"))
+ return
+
+ var/drained = drain_blood_from_target(target, user, drain_amount)
+ if(drained <= 0)
+ to_chat(user, span_warning("You failed to draw any blood!"))
+ return
+
+ user.blood_volume += drained
+ to_chat(user, span_notice("You siphon [round(drained, 0.1)]u of blood into yourself."))
+ playsound(get_turf(user), 'sound/effects/splat.ogg', 30, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+
+/// Proc that determines what function we call for the target, depending on what we're drawing blood from.
+/obj/item/melee/channel_blood/proc/drain_blood_from_target(atom/target, mob/living/user, amount)
+ if(istype(target, /obj/item/reagent_containers))
+ return drain_blood_from_container(target, amount)
+ if(istype(target, /obj/effect/decal/cleanable/blood))
+ return drain_blood_from_decal(target, amount)
+ return 0
+
+/// Starts or refreshes blood channeling on a mob.
+/obj/item/melee/channel_blood/proc/start_blood_channel(mob/living/target_mob, mob/living/user)
+ if(!target_mob || !user)
+ return FALSE
+ // Respect anti-magic protections similarly to power anti_magic_on_target handling.
+ if(target_mob.can_block_resonance(1) || target_mob.can_block_magic(MAGIC_RESISTANCE | MAGIC_RESISTANCE_HOLY, charge_cost = 1))
+ return FALSE
+ if(target_mob.get_blood_reagent() != /datum/reagent/blood || target_mob.blood_volume <= 0)
+ return FALSE
+
+ // Removes it if its already there.
+ target_mob.remove_status_effect(/datum/status_effect/power/thaumaturge_blood_channeling)
+ return target_mob.apply_status_effect(/datum/status_effect/power/thaumaturge_blood_channeling, user, mob_drain_amount)
+
+/// Acquires blood from a reagent container.
+/obj/item/melee/channel_blood/proc/drain_blood_from_container(obj/item/reagent_containers/container, amount)
+ if(!container?.reagents || amount <= 0)
+ return 0
+ var/available = container.reagents.get_reagent_amount(/datum/reagent/blood)
+ if(available <= 0)
+ return 0
+ var/to_take = min(amount, available)
+ container.reagents.remove_reagent(/datum/reagent/blood, to_take, include_subtypes = TRUE)
+ return to_take
+
+/// Acquires blood from decals.
+/obj/item/melee/channel_blood/proc/drain_blood_from_decal(obj/effect/decal/cleanable/blood/blood_decal, amount)
+ if(!blood_decal || amount <= 0)
+ return 0
+ if(blood_decal.dried || blood_decal.bloodiness <= 0)
+ return 0
+ if(!(blood_decal.decal_reagent == /datum/reagent/blood || blood_decal.reagents?.has_reagent(/datum/reagent/blood)))
+ return 0
+
+ var/available_units = blood_decal.bloodiness * BLOOD_TO_UNITS_MULTIPLIER
+ if(available_units <= 0)
+ return 0
+
+ var/to_take = min(amount, available_units)
+ if(to_take >= available_units)
+ if(blood_decal.reagents)
+ blood_decal.reagents.remove_reagent(/datum/reagent/blood, to_take, include_subtypes = TRUE)
+ qdel(blood_decal)
+ else
+ var/bloodiness_to_remove = to_take / BLOOD_TO_UNITS_MULTIPLIER
+ blood_decal.adjust_bloodiness(-bloodiness_to_remove)
+ if(blood_decal.reagents)
+ blood_decal.reagents.remove_reagent(/datum/reagent/blood, to_take, include_subtypes = TRUE)
+
+ return to_take
+
+/*
+ Status effect used for continuous blood channeling from mob targets.
+*/
+/datum/status_effect/power/thaumaturge_blood_channeling
+ id = "thaumaturge_blood_channeling"
+ duration = -1
+ tick_interval = 1 SECONDS
+ alert_type = /atom/movable/screen/alert/status_effect/thaumaturge_blood_channeling
+
+ /// Who receives the siphoned blood.
+ var/mob/living/channel_origin
+ /// Continuous beam visual.
+ var/datum/beam/channel_beam
+ /// Blood moved each tick.
+ var/channel_amount = 10
+
+// Passes the channel amount from the var over.
+/datum/status_effect/power/thaumaturge_blood_channeling/on_creation(mob/living/new_owner, mob/living/new_channel_origin, new_channel_amount)
+ channel_origin = new_channel_origin
+ if(isnum(new_channel_amount))
+ channel_amount = max(0, new_channel_amount)
+ . = ..()
+
+// Creates the beaaam.
+/datum/status_effect/power/thaumaturge_blood_channeling/on_apply()
+ . = ..()
+ if(!isliving(owner) || !isliving(channel_origin))
+ return FALSE
+ if(get_dist(owner, channel_origin) > 1)
+ return FALSE
+ channel_beam = owner.Beam(channel_origin, icon_state = "blood", time = 10 MINUTES, maxdistance = 1)
+ to_chat(owner, span_userdanger("You feel your blood being siphoned by [channel_origin]!"))
+ return TRUE
+
+// Deletes the beaaaam
+/datum/status_effect/power/thaumaturge_blood_channeling/on_remove()
+ QDEL_NULL(channel_beam)
+ return ..()
+
+// Transfers blood on tick.
+/datum/status_effect/power/thaumaturge_blood_channeling/tick(seconds_between_ticks)
+ var/mob/living/channel_target = owner
+ if(!channel_target || !channel_origin)
+ qdel(src)
+ return
+
+ // Channel requires adjacency.
+ if(get_dist(channel_target, channel_origin) > 1)
+ qdel(src)
+ return
+
+ // Origin must still be actively channeling with the blood hand and action toggled on.
+ if(!origin_can_channel())
+ qdel(src)
+ return
+
+ // Respect anti-magic and blood validity each tick.
+ if(channel_target.can_block_resonance(1) || channel_target.can_block_magic(MAGIC_RESISTANCE | MAGIC_RESISTANCE_HOLY, charge_cost = 1))
+ qdel(src)
+ return
+ if(channel_target.get_blood_reagent() != /datum/reagent/blood || channel_target.blood_volume <= 0)
+ qdel(src)
+ return
+
+ var/transferred = min(channel_amount, channel_target.blood_volume)
+ // if juice-box, delete.
+ if(transferred <= 0)
+ qdel(src)
+ return
+
+ channel_target.blood_volume = max(channel_target.blood_volume - transferred, 0)
+ channel_origin.blood_volume += transferred
+
+/// Validates if the original caster meets the prerequisites.
+/datum/status_effect/power/thaumaturge_blood_channeling/proc/origin_can_channel()
+ if(!isliving(channel_origin))
+ return FALSE
+
+ // If the blood hand is active
+ var/has_live_blood_hand = FALSE
+ for(var/obj/item/melee/channel_blood/blood_hand as anything in channel_origin.held_items)
+ if(QDELETED(blood_hand) || blood_hand.loc != channel_origin)
+ continue
+ has_live_blood_hand = TRUE
+ break
+ if(!has_live_blood_hand)
+ return FALSE
+
+ // If the power is considered active.
+ for(var/datum/action/action as anything in channel_origin.actions)
+ if(!istype(action, /datum/action/cooldown/power/thaumaturge/channel_blood))
+ continue
+ var/datum/action/cooldown/power/thaumaturge/channel_blood/channel_action = action
+ return !!channel_action.active
+ return FALSE
+
+/atom/movable/screen/alert/status_effect/thaumaturge_blood_channeling
+ name = "Blood Channeling"
+ desc = "Your blood is being drained!"
+ icon = 'icons/mob/actions/actions_cult.dmi'
+ icon_state = "manip"
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm
new file mode 100644
index 00000000000000..cadb2597d2d919
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm
@@ -0,0 +1,293 @@
+/*
+ Deviates from standard norms by being a projectile spell with click functionalities, but using neither because it is too 'unique' in its application.
+ Really its an example of what being clever gets you.
+*/
+
+/datum/power/thaumaturge/magical_barrage
+ name = "Magical Barrage"
+ desc = "Shoots a volley of magic projectiles equal to your Affinity + 2. You can either fire single shots with a short delay between shots, or shoot all your remaining shots in a barrage. \
+ \nRequires Affinity 3."
+ security_record_text = "Subject can conjure and shoot a volley of magical lasers."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+
+ action_path = /datum/action/cooldown/power/thaumaturge/magical_barrage
+ required_powers = list(/datum/power/thaumaturge_root)
+ required_allow_subtypes = TRUE
+
+/datum/action/cooldown/power/thaumaturge/magical_barrage
+ name = "Magical Barrage"
+ desc = "Shoots a volley of magic projectiles equal to your Affinity + 2. Left click to fire single shots with a short delay between shots, or right click to shoot all your remaining shots in a barrage."
+ button_icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ button_icon_state = "arcane_barrage"
+
+ required_affinity = 3
+ prep_cost = 5
+ anti_magic_on_target = FALSE
+
+ /// The projectile we fire
+ var/obj/projectile/projectile_path = /obj/projectile/resonant/magic_barrage
+
+ /// How many missiles we have left to fire.
+ var/missiles_remaining = 0
+
+ /// List of missiles currently oribing us
+ var/list/orbiting_missiles = list()
+ /// Times between each shot materializing
+ var/time_between_initial_missiles = 0.12 SECONDS // Missiles spawned sequentially to prevent stacking.
+ /// The radius in which the missiles orbit us
+ var/missile_orbit_radius = 20
+ /// The speed at which the missiles orbit us.
+ var/missile_rotation_speed = 15
+
+ /// world.time until we can fire our next shot
+ var/next_single_shot_time = 0
+ /// Cooldown for single shots in miliseconds.
+ var/single_shot_delay = 3
+
+ /// world.time wind-up before you can start casting.
+ var/barrage_ready_time = 0
+
+/datum/action/cooldown/power/thaumaturge/magical_barrage/use_action(mob/living/user, atom/target)
+ // Toggle the barrage firing mode.
+ if(active)
+ disable_barrage(user, span_warning("You dispel the magic missiles."))
+ return FALSE
+
+ if(user != owner)
+ return FALSE
+
+ active = TRUE
+ missiles_remaining = clamp(affinity + 2, 3, 10)
+ next_single_shot_time = world.time // allow immediate first shot
+
+ // prevent firing until all the projectiles are ready
+ barrage_ready_time = world.time + round((missiles_remaining - 1) * time_between_initial_missiles)
+ spawn_orbitals(missiles_remaining)
+ RegisterSignal(owner, COMSIG_MOB_CLICKON, PROC_REF(on_owner_clickon))
+ to_chat(owner, span_notice("Magical missiles orbit you. Left-click: Fire one. Right-click: Fire all."))
+ return TRUE
+
+/// Turns off barrage mode and cleans up signals + orbitals.
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/disable_barrage(mob/living/user, message)
+ if(!active)
+ return
+
+ active = FALSE
+ missiles_remaining = 0
+
+ if(owner)
+ UnregisterSignal(owner, COMSIG_MOB_CLICKON)
+
+ clear_orbitals()
+
+ if(user && message)
+ to_chat(user, message)
+
+/// Click handler while barrage mode is active.
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/on_owner_clickon(mob/living/clicker, atom/target, params)
+ SIGNAL_HANDLER
+
+ if(!active)
+ return
+ if(clicker != owner)
+ return
+ if(missiles_remaining <= 0)
+ disable_barrage(owner, null)
+ return
+
+ // Don't shoot yourself dummy.
+ if(target == owner)
+ return
+
+ // Params may already be a list (depends on the signal source).
+ var/list/modifiers
+ if(islist(params))
+ modifiers = params
+ else
+ modifiers = params2list(params)
+
+ // Right click: dump all remaining missiles.
+ if(LAZYACCESS(modifiers, "right") || LAZYACCESS(modifiers, "button") == "right")
+ if(fire_projectile_shotgun(owner, target, projectile_path, pellet_count = missiles_remaining))
+ disable_barrage(owner, null)
+ return
+
+ // Left click: single shot
+ if(fire_single_shot(owner, target))
+ missiles_remaining--
+ remove_one_orbital()
+ if(missiles_remaining <= 0)
+ disable_barrage(owner, null)
+
+/// Proc for firing a single shot.
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/fire_single_shot(mob/living/user, atom/target)
+ if(world.time < next_single_shot_time) // anti spam-click.
+ return FALSE
+
+ next_single_shot_time = world.time + single_shot_delay
+
+ playsound(owner, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ return fire_projectile(user, target, projectile_path)
+
+
+/// Special proc for shotgunning it.
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/fire_projectile_shotgun(mob/living/user, atom/target, obj/projectile/projectile, pellet_count = 5, cone_degrees = 18, angle_jitter_degrees = 1)
+ SHOULD_CALL_PARENT(TRUE)
+
+ if(!can_fire_now(user))
+ return FALSE
+
+ var/projectile_path = projectile
+ if(!projectile_path || !user || !target)
+ return FALSE
+
+ var/turf/user_turf = get_turf(user)
+ var/turf/target_turf = get_turf(target)
+ if(!user_turf || !target_turf)
+ return FALSE
+
+ pellet_count = clamp(pellet_count, 1, 50)
+ cone_degrees = clamp(cone_degrees, 0, 90)
+ angle_jitter_degrees = clamp(angle_jitter_degrees, 0, 15)
+
+ // Base angle from shooter to clicked turf
+ var/base_angle = get_angle(user_turf, target_turf)
+
+ // Evenly distribute pellets across [-cone/2 .. +cone/2]
+ var/half_cone = cone_degrees / 2
+ var/step = (pellet_count > 1) ? (cone_degrees / (pellet_count - 1)) : 0
+
+ var/fired_any = FALSE
+
+ for(var/pellet_index in 1 to pellet_count)
+ var/angle_offset
+
+ if(pellet_count <= 1 || cone_degrees <= 0)
+ angle_offset = 0
+ else
+ angle_offset = -half_cone + (pellet_index - 1) * step
+
+ // Small jitter so it doesn't look like a perfectly spaced laser fan
+ if(angle_jitter_degrees)
+ angle_offset += rand(-angle_jitter_degrees * 10, angle_jitter_degrees * 10) / 10
+
+ var/obj/projectile/projectile_instance = new projectile_path(user_turf)
+ ready_projectile(projectile_instance, target, user)
+
+ projectile_instance.fire(base_angle + angle_offset, target)
+ projectile_instance.spread = 2
+ fired_any = TRUE
+
+ playsound(owner, 'sound/effects/magic/magic_missile.ogg', 75, TRUE)
+ return fired_any
+
+/// checks if we're allowed to fire after cast
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/can_fire_now(mob/living/user)
+ if(world.time < barrage_ready_time)
+ user.balloon_alert(user, "Wait for the missiles!")
+ return FALSE
+ return TRUE
+
+/// Flavor override: hemomancy users fire blood bolts instead of arcane bolts.
+/datum/action/cooldown/power/thaumaturge/magical_barrage/ready_projectile(obj/projectile/projectile_instance, atom/target, mob/living/user)
+ . = ..()
+ if(!projectile_instance || !isliving(user))
+ return
+ if(user.GetComponent(/datum/component/thaumaturge/hemomancy))
+ projectile_instance.icon_state = "blood_bolt"
+
+
+// the projectile in question
+/obj/projectile/resonant/magic_barrage
+ name = "magic missile"
+ icon_state = "arcane_barrage"
+ damage = 9
+ damage_type = BURN
+ armour_penetration = 25 // Great for civilian use, less-so on armored opponents.
+ armor_flag = LASER
+ pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE // unfortunately for you this is a magical LASER
+
+/* Code for orbitals below */
+/obj/effect/magic_missile_orbiter
+ name = "magic missile"
+ icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ icon_state = "arcane_barrage"
+ mouse_opacity = MOUSE_OPACITY_TRANSPARENT
+ layer = ABOVE_MOB_LAYER
+ anchored = TRUE
+ alpha = 180
+
+/// Spawns the oribitng effects around the mob.
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/spawn_orbitals(amount)
+ clear_orbitals()
+ if(amount <= 0 || QDELETED(owner))
+ return
+
+ for(var/missile_num in 1 to amount)
+ var/time_until_created = (missile_num - 1) * time_between_initial_missiles
+ if(time_until_created <= 0)
+ create_orbital()
+ else
+ addtimer(CALLBACK(src, PROC_REF(create_orbital)), time_until_created)
+
+/// Creates one missile and makes it orbit
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/create_orbital()
+ if(QDELETED(src) || QDELETED(owner))
+ return
+
+ var/obj/effect/magic_missile_orbiter/orbiter = new(get_turf(owner))
+ orbiter.transform = matrix()
+ orbiter.transform.Scale(0.5, 0.5)
+ orbiter.icon = projectile_path.icon // if you end up editing the projectile, it should also affect the orbitals.
+ orbiter.icon_state = owner?.GetComponent(/datum/component/thaumaturge/hemomancy) ? "blood_bolt" : projectile_path.icon_state // changes the icon_state to the blood ones if we have hemomancy.
+ orbiting_missiles += orbiter
+ orbiter.orbit(owner, missile_orbit_radius, rotation_speed = missile_rotation_speed)
+ RegisterSignal(orbiter, COMSIG_QDELETING, PROC_REF(on_orbiter_deleted))
+ playsound(owner, 'sound/effects/magic/blink.ogg', 75, TRUE)
+
+/// Clears all orbiting missiles.
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/clear_orbitals()
+ if(!length(orbiting_missiles))
+ return
+ QDEL_LIST(orbiting_missiles)
+ orbiting_missiles.Cut()
+
+/// Removes exactly one orbital.
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/remove_one_orbital()
+ if(!length(orbiting_missiles))
+ return FALSE
+ qdel(orbiting_missiles[1])
+ return TRUE
+
+/// On qdel signaler that removes it from the orbiting list.
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/on_orbiter_deleted(obj/effect/magic_missile_orbiter/orbiter)
+ SIGNAL_HANDLER
+
+ if(!(orbiter in orbiting_missiles))
+ return
+
+ if(!QDELETED(owner))
+ orbiter.stop_orbit(owner.orbiters)
+
+ orbiting_missiles -= orbiter
+
+// Dispel functionality
+/datum/action/cooldown/power/thaumaturge/magical_barrage/Grant(mob/granted_to)
+ . = ..()
+ if(resonant)
+ RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+/datum/action/cooldown/power/thaumaturge/magical_barrage/Remove(mob/removed_from)
+ . = ..()
+ if(resonant)
+ UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
+
+/// On dispel, poof there go your orbitals.
+/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+ if(!active)
+ return NONE
+ disable_barrage(owner, span_userdanger("Your magic missiles vanish as they are dispelled!"))
+ return DISPEL_RESULT_DISPELLED
+
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm
new file mode 100644
index 00000000000000..5901798d9084d0
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm
@@ -0,0 +1,123 @@
+/*
+ For the people that don't want to hunt for tools, or are just too bothered to carry one.
+*/
+/datum/power/thaumaturge/phantasmal_tool
+ name = "Phantasmal Tool"
+ desc = "Summons a basic tool of your choice in your hand, that disappears after a duration, or if it is dropped/used to attack a person. \
+ \nRequires Affinity 1 to cast. Affinity gives a chance to not consume charges on cast."
+ security_record_text = "Subject can conjure ephemeral tools out of thin air."
+ security_threat = POWER_THREAT_MAJOR
+ value = 3
+
+ action_path = /datum/action/cooldown/power/thaumaturge/phantasmal_tool
+ required_powers = list(/datum/power/thaumaturge_root)
+ required_allow_subtypes = TRUE
+
+/datum/action/cooldown/power/thaumaturge/phantasmal_tool
+ name = "Phantasmal Tool"
+ desc = "Summons a basic tool of your choice in your hand, that disappears after a duration, or if it is dropped/used to attack a person."
+ button_icon = 'icons/obj/weapons/club.dmi'
+ button_icon_state = "hypertool"
+
+ required_affinity = 1
+ prep_cost = 3
+ power_refunds = TRUE
+ power_refund_chance = THAUMATURGE_REFUND_MULT_BASE
+ power_refund_affinity_bonus = THAUMATURGE_REFUND_MULT_AFFINITY
+
+/datum/action/cooldown/power/thaumaturge/phantasmal_tool/use_action(mob/living/user, atom/target)
+ if(user.get_active_held_item() && user.get_inactive_held_item())
+ user.balloon_alert(user, "hands are not empty!")
+
+ var/list/tool_type_to_image = get_phantasmal_tool_radial_images()
+
+ // show_radial_menu returns the key from the assoc list.
+ var/selected_tool_type = show_radial_menu(
+ user,
+ user, // "anchor" just for placement; using the user keeps it simple
+ tool_type_to_image,
+ tooltips = TRUE
+ )
+
+ if(!selected_tool_type)
+ return FALSE
+
+ // Creates item, adds the special phantasmal tool properties, give to user.
+ var/obj/item/new_tool_item = new selected_tool_type(user)
+ new_tool_item.AddElement(/datum/element/phantasmal_tool)
+ if(!user.put_in_hands(new_tool_item))
+ qdel(new_tool_item) // destroys the item if it fails to put it in our hands, as it shouldn't ever exist out of hands.
+ playsound(user, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+ return TRUE
+
+/// Checks if we're capable of using the menu
+/datum/action/cooldown/power/thaumaturge/phantasmal_tool/proc/phantasmal_tool_menu_check(mob/user)
+ if(!istype(user))
+ return FALSE
+ if(user.incapacitated)
+ return FALSE
+ return TRUE
+
+/// Gets all the images of the tools within phantasmal tool
+/datum/action/cooldown/power/thaumaturge/phantasmal_tool/proc/get_phantasmal_tool_radial_images()
+ var/static/list/tool_type_to_image
+ if(tool_type_to_image)
+ return tool_type_to_image
+
+ tool_type_to_image = list()
+
+ var/list/allowed_tool_types = list(
+ /obj/item/weldingtool,
+ /obj/item/screwdriver,
+ /obj/item/wirecutters,
+ /obj/item/crowbar,
+ /obj/item/wrench,
+ /obj/item/multitool
+ )
+
+ for(var/tool_type in allowed_tool_types)
+ // One-time temporary instance to fetch icon/icon_state reliably
+ var/obj/item/temporary_tool = new tool_type
+ tool_type_to_image[tool_type] = image(temporary_tool.icon, temporary_tool.icon_state)
+ qdel(temporary_tool)
+
+
+ return tool_type_to_image
+
+
+// The element we attach with phantasmal tool. Handles making it harmless, duration and disappearing on.
+/datum/element/phantasmal_tool
+ element_flags = ELEMENT_DETACH_ON_HOST_DESTROY
+
+ /// The item we're attached to.
+ var/obj/item/attached_item
+
+/datum/element/phantasmal_tool/Attach(datum/target)
+ . = ..()
+ attached_item = target
+ attached_item.item_flags = DROPDEL | ABSTRACT
+ attached_item.alpha = 200
+ attached_item.color = "#66cbdd"
+ attached_item.AddElementTrait(TRAIT_ON_HIT_EFFECT, REF(src), /datum/element/on_hit_effect)
+ RegisterSignal(attached_item, COMSIG_ON_HIT_EFFECT, PROC_REF(break_on_hit))
+ RegisterSignal(attached_item, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+/datum/element/phantasmal_tool/Detach(datum/source)
+ UnregisterSignal(source, COMSIG_ON_HIT_EFFECT)
+ UnregisterSignal(source, COMSIG_ATOM_DISPEL)
+ REMOVE_TRAIT(source, TRAIT_ON_HIT_EFFECT, REF(src))
+ return ..()
+
+/// Listener so that we shatter on hit
+/datum/element/phantasmal_tool/proc/break_on_hit(datum/source, atom/damage_target, hit_zone, throw_hit)
+ SIGNAL_HANDLER
+ if(ismob(damage_target))
+ playsound(attached_item, 'sound/items/ceramic_break.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ qdel(attached_item)
+
+/// On dispel, we shatter too.
+/datum/element/phantasmal_tool/proc/on_dispel(datum/source, atom/dispeller)
+ SIGNAL_HANDLER
+ if(attached_item)
+ playsound(attached_item, 'sound/items/ceramic_break.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ qdel(attached_item)
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm
new file mode 100644
index 00000000000000..6282f01770fdf1
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm
@@ -0,0 +1,221 @@
+// The classic cantrip that does neat things to make you feel magical, without needing to spend precious spell charges.
+/datum/power/thaumaturge/prestidigtation
+ name = "Prestidigtation"
+ desc = "Perform a minor feat of magic. Right-click to select between modes, Left-click to execute.\
+ \nAllows you to do various actions like summoning sparks, cleaning objects and flavor food.\
+ \nRequires Affinity 1. Does not scale with Affinity and does not use charges."
+ security_record_text = "Subject can perform minor magical tricks, such as creating sparks and flavoring food."
+ value = 1
+
+ action_path = /datum/action/cooldown/power/thaumaturge/prestidigtation
+ required_powers = list(/datum/power/thaumaturge_root)
+ required_allow_subtypes = TRUE
+
+#define PRESTI_SUMMON_SPARKS "Summon Sparks"
+#define PRESTI_CLEAN_OBJECTS "Clean Objects"
+#define PRESTI_FLASH_MAGIC "Flash Magic"
+#define PRESTI_FLAVOR_GOOD "Flavor Food (Good)"
+#define PRESTI_FLAVOR_BAD "Flavor Food (Bad)"
+
+/datum/action/cooldown/power/thaumaturge/prestidigtation
+ name = "Prestidigtation"
+ desc = "Perform a minor feat of magic on an object or location within touch range. Right-click to select between modes, Left-click to execute"
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "spell_default"
+
+ max_charges = 0 // does not interact with the charges system
+ required_affinity = 1
+
+ click_to_activate = TRUE
+ target_range = 1
+ aim_assist = FALSE // complex targeting
+
+ /// Currently selected prestidigitation mode.
+ var/selected_mode = PRESTI_SUMMON_SPARKS
+
+/datum/action/cooldown/power/thaumaturge/prestidigtation/InterceptClickOn(mob/living/clicker, params, atom/target)
+ var/list/mods = params2list(params)
+ if(LAZYACCESS(mods, RIGHT_CLICK))
+ open_selection_menu(clicker)
+ return TRUE
+
+ return ..()
+
+/// Routes for our various unique actions
+/datum/action/cooldown/power/thaumaturge/prestidigtation/use_action(mob/living/user, atom/target)
+ switch(selected_mode)
+ if(PRESTI_SUMMON_SPARKS)
+ return summon_sparks(user, target)
+ if(PRESTI_CLEAN_OBJECTS)
+ return clean_objects(user, target)
+ if(PRESTI_FLASH_MAGIC)
+ return flash_magic(user, target)
+ if(PRESTI_FLAVOR_GOOD)
+ return flavor_food_good(user, target)
+ if(PRESTI_FLAVOR_BAD)
+ return flavor_food_bad(user, target)
+ return FALSE
+
+/// Right click selection menu that lets you choose what you are doing with your presti.
+/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/open_selection_menu(mob/living/user)
+ if(!check_selection_menu(user))
+ return FALSE
+
+ var/list/radial_items = get_radial_items()
+ var/choice = show_radial_menu(
+ user,
+ user, // anchor for placement
+ radial_items,
+ custom_check = CALLBACK(src, PROC_REF(check_selection_menu), user, target),
+ tooltips = TRUE
+ )
+
+ if(!choice)
+ return FALSE
+
+ selected_mode = choice
+ user.balloon_alert(user, "[selected_mode]")
+ return TRUE
+
+/// Validation for the right click
+/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/check_selection_menu(mob/living/user, atom/target)
+ if(QDELETED(src))
+ return FALSE
+ if(!istype(user))
+ return FALSE
+ if(!can_use(user, target))
+ return FALSE
+ return TRUE
+
+/// Populates our list of 'actions' our prestidigation spell can take.
+/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/get_radial_items()
+ var/static/list/radial_items
+ if(radial_items)
+ return radial_items
+
+ radial_items = list()
+
+ var/list/options = list(
+ PRESTI_SUMMON_SPARKS = list("icon" = 'icons/effects/effects.dmi', "state" = "electricity3"),
+ PRESTI_CLEAN_OBJECTS = list("icon" = 'icons/obj/watercloset.dmi', "state" = "soap"),
+ PRESTI_FLASH_MAGIC = list("icon" = 'icons/mob/actions/actions_spells.dmi', "state" = "exit_possession"),
+ PRESTI_FLAVOR_GOOD = list("icon" = 'icons/obj/drinks/mixed_drinks.dmi', "state" = "wizz_fizz"),
+ PRESTI_FLAVOR_BAD = list("icon" = 'icons/obj/drinks/drinks.dmi', "state" = "acidspitglass")
+ )
+
+ for(var/option_name in options)
+ var/list/entry = options[option_name]
+ var/datum/radial_menu_choice/choice = new()
+ choice.name = option_name
+ choice.image = image(icon = entry["icon"], icon_state = entry["state"])
+ radial_items[option_name] = choice
+
+ return radial_items
+
+/// Summons sparks as if you were spamming the RCD
+/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/summon_sparks(mob/living/user, atom/target)
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ return FALSE
+ do_sparks(5, FALSE, target_turf)
+ return TRUE
+
+/// Cleans everything on the target turf.
+/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/clean_objects(mob/living/user, atom/target)
+ var/turf/target_turf = get_turf(target)
+ if(!target_turf)
+ return FALSE
+ target_turf.wash(CLEAN_WASH, TRUE)
+ playsound(user, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ new /obj/effect/temp_visual/presti_clean(target_turf)
+ to_chat(user, span_notice("You clean [target]!"))
+ return TRUE
+
+/// Calls flash_blue. You did a magic thing; as placebo as it gets.
+/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/flash_magic(mob/living/user, atom/target)
+ flash_blue(target)
+ playsound(user, 'sound/effects/magic/charge.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ to_chat(user, span_notice("You make [target] feel magical! Wow!"))
+ return TRUE
+
+/// Flashes a target item blue briefly.
+/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/flash_blue(atom/target)
+ if(!isatom(target))
+ return
+ var/filter_id = "presti_flash"
+ target.add_filter(filter_id, 1, list(type = "outline", color = "#7266dd", size = 2, alpha = 255))
+ target.transition_filter(filter_id, list("alpha" = 0), 2 SECONDS) // this actually looks smoother
+ addtimer(CALLBACK(target, PROC_REF(remove_filter), filter_id), 2 SECONDS)
+
+/// Adds a flavor component to food that makes it slightly better
+/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/flavor_food_good(mob/living/user, atom/target)
+ if(!IS_EDIBLE(target))
+ return FALSE
+ if(!target.reagents)
+ target.create_reagents(5, INJECTABLE)
+ target.AddComponent(/datum/component/prestidigitation_flavor, TRUE)
+ flash_blue(target) // temporary filter just to show people are tampering with food
+ playsound(user, 'sound/effects/magic/charge.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+ to_chat(user, span_notice("You make [target] taste better!"))
+ return TRUE
+
+/// Adds a flavor component to food that makes it notoriously worse (its easier to screw it up than to make it better)
+/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/flavor_food_bad(mob/living/user, atom/target)
+ if(!IS_EDIBLE(target))
+ return FALSE
+ if(!target.reagents)
+ target.create_reagents(5, INJECTABLE)
+ target.AddComponent(/datum/component/prestidigitation_flavor, FALSE)
+ flash_blue(target) // temporary filter just to show people are tampering with food
+ playsound(user, 'sound/effects/magic/charge.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+ to_chat(user, span_notice("You make [target] taste worse!"))
+ return TRUE
+
+#undef PRESTI_SUMMON_SPARKS
+#undef PRESTI_CLEAN_OBJECTS
+#undef PRESTI_FLASH_MAGIC
+#undef PRESTI_FLAVOR_GOOD
+#undef PRESTI_FLAVOR_BAD
+
+/// Temp effect for the cleaning sparkles
+/obj/effect/temp_visual/presti_clean
+ icon_state = "shieldsparkles"
+ duration = 1 SECONDS
+
+/// Flavor component added by presti: tweaks quality and expires on eat.
+/datum/component/prestidigitation_flavor
+ dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS
+ /// TRUE for good flavor, FALSE for bad.
+ var/is_good = TRUE
+ /// Quality bonus applied to the food.
+ var/quality_bonus = 0
+
+/datum/component/prestidigitation_flavor/Initialize(good_flavor = TRUE)
+ if(!IS_EDIBLE(parent))
+ return COMPONENT_INCOMPATIBLE
+ is_good = good_flavor
+ quality_bonus = is_good ? 1 : 0
+
+/datum/component/prestidigitation_flavor/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_FOOD_EATEN, PROC_REF(on_food_eaten))
+ if(quality_bonus)
+ RegisterSignal(parent, COMSIG_FOOD_GET_EXTRA_COMPLEXITY, PROC_REF(add_quality), TRUE)
+
+/datum/component/prestidigitation_flavor/UnregisterFromParent()
+ UnregisterSignal(parent, COMSIG_FOOD_EATEN)
+ UnregisterSignal(parent, COMSIG_FOOD_GET_EXTRA_COMPLEXITY)
+
+/// Adds quality to a flavor component
+/datum/component/prestidigitation_flavor/proc/add_quality(datum/source, list/extra_complexity)
+ SIGNAL_HANDLER
+ extra_complexity[1] += quality_bonus
+
+/// Signaler for bad mood to give the disgusting fod modlet.
+/datum/component/prestidigitation_flavor/proc/on_food_eaten(datum/source, mob/eater, mob/feeder, bitecount, bite_consumption)
+ SIGNAL_HANDLER
+ if(!isliving(eater))
+ return
+ var/mob/living/living_eater = eater
+ if(!is_good) // just give the disgusting food moodlet despite existing taste.
+ living_eater.add_mood_event("presti_flavor_bad", /datum/mood_event/disgusting_food)
+ qdel(src)
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm
new file mode 100644
index 00000000000000..7a74f36d2403e1
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm
@@ -0,0 +1,337 @@
+/*
+ Heal someone using BLOOD.
+*/
+/datum/power/thaumaturge/sanguine_absorption
+ name = "Sanguine Absorption"
+ desc = "You draw nearby blood into the target. This draws up to 100u of blood from adjacent floor/wall splatters, containers and other mobs (in that order). It then transfers that blood to the target and converts it to universally accepted blood.\
+ \nAny excess blood in the target creature beyond 100% is transformed into healing, at a 10u per 4 damage ratio. This can only heal organic bodyparts and does not heal any damage-types besides Brute or Burn. This also does not affect creatures with non-blood bloodtypes such as Ethereals or Slimepeople. \
+ \nRequires Affinity 3. Additional affinity increases the healing ratio by 0.5 per affinity"
+ security_record_text = "Subject can draw blood from varying sources (including humanoids) and transmute it into universal blood, potentially healing the target."
+ value = 5
+
+ action_path = /datum/action/cooldown/power/thaumaturge/sanguine_absorption
+ required_powers = list(/datum/power/thaumaturge_root/hemomancy)
+
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption
+ name = "Sanguine Absorption"
+ desc = "You draw nearby blood into the target. This draws up to 100u of blood from adjacent floor/wall splatters, containers and other mobs (in that order). It then tranfers that blood to the target and converts it to universally accepted blood.\
+ \nAny excess blood in the target creature beyond 100% is transformed into healing, at a 10u per 4 damage ratio. This can only heal organic bodyparts and does not heal any damage-types besides Brute or Burn. This also does not affect creatures with non-blood bloodtypes such as Ethereals or Slimepeople."
+ button_icon = 'icons/effects/blood.dmi'
+ button_icon_state = "bubblegumfoot"
+
+ required_affinity = 3
+ prep_cost = 5
+ target_range = 4
+
+ use_time = 3 SECONDS
+ click_to_activate = TRUE
+
+ /// Healing ratio per 1u
+ var/healing_ratio = 0.4
+ /// How much extra affinity adds to the ratio.
+ var/affinity_healing_ratio_bonus = 0.05
+
+ /// How much blood (in units) we try to gather.
+ var/harvest_goal = 100
+
+ /// The special effect on the target
+ var/use_time_target_overlay = /obj/effect/temp_visual/sanguine_absorption
+ /// Tracks whether the current cast was dispelled mid-channel.
+ var/cast_interrupted_by_dispel = FALSE
+
+/datum/action/cooldown/power/aberrant/cocoon/InterceptClickOn(mob/living/clicker, params, atom/target)
+ ..()
+ // Always consume the click to avoid normal click interactions.
+ return TRUE
+
+// We do extra validation because we want to make sure containers aren't full and we aren't trying to put blood in a mob that can't hold it.
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/can_use(mob/living/user, atom/target)
+ . = ..()
+ if(istype(target, /obj/item/reagent_containers))
+ var/obj/item/reagent_containers/container = target
+ if(!container.reagents || container.reagents.total_volume >= container.reagents.maximum_volume)
+ user.balloon_alert(user, "container is full!")
+ return FALSE
+ return ..()
+
+ if(!isliving(target))
+ return FALSE
+
+ var/mob/living/target_mob = target
+ // ew, electricity/motor oil/plasma/whatever else aliens are composed of
+ if(!is_valid_blood_target(target_mob))
+ user.balloon_alert(user, "no blood to work with!")
+ return FALSE
+ if(target_mob.blood_volume <= BLOOD_VOLUME_NORMAL + 10 && !has_valid_blood_sources(get_turf(target_mob), target_mob))
+ user.balloon_alert(user, "no blood nearby!")
+ return FALSE
+
+// Special cast effects; we want the blood orb to appear above the target..
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/do_use_time(mob/living/user, atom/target)
+ cast_interrupted_by_dispel = FALSE
+ if(user)
+ RegisterSignal(user, COMSIG_ATOM_DISPEL, PROC_REF(on_cast_dispel))
+ if(target && !user == target) // prevents double registering
+ RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_cast_dispel))
+ var/target_use_overlay
+ if(use_time_target_overlay)
+ var/atom/overlay_obj = new use_time_target_overlay(null)
+ target_use_overlay = new /mutable_appearance(overlay_obj)
+ qdel(overlay_obj)
+ target.add_overlay(target_use_overlay)
+ // Spawns an indicator meant to show nearby targets that they are in the danger zone of having their blood donated to a blood drive.
+ var/target_location = get_turf(target)
+ for(var/atom/movable/source as anything in get_valid_blood_sources(target_location, null, null))
+ new /obj/effect/temp_visual/sanguine_absorption_target(get_turf(source))
+
+ target.visible_message(span_warning("[user] draws nearby blood into an orb above [target]!"))
+ playsound(target, 'sound/effects/magic/enter_blood.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ . = ..()
+ if(user)
+ UnregisterSignal(user, COMSIG_ATOM_DISPEL)
+ if(target && !user == target)
+ UnregisterSignal(target, COMSIG_ATOM_DISPEL)
+ if(cast_interrupted_by_dispel)
+ return FALSE
+ if(target_use_overlay && !QDELETED(target))
+ target.cut_overlay(target_use_overlay)
+
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/use_action(mob/living/user, atom/target)
+// Filling reagent containers with blood.
+ if(istype(target, /obj/item/reagent_containers))
+ var/obj/item/reagent_containers/container = target
+ if(!container.reagents)
+ return FALSE
+
+ // If between the cast time finishing and this happening the container is filled.
+ var/remaining_capacity = container.reagents.maximum_volume - container.reagents.total_volume
+ if(remaining_capacity <= 0)
+ user.balloon_alert(user, "container is full!")
+ return FALSE
+
+ var/harvest_cap = min(harvest_goal, remaining_capacity) // harvest_goal capped by the spare space in teh cotnainer
+ var/turf/center = get_turf(container)
+ if(!center)
+ return FALSE
+
+ // Go get some blood.
+ var/harvested = harvest_blood(center, harvest_cap, null, container)
+ if(harvested <= 0) // you failed.
+ user.balloon_alert(user, "no blood nearby!")
+ return FALSE
+ container.reagents.add_reagent(/datum/reagent/blood, harvested)
+
+ user.visible_message(span_notice("Blood gathers into [target]."))
+ playsound(target, 'sound/effects/splat.ogg', 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+
+// Filling mobs with blood.
+ if(!isliving(target))
+ return FALSE
+
+ var/mob/living/target_mob = target
+ var/turf/center = get_turf(target_mob)
+ if(!center)
+ return FALSE
+
+ // Harvest loop: We try to gather as much as possible from nearby sources, one at a time, until we meet the quota.
+ var/harvested = harvest_blood(center, harvest_goal, target_mob, null)
+
+ // What a shitty blood drive.
+ if(harvested <= 0 && target_mob.blood_volume <= BLOOD_VOLUME_NORMAL + 10) // we do +10 just to make sure we have something to work with
+ user.balloon_alert(user, "no blood nearby!")
+ return FALSE
+
+ target_mob.blood_volume += harvested
+
+ // We set the healing ratio and attempt to heal the target.
+ var/ratio = healing_ratio + (isnum(affinity) ? max(affinity - required_affinity, 0) * affinity_healing_ratio_bonus : 0)
+ if(ratio > 0)
+ var/excess_blood = max(target_mob.blood_volume - BLOOD_VOLUME_NORMAL, 0)
+ if(excess_blood > 0 && iscarbon(target_mob))
+ var/mob/living/carbon/target_carbon = target_mob
+ var/total_brute = 0
+ var/total_burn = 0
+ // Gets all the damage across various bodyparts.
+ for(var/obj/item/bodypart/part as anything in target_carbon.bodyparts)
+ if(!(part.bodytype & BODYTYPE_ORGANIC))
+ continue
+ total_brute += part.brute_dam
+ total_burn += part.burn_dam
+ var/total_damage = total_brute + total_burn
+
+ // Based on the total damage, we heal based on the excess blood compared to the normal blood volume.
+ if(total_damage > 0)
+ var/heal_capacity = excess_blood * ratio // max we can heal
+ var/heal_amount = min(heal_capacity, total_damage) // how much we will heal total
+ var/heal_brute = total_damage ? (heal_amount * (total_brute / total_damage)) : 0 // we try to heal all brute damage first
+ var/heal_burn = heal_amount - heal_brute // then we heal burn damage
+ var/actual_healed = target_carbon.heal_overall_damage(brute = heal_brute, burn = heal_burn, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC)
+ // update the blood in the target based on the healing used.
+ if(actual_healed > 0)
+ var/blood_used = min(excess_blood, actual_healed / ratio)
+ target_carbon.blood_volume = max(target_carbon.blood_volume - blood_used, BLOOD_VOLUME_NORMAL)
+ target_carbon.updatehealth()
+ target.visible_message(span_notice("Blood flows into [target]'s body, reinvigorating them!"), span_notice("You feel energized as the blood mends your body!"))
+ playsound(target, 'sound/effects/splat.ogg', 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+
+/// Do you have BLOOD; as in the real deal.
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/is_valid_blood_target(mob/living/target_mob)
+ if(!target_mob)
+ return FALSE
+ return target_mob.get_blood_reagent() == /datum/reagent/blood
+
+/// Ends the cast if we are dispelled during it.
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/on_cast_dispel(datum/source, atom/dispeller)
+ cast_interrupted_by_dispel = TRUE
+ to_chat(owner, span_warning("Your [name] is dispelled!"))
+
+/// Checks if there's any valid blood sources in the area.
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/get_valid_blood_sources(turf/center, mob/living/target_mob, obj/item/reagent_containers/ignore_container)
+ if(!center)
+ return list()
+
+ var/list/sources = list()
+
+ for(var/obj/effect/decal/cleanable/blood/blood_decal in range(1, center))
+ if(blood_decal.dried || blood_decal.bloodiness <= 0)
+ continue
+ if(!(blood_decal.decal_reagent == /datum/reagent/blood || blood_decal.reagents?.has_reagent(/datum/reagent/blood)))
+ continue
+ sources += blood_decal
+
+ for(var/obj/item/reagent_containers/container in range(1, center))
+ if(ignore_container && container == ignore_container)
+ continue
+ if(!container.reagents)
+ continue
+ if(container.reagents.get_reagent_amount(/datum/reagent/blood) <= 0)
+ continue
+ sources += container
+
+ for(var/mob/living/other in range(1, center))
+ if(other == target_mob)
+ continue
+ if(other.get_blood_reagent() != /datum/reagent/blood)
+ continue
+ if(other.blood_volume <= 0)
+ continue
+ sources += other
+
+ return sources
+
+/// Checks if the mob actually contains proper useable blood
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/has_valid_blood_sources(turf/center, mob/living/target_mob, obj/item/reagent_containers/ignore_container)
+ return length(get_valid_blood_sources(center, target_mob, ignore_container)) > 0
+
+/// Attempts to do a blood drive on decals, containers and mobs in descending order.
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/harvest_blood(turf/center, amount_needed, mob/living/target_mob, obj/item/reagent_containers/ignore_container)
+ if(amount_needed <= 0 || !center)
+ return 0
+
+ var/harvested = 0
+ harvested += harvest_blood_from_decals(center, amount_needed - harvested)
+ if(harvested < amount_needed)
+ harvested += harvest_blood_from_containers(center, amount_needed - harvested, ignore_container)
+ if(harvested < amount_needed)
+ harvested += harvest_blood_from_mobs(center, amount_needed - harvested, target_mob)
+
+ return harvested
+
+/// Attempts to harvest decals.
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/harvest_blood_from_decals(turf/center, amount_needed)
+ if(amount_needed <= 0 || !center)
+ return 0
+
+ var/harvested = 0
+ for(var/obj/effect/decal/cleanable/blood/blood_decal in range(1, center))
+ if(harvested >= amount_needed)
+ break
+ if(blood_decal.dried || blood_decal.bloodiness <= 0)
+ continue
+ if(!(blood_decal.decal_reagent == /datum/reagent/blood || blood_decal.reagents?.has_reagent(/datum/reagent/blood)))
+ continue
+
+ var/available_units = blood_decal.bloodiness * BLOOD_TO_UNITS_MULTIPLIER
+ if(available_units <= 0)
+ continue
+
+ var/to_take = min(amount_needed - harvested, available_units)
+ if(to_take >= available_units) // if we would take the max amount, we destroy hte decal in the process.
+ if(blood_decal.reagents)
+ blood_decal.reagents.remove_reagent(/datum/reagent/blood, to_take, include_subtypes = TRUE)
+ qdel(blood_decal)
+ else // otherwise, we take away the reagent.
+ var/bloodiness_to_remove = to_take / BLOOD_TO_UNITS_MULTIPLIER
+ blood_decal.adjust_bloodiness(-bloodiness_to_remove)
+ if(blood_decal.reagents)
+ blood_decal.reagents.remove_reagent(/datum/reagent/blood, to_take, include_subtypes = TRUE)
+ harvested += to_take
+
+ return harvested
+
+/// Attempts to harvest containers
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/harvest_blood_from_containers(turf/center, amount_needed, obj/item/reagent_containers/ignore_container)
+ if(amount_needed <= 0 || !center)
+ return 0
+
+ var/harvested = 0
+ for(var/obj/item/reagent_containers/container in range(1, center))
+ if(harvested >= amount_needed)
+ break
+ if(ignore_container && container == ignore_container)
+ continue
+ if(!isturf(container.loc) || !container.reagents)
+ continue
+ var/available_units = container.reagents.get_reagent_amount(/datum/reagent/blood)
+ if(available_units <= 0)
+ continue
+ var/to_take = min(amount_needed - harvested, available_units)
+ container.reagents.remove_reagent(/datum/reagent/blood, to_take, include_subtypes = TRUE)
+ harvested += to_take
+
+ return harvested
+
+/// Attempts to harvest mobs.
+/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/harvest_blood_from_mobs(turf/center, amount_needed, mob/living/target_mob)
+ if(amount_needed <= 0 || !center)
+ return 0
+
+ var/harvested = 0
+ for(var/mob/living/other in range(1, center))
+ if(harvested >= amount_needed)
+ break
+ if(other == target_mob)
+ continue
+ if(other.can_block_resonance(1)) // Doesn't work if you're immune to resonance magic.
+ continue
+ if(other.get_blood_reagent() != /datum/reagent/blood)
+ continue
+ if(other.blood_volume <= 0)
+ continue
+
+ var/to_take = min(amount_needed - harvested, other.blood_volume)
+ if(to_take <= 0)
+ continue
+ to_chat(other, span_userdanger("Blood is drawn from your body by [owner]!"))
+ other.blood_volume = max(other.blood_volume - to_take, 0)
+ harvested += to_take
+
+ return harvested
+
+
+// The visual effect of the cast
+/obj/effect/temp_visual/sanguine_absorption
+ name = "blood bubble"
+ icon = 'icons/obj/weapons/guns/projectiles.dmi'
+ icon_state = "mini_leaper"
+ layer = ABOVE_MOB_LAYER
+ duration = 3 SECONDS
+ alpha = 200
+
+
+/obj/effect/temp_visual/sanguine_absorption_target
+ icon_state = "blessed"
+ color = "#da2424"
+ duration = 3 SECONDS
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/spell_preparation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/spell_preparation.dm
new file mode 100644
index 00000000000000..d3709d0d16be8f
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/spell_preparation.dm
@@ -0,0 +1,49 @@
+/datum/power/thaumaturge_root/spell_preparation
+ name = "Spell Preparation"
+ desc = "You cast spells by focusing on your thaumaturic magic throughout your sleep. Your spells have a limited amount of charges based upon the total amount of points in the Thaumaturgy path. You can allocate up to 6 charges to these spells\
+ by using a special action, depending on each spell's power cost. You can change which spells you have prepared and recharge them by sleeping with your Spell Focus. \
+ \nGrants you a Spell Focus, an unique item that allows you to charge your Thaumaturge spells while sleeping, and enhance them by holding it. Use the Spell Focus in your hand to change it's form."
+ security_record_text = "Subject is capable of performing feats of thaumaturgic magic while in possession of a spell focus."
+ action_path = /datum/action/cooldown/power/thaumaturge/spell_preparation
+
+ value = 0
+
+/datum/power/thaumaturge_root/spell_preparation/add_unique(client/client_source)
+ var/obj/item/spell_focus/spell_focus = new(get_turf(power_holder))
+ give_item_to_holder(spell_focus, list(LOCATION_BACKPACK, LOCATION_HANDS))
+
+/datum/power/thaumaturge_root/spell_preparation/post_add()
+ if(!power_holder) // So it doesn't runtime at init
+ return
+ // Spell preperation is so complicated we basically handle it all in a component, including the UI part.
+ power_holder.AddComponent(/datum/component/thaumaturge/preparation, power_holder)
+ . = ..()
+
+/datum/power/thaumaturge_root/spell_preparation/remove()
+ . = ..()
+ if(!power_holder)
+ return
+ var/tobedel = power_holder.GetComponent(/datum/component/thaumaturge/preparation)
+ QDEL_NULL(tobedel)
+
+
+/datum/action/cooldown/power/thaumaturge/spell_preparation
+ name = "Spell Preparation"
+ desc = "Adjust the amount of charges your spells have! Requires sleeping with a Spell Focus on your person to apply (except the first time in a round)."
+ button_icon = 'icons/obj/service/library.dmi'
+ button_icon_state = "bookcharge"
+
+ // Makes it not interact with the charges system.
+ max_charges = null
+ // Lets you tweak it while you sleep.
+ disabled_by_incapacitate = FALSE
+
+/datum/action/cooldown/power/thaumaturge/spell_preparation/use_action(mob/living/user, atom/target)
+ var/datum/component/thaumaturge/preparation/prep_component = user.GetComponent(/datum/component/thaumaturge/preparation)
+ if(!prep_component)
+ to_chat(user, span_warning("Something terrible has happened; you're missing your preparation component. Yell at devs!"))
+ return FALSE
+ prep_component.build_spells() // We call it here so all the spells are loaded when we open it.
+ prep_component.ui_interact(user)
+ return TRUE
+
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm
new file mode 100644
index 00000000000000..445c358351c0a5
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm
@@ -0,0 +1,112 @@
+// Does a lot of things to plants. Makes em grow, makes em produce, makes em healthy.
+// This applies to A LOT of plants. Interpet as you will.
+
+/datum/power/thaumaturge/vitalize_flora
+ name = "Vitalize Flora"
+ desc = "Breathes life into the plants around you. This heals any and all plants (including plant creatures), makes them grow if they're still in the growth phase, and speeds up the time until the next harvest. \
+ \nRequires Affinity 1. Affinity gives a chance to not consume charges."
+ security_record_text = "Subject can magically heal and grow plantlife around it."
+ value = 2
+
+ action_path = /datum/action/cooldown/power/thaumaturge/vitalize_flora
+ required_powers = list(/datum/power/thaumaturge_root)
+ required_allow_subtypes = TRUE
+
+/datum/action/cooldown/power/thaumaturge/vitalize_flora
+ name = "Vitalize Flora"
+ desc = "Breathes life into the plants around you. This heals any and all plants (including plant creatures), makes them grow if they're still in the growth phase, and speeds up the time until the next harvest."
+ button_icon = 'icons/obj/fluff/flora/plants.dmi'
+ button_icon_state = "plant-03"
+
+ required_affinity = 1
+ prep_cost = 2
+ power_refunds = TRUE
+ power_refund_chance = THAUMATURGE_REFUND_MULT_BASE
+ power_refund_affinity_bonus = THAUMATURGE_REFUND_MULT_AFFINITY
+
+ /// the amount to heal mob plants by
+ var/mob_heal_amount = 15
+ /// the amount to heal non-mob plants by
+ var/obj_heal_amount = 10
+ /// How many seconds to make it grow by.
+ var/grow_amount = (15 SECONDS) / (HYDROTRAY_CYCLE_DELAY)
+
+/datum/action/cooldown/power/thaumaturge/vitalize_flora/use_action(mob/living/user, atom/target)
+ var/turf/user_turf = get_turf(user)
+ if(!user_turf)
+ return FALSE
+
+ // affected anything at all
+ var/affected_anything = FALSE
+ // affected something this cycle
+ var/affected_anything_this_cycle = FALSE
+ // nearby vine to propagate from (if any)
+ var/obj/structure/spacevine/nearby_vine = locate(/obj/structure/spacevine) in range(1, user_turf)
+ var/datum/spacevine_controller/vine_master = nearby_vine?.master
+
+ // Get everyhing in a 3x3 area
+ for(var/turf/area_turf in range(1, user_turf))
+ affected_anything_this_cycle = FALSE
+ // If hydro tray: Heals the plant inside it.
+ for(var/obj/machinery/hydroponics/hydro_tray in area_turf)
+ if(!hydro_tray.myseed || hydro_tray.plant_status == HYDROTRAY_PLANT_DEAD)
+ continue
+ // heals the plant
+ hydro_tray.adjust_plant_health(obj_heal_amount)
+ // if its not fully aged yet; make it age.
+ if(hydro_tray.age < hydro_tray.myseed.maturation)
+ hydro_tray.age += grow_amount
+ hydro_tray.lastproduce = hydro_tray.age
+ // if it is mature, advance progress toward next harvest
+ else
+ hydro_tray.lastproduce = max(hydro_tray.lastproduce - grow_amount, 0)
+ hydro_tray.update_appearance()
+ affected_anything = TRUE
+ affected_anything_this_cycle = TRUE
+
+ // As above, but instead of hydotray its other flora objects.
+ for(var/obj/structure/flora/area_flora in area_turf)
+ if(area_flora.get_integrity() < area_flora.max_integrity)
+ area_flora.repair_damage(obj_heal_amount)
+ if(area_flora.harvested && prob(30)) // Because of how area flora is coded this is best we can do to speed it up in a way that isn't always success.
+ area_flora.regrow()
+ affected_anything = TRUE
+ affected_anything_this_cycle = TRUE
+
+ // Kudzu growth (spacevines) around the caster, only if vines are nearby
+ if(vine_master && !isspaceturf(area_turf) && !locate(/obj/structure/spacevine) in area_turf)
+ vine_master.spawn_spacevine_piece(area_turf, nearby_vine, list())
+ affected_anything = TRUE
+ affected_anything_this_cycle = TRUE
+
+ // Heals plant mobs in the area for either burn, brute or tox. Also does things to turtles.
+ for(var/mob/living/area_mob in area_turf)
+ if(!(area_mob.mob_biotypes & MOB_PLANT))
+ continue
+ // prevents charge consumption for platn creatures when theyre at full hp
+ if(area_mob.health >= area_mob.maxHealth)
+ continue
+ // heals plant creatures
+ area_mob.heal_ordered_damage(mob_heal_amount, list(BRUTE, BURN, TOX))
+ // so there's these cute turtles that can grow plants on themselves and clearly we should be able to grow that too.
+ if(istype(area_mob, /mob/living/basic/turtle))
+ var/mob/living/basic/turtle/plant_turtle = area_mob
+ plant_turtle.set_plant_growth(plant_turtle.retrieve_destined_path(), grow_amount)
+ affected_anything = TRUE
+ affected_anything_this_cycle = TRUE
+
+ //glowy particles to tell people somethings happening on that space.
+ if(affected_anything_this_cycle)
+ new /obj/effect/temp_visual/plant_growth(area_turf)
+
+ if(!affected_anything)
+ user.balloon_alert(user, "no valid targets in range!")
+ return FALSE
+ playsound(user, 'sound/effects/magic/charge.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
+ return TRUE
+
+// visual effect for plant growth
+/obj/effect/temp_visual/plant_growth
+ icon_state = "blessed"
+ color = "#24da3c"
+ duration = 1 SECONDS
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_action.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_action.dm
new file mode 100644
index 00000000000000..76b311a3abae12
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_action.dm
@@ -0,0 +1,58 @@
+/datum/action/cooldown/power/theologist
+ name = "abstract theologist power action - ahelp this"
+ background_icon_state = "bg_clock"
+ overlay_icon_state = "bg_clock_border"
+ button_icon = 'icons/mob/actions/backgrounds.dmi'
+
+ /// The component that handles most piety components.
+ var/datum/component/theologist_piety/piety_component
+
+ /// Reference to the theologist UI component
+ var/atom/movable/screen/theologist_piety/theologist_ui
+
+ /// Cost in Piety to use.
+ var/cost
+
+/datum/action/cooldown/power/theologist/Grant(mob/grant_to)
+ . = ..()
+ ValidatePietyComponent()
+ return .
+
+/// Since Theologist has both 3 roots and a persistent resource system, we use a component for handling Piety
+/datum/action/cooldown/power/theologist/proc/ValidatePietyComponent()
+ if(owner) // Prevents runtiming on start
+ var/mob/living/carrier = owner
+ piety_component = carrier.GetComponent(/datum/component/theologist_piety)
+ if(!piety_component)
+ return FALSE
+ return TRUE
+
+/// Validation handled in the piety component.
+/datum/action/cooldown/power/theologist/proc/adjust_piety(amount, override_cap)
+ piety_component.adjust_piety(amount, override_cap)
+
+/// Gets the current piety of the mob.
+/datum/action/cooldown/power/theologist/proc/get_piety()
+ return piety_component.piety
+
+// We check to see if our piety component is actually there, because usually things will go bad if they don't.
+/datum/action/cooldown/power/theologist/try_use(mob/living/user, mob/living/target)
+ if(!ValidatePietyComponent())
+ owner.balloon_alert(owner, "Yell at the coders; you're missing your piety system!")
+ return FALSE
+ if(piety_component.piety < cost)
+ user.balloon_alert(user, "needs [cost] piety!")
+ return FALSE
+ . = .. ()
+
+// Make sure the cost gets deducted after using the power (we already checked if we can afford it)
+/datum/action/cooldown/power/theologist/on_action_success(mob/living/user, atom/target)
+ if(cost)
+ adjust_piety(-cost)
+ return
+
+/// Applies tox changes whilst respecting toxinlover as a trait
+/datum/action/cooldown/power/theologist/proc/adjust_tox_noinvert(mob/living/target, amount, updating_health = TRUE, required_biotype = ALL)
+ if(HAS_TRAIT(target, TRAIT_TOXINLOVER))
+ amount = -amount
+ return target.adjustToxLoss(amount, updating_health = updating_health, forced = FALSE, required_biotype = required_biotype)
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_piety.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_piety.dm
new file mode 100644
index 00000000000000..c02b595f9e58d7
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_piety.dm
@@ -0,0 +1,98 @@
+/// Helper to format the text that gets thrown onto the piety hud element.
+#define FORMAT_PIETY_TEXT(charges) MAPTEXT("
[round(charges)]
")
+
+/datum/component/theologist_piety
+ dupe_mode = COMPONENT_DUPE_UNIQUE
+
+ /// The mob we’re attached to is always `parent`.
+ var/mob/living/attached_mob
+
+ /// current piety
+ var/piety = 0
+ /// max piety
+ var/max_piety = THEOLOGIST_PIETY_MAX
+
+ /// The UI itself
+ var/atom/movable/screen/theologist_piety/theologist_ui
+
+/datum/component/theologist_piety/Initialize()
+ . = ..()
+ if(!isliving(parent))
+ return COMPONENT_INCOMPATIBLE
+ attached_mob = parent
+
+ // Clearly the Chaplain is VERY pious.
+ if(is_chaplain_job(attached_mob.mind?.assigned_role))
+ max_piety *= 2
+
+ RegisterWithParent()
+
+/datum/component/theologist_piety/RegisterWithParent()
+ . = ..()
+ if(attached_mob.hud_used)
+ install_piety_hud(parent)
+ else
+ RegisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created))
+
+/datum/component/theologist_piety/UnregisterFromParent()
+ // UnregisterSignal(attached_mob, list(COMSIG_..., COMSIG_...))
+ . = ..()
+ if(attached_mob) // prevents runtiming when adding/removing duplicate components
+ UnregisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED)
+
+/datum/component/theologist_piety/Destroy()
+ UnregisterFromParent()
+
+ if(!attached_mob)
+ return
+
+ if(attached_mob.hud_used && theologist_ui)
+ attached_mob.hud_used.infodisplay -= theologist_ui
+ qdel(theologist_ui)
+ theologist_ui = null
+
+ attached_mob = null
+ return ..()
+
+/// Signal handler when the base hud has finished initializing.
+/datum/component/theologist_piety/proc/on_hud_created(datum/source)
+ SIGNAL_HANDLER
+
+ var/mob/living/living_holder = attached_mob
+ if(!living_holder || !living_holder.hud_used)
+ return
+
+ install_piety_hud(living_holder)
+
+/// Applies the piety hud to the mob's UI.
+/datum/component/theologist_piety/proc/install_piety_hud(mob/living/living_holder)
+ if(theologist_ui) // already installed
+ return
+
+ var/datum/hud/hud_used = living_holder.hud_used
+ theologist_ui = new /atom/movable/screen/theologist_piety(null, hud_used)
+ // If the cultivator energy UI is present, use the alternate screen loc to avoid overlap.
+ if(living_holder.GetComponent(/datum/component/cultivator_energy))
+ theologist_ui.screen_loc = THEOLOGIST_ALT_UI_SCREEN_LOC
+ hud_used.infodisplay += theologist_ui
+
+ // Set initial text so it isn't blank until first adjust.
+ theologist_ui.maptext = FORMAT_PIETY_TEXT(piety)
+
+ hud_used.show_hud(hud_used.hud_version)
+
+/// Handler for adjusting piety.
+/datum/component/theologist_piety/proc/adjust_piety(amount, override_cap)
+ if(!isnum(amount))
+ return
+ var/cap_to = isnum(override_cap) ? override_cap : max_piety
+ piety = clamp(piety + amount, 0, cap_to)
+
+ theologist_ui?.maptext = FORMAT_PIETY_TEXT(piety)
+
+// UI Elements for Piety
+/atom/movable/screen/theologist_piety
+ name = "piety"
+ icon = 'icons/hud/blob.dmi' // TODO: Get sprites/UI for this.
+ icon_state = "block"
+ screen_loc = THEOLOGIST_UI_SCREEN_LOC
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_power.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_power.dm
new file mode 100644
index 00000000000000..e2a00c4c07ad3d
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_power.dm
@@ -0,0 +1,9 @@
+/datum/power/theologist
+ name = "Theologist Power"
+ desc = "We used to call it spells but lets be real here we ditched the spell code for our own snowflake action-code a while ago. The spells system hasn't called in years; its time to let her go. \
+ Also tell a developer you're seeing this when you read this; this isn't meant for your eyes. Shoo!"
+
+ archetype = POWER_ARCHETYPE_SORCEROUS
+ path = POWER_PATH_THEOLOGIST
+ priority = POWER_PRIORITY_BASIC
+ abstract_parent_type = /datum/power/theologist
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm
new file mode 100644
index 00000000000000..d7ec038114240a
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm
@@ -0,0 +1,34 @@
+/datum/power/theologist_root
+ name = "Abstract theologist root"
+ desc = "Some spend most of their life looking for the holy grail, the root of life, yggdrasil and all those things. This is the root. Of the healer powers. So you're getting close? \
+ Present this to the developers for the next hint in your quest. Because you're not actually meant to have this."
+ abstract_parent_type = /datum/power/theologist_root
+
+
+ archetype = POWER_ARCHETYPE_SORCEROUS
+ path = POWER_PATH_THEOLOGIST
+ priority = POWER_PRIORITY_ROOT
+
+/datum/power/theologist_root/post_add() // I'd love to run this during add but that runtimes at round start.
+ if(!power_holder) // So it doesn't runtime at init
+ return
+ // We pass along the piety component to actually handle most of the piety stuff.
+ power_holder.AddComponent(/datum/component/theologist_piety, power_holder)
+ . = ..()
+
+/datum/power/theologist_root/remove()
+ . = ..()
+ var/mob/living/holder = power_holder
+ if(!holder)
+ return
+
+ // We check for other roots of our type, in the event that admin shenanigangs gave multiple roots. Don't want to throw out the whole component when other things are still in use.
+ var/has_other_root = FALSE
+ for(var/datum/power/power as anything in holder.powers)
+ if(istype(power, /datum/power/theologist_root))
+ has_other_root = TRUE
+ break
+
+ if(!has_other_root)
+ var/tobedel = holder.GetComponent(/datum/component/theologist_piety)
+ QDEL_NULL(tobedel)
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm
new file mode 100644
index 00000000000000..fd08d0a21de211
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm
@@ -0,0 +1,160 @@
+
+/datum/power/theologist_root/revered
+ name = "A Burden Revered"
+ desc = "Nullifies pain and slowly heals the targeted creature's burn and brute damage over a prolonged period of time. This may be yourself. \
+ \nGrants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again. Does not work on synthetic bodyparts."
+ security_record_text = "Subject can magically mend their own wounds and the wounds of others slowly over a long duration."
+ security_threat = POWER_THREAT_MAJOR
+ action_path = /datum/action/cooldown/power/theologist/theologist_root/revered
+
+ value = 5
+
+/datum/action/cooldown/power/theologist/theologist_root/revered
+ name = "A Burden Revered"
+ desc = "Nullifies pain and slowly heals the targeted creature's burn and brute damage over a prolonged period of time. This may be yourself. \
+ Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again. Does not work on synthetic bodyparts."
+ button_icon = 'modular_doppler/modular_powers/icons/powers/actions_icons.dmi'
+ button_icon_state = "burden_revered" // I need something better
+ cooldown_time = 50
+ target_range = 1
+ target_type = /mob/living
+ click_to_activate = TRUE
+ target_self = TRUE
+
+ /// Current instance of the status effect
+ var/datum/status_effect/power/burden_revered/active_effect
+
+ /// Keeps track if we are targeting ourselves, as to ensure we don't give ourselves piety by repeatedly healing ourselves, which isn't very pious (according to MOST religions).
+ var/healing_self = FALSE
+ /// The maximum amount we will heal.
+ var/healing_max = THEOLOGIST_ROOT_HEALING
+ /// The amount we heal per tick
+ var/healing_amount = 1
+
+/datum/action/cooldown/power/theologist/theologist_root/revered/use_action(mob/living/user, mob/living/target)
+ if(active_effect)
+ qdel(active_effect)
+ active_effect = target.apply_status_effect(/datum/status_effect/power/burden_revered, src)
+ active = TRUE
+ if(active_effect && target == owner)
+ healing_self = TRUE
+ playsound(target, 'sound/effects/magic/staff_healing.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ to_chat(target, span_notice("[user] lays [user.p_their()] hand on you, and your wounds start to heal!"))
+ to_chat(user, span_notice("You lay your hand on [target]'s shoulder, revering their burdens."))
+ return TRUE
+
+/// Callback communication from the status effect on expiration that handles piety gain and feedbackk.
+/datum/action/cooldown/power/theologist/theologist_root/revered/proc/effect_expired(mob/living/target, amount)
+ if(target.ckey) // Don't get piety from healing nobodies.
+ if(amount >= 1 && !healing_self)
+ adjust_piety(amount)
+ to_chat(owner, span_notice("Your previous Burden Revered has expired! You gained [amount] piety!"))
+ else
+ to_chat(owner, span_notice("Your previous Burden Revered has expired!"))
+ else
+ to_chat(owner, span_notice("Your previous Burden Revered has expired!"))
+ owner.playsound_local(owner, 'sound/effects/magic/charge.ogg', 50, FALSE)
+
+ //Always reset this after use.
+ active = FALSE
+ healing_self = FALSE
+
+ return
+
+// Status effect that Burden Revered applies
+/datum/status_effect/power/burden_revered
+ id = "burden_revered"
+ duration = 2 MINUTES // If somehow it overestays its welcome
+ tick_interval = 1 SECONDS
+ alert_type = /atom/movable/screen/alert/status_effect/burden_revered
+ /// The power responsible for this, so we can make sure it properly gives piety to the caster
+ var/datum/action/cooldown/power/theologist/theologist_root/revered/burden_power
+ /// The maximum amount we will heal
+ var/healing_max = THEOLOGIST_ROOT_HEALING
+ /// How much we have healed already
+ var/healing_done = 0
+ /// How much we heal per tick.
+ var/base_healing_amount = 1
+ /// Has the thing already expired?
+ var/already_expired
+
+/datum/status_effect/power/burden_revered/on_apply()
+ ADD_TRAIT(owner, TRAIT_ANALGESIA, type)
+ RegisterSignal(owner, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+ return TRUE
+
+// Sets the link with the original action
+/datum/status_effect/power/burden_revered/on_creation(mob/living/new_owner, datum/action/cooldown/power/theologist/theologist_root/revered/passed_power)
+ . = ..()
+ burden_power = passed_power
+ if(burden_power) // inherit the healing from the power, for potential future upgrades / varedits.
+ healing_max = burden_power.healing_max
+ base_healing_amount = burden_power.healing_amount
+
+
+// You might wonder why we run Destroy as well as on_remove. The issue is that on_remove can trigger on qdel, which invalidates burden_power, which prevents us from efficiently passing on the piety back to the owner.
+/datum/status_effect/power/burden_revered/Destroy()
+ if(!already_expired)
+ expire()
+ ..()
+
+/datum/status_effect/power/burden_revered/on_remove()
+ UnregisterSignal(owner, COMSIG_ATOM_DISPEL)
+ REMOVE_TRAIT(owner, TRAIT_ANALGESIA, type)
+ return
+
+/// Dispel functionality
+/datum/status_effect/power/burden_revered/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+ to_chat(owner, span_userdanger("Your [burden_power.name] deactives prematurely!"))
+ if(!owner == burden_power.owner)
+ to_chat(burden_power.owner, span_warning("Your [burden_power.name] has been dispelled!"))
+ burden_power.StartCooldownSelf()
+ expire()
+ return DISPEL_RESULT_DISPELLED
+
+
+// This is where the heal budgeting happens.
+/datum/status_effect/power/burden_revered/tick(seconds_between_ticks)
+ var/healing_amount = (base_healing_amount * seconds_between_ticks)
+ new /obj/effect/temp_visual/heal(get_turf(owner), "#ddd166")
+
+ // Expire if we've reached the max.
+ if(healing_done >= healing_max)
+ expire()
+ return
+
+ // Limb-based healing: only organic bodyparts.
+ if(!istype(owner, /mob/living/carbon))
+ expire()
+ return
+
+ var/mob/living/carbon/mob = owner
+ var/healed_any = FALSE
+ // gets random bodypart, heals it, bam.
+ for(var/obj/item/bodypart/bodypart in mob.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC))
+ bodypart.heal_damage(healing_amount, healing_amount, required_bodytype = BODYTYPE_ORGANIC)
+ mob.update_damage_overlays()
+ healing_done += healing_amount
+ healed_any = TRUE
+ break
+
+ // Expire if there's nothing left to heal.
+ if(!healed_any)
+ expire()
+ return
+
+/// QDEL destroys burden_power so we can handle this b4 destroy. Passes piety back.
+/datum/status_effect/power/burden_revered/proc/expire()
+ var/piety_gained = max(0, floor(healing_done * THEOLOGIST_PIETY_HEALING_COEFFICIENT))
+ // Report back BEFORE deletion starts
+ if(burden_power)
+ burden_power.effect_expired(owner, piety_gained)
+ already_expired = TRUE
+ src.Destroy() // There might be something better, but QDEL triggers the qdel loop warning.
+
+/atom/movable/screen/alert/status_effect/burden_revered
+ name = "A Burden Revered"
+ desc = "You passively heal damage, and are immune to pain for it's duration."
+ icon = 'modular_doppler/modular_powers/icons/powers/actions_icons.dmi'
+ icon_state = "burden_revered"
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm
new file mode 100644
index 00000000000000..c202d301ddfc59
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm
@@ -0,0 +1,334 @@
+/datum/power/theologist_root/shared
+ name = "A Burden Shared"
+ desc = "Channels a beam of energy between you and a target, equalizing damage over a period of time, scaling with severity. The beam requires continous line of sight to function, and neither you or your target can be incapacitated.\
+ \nGenerates Piety if you are transfering damage to yourself. Works on synthetic bodyparts"
+ security_record_text = "Subject can transfer the injuries of a target onto themselves, or visa versa."
+ security_threat = POWER_THREAT_MAJOR
+ action_path = /datum/action/cooldown/power/theologist/theologist_root/shared
+
+ value = 5
+
+/datum/action/cooldown/power/theologist/theologist_root/shared
+ name = "A Burden Shared"
+ desc = "Channels a beam of energy between you and a target, equalizing damage over a period of time, scaling with severity. \
+ The beam requires continous line of sight to function, and neither you or your target can be incapacitated. Generates Piety if you are transfering damage to yourself. Works on synthetic bodyparts"
+ button_icon = 'icons/mob/actions/actions_spells.dmi'
+ button_icon_state = "swap"
+ cooldown_time = 150
+ click_to_activate = TRUE
+
+ target_range = 10 // 2 space beyond screen width if purely vertical/horizontal
+ target_type = /mob/living
+ target_self = FALSE
+ unset_after_click = TRUE
+
+ /// The piety build-up. Gets exchanged at exchange_build() if its either positive or negative.
+ var/piety_buildup
+
+ /// Who we're currently linked to.
+ var/mob/living/carbon/current_target
+
+ /// Visual beam datum we keep alive while the link is active.
+ var/datum/beam/current_beam
+
+ // Visual for the glow on the target
+ var/mutable_appearance/target_glow
+
+ /// How often (in deciseconds) we validate LoS + apply the equalization tick.
+ var/check_delay = 10
+ var/last_check = 0
+
+ /// Current instance of the status effect
+ var/datum/status_effect/power/burden_revered/active_effect
+
+ /// healing values
+ /// How much we divide HP by to determine our healing
+ var/heal_division_factor = 20
+ /// How much we heal at the minimum per tick
+ var/heal_min = 0.5
+ /// How much we heal at the maximum per tick
+ var/heal_max = 3
+
+/datum/action/cooldown/power/theologist/theologist_root/shared/Destroy()
+ clear_link(manual = TRUE)
+ return ..()
+
+// We override trigger to be able to cancel the ability on clicking the button
+/datum/action/cooldown/power/theologist/theologist_root/shared/Trigger(mob/clicker, trigger_flags, atom/target)
+ // If we're already actively beaming, pressing the button again should cancel immediately.
+ if(current_target)
+ clear_link(manual = TRUE)
+ // Also ensure click-intercept is not left enabled.
+ unset_click_ability(owner, refund_cooldown = FALSE)
+ return FALSE
+
+ . = ..()
+
+/// Currency exchange for piety.
+/datum/action/cooldown/power/theologist/theologist_root/shared/proc/exchange_buildup()
+ // Have we been a good boy?
+ if(piety_buildup >= 1)
+ piety_buildup -= 1
+ adjust_piety(THEOLOGIST_PIETY_TRIVIAL)
+ to_chat(owner, span_notice("Taking on the burdens of others has gained you piety!"))
+ // Have we been a bad boy?
+ else if (piety_buildup <= -1)
+ piety_buildup += 1
+ // Have we been a VERY bad boy? Don't think you can get away with willynilly using this at 0 piety.
+ if(get_piety() <= 0 && prob(25))
+ lightningbolt(owner)
+ if(ishuman(owner))
+ var/mob/living/carbon/human/sinner = owner
+ sinner.Paralyze(100)
+ to_chat(owner, span_userdanger("You have been punished for your lack of piety!"), confidential = TRUE)
+ clear_link()
+ return
+ adjust_piety(-THEOLOGIST_PIETY_TRIVIAL)
+ to_chat(owner, span_warning("The transfer of your burdens onto others lost you piety!"))
+
+
+/**
+ * Always-called cleanup. Use manual = TRUE when the user actively cancels the power.
+ */
+/datum/action/cooldown/power/theologist/theologist_root/shared/proc/clear_link(manual = FALSE)
+ // gets rid of the dispel signaler
+ UnregisterSignal(current_target, COMSIG_ATOM_DISPEL)
+ UnregisterSignal(owner, COMSIG_ATOM_DISPEL)
+ // gets rid of the beam
+ if(current_beam)
+ UnregisterSignal(current_beam, COMSIG_QDELETING)
+ if(!QDELETED(current_beam)) // prevents a qdel loop because clear_link from walking away also deletes it
+ QDEL_NULL(current_beam)
+ else
+ current_beam = null
+ // gets rid of the target's glow
+ if(target_glow)
+ current_target.cut_overlay(target_glow)
+ target_glow = null
+ // unflags active and tells the caster that the link :b:roke
+ if(active)
+ active = FALSE
+ if(!manual && owner && isliving(owner))
+ owner.balloon_alert(owner, "link broken!")
+ // gets rid of the warning status message
+ if(active_effect)
+ qdel(active_effect)
+
+ current_target = null
+ if(manual)
+ unset_click_ability(owner, refund_cooldown = FALSE)
+
+/**
+ * Called when the beam is deleted by something external (range/los/cleanup, etc).
+ */
+/datum/action/cooldown/power/theologist/theologist_root/shared/proc/beam_died()
+ SIGNAL_HANDLER
+ current_beam = null
+ clear_link()
+
+/**
+ * Called when the target or the caster is dispelled
+ */
+/datum/action/cooldown/power/theologist/theologist_root/shared/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+ if(!active)
+ return NONE
+ to_chat(owner, span_userdanger("Your burdens are no longer shared!"))
+ to_chat(current_target, span_userdanger("Your burdens are no longer shared!"))
+ clear_link()
+ StartCooldownSelf() // Just so you don't immediately reapply it.
+ return DISPEL_RESULT_DISPELLED
+
+/**
+ * Starts (or re-targets) the link between the user and a clicked target.
+ * Returning TRUE means: the power was used successfully and should start cooldown (and unset targeting mode).
+ */
+/datum/action/cooldown/power/theologist/theologist_root/shared/use_action(mob/living/carbon/user, atom/target)
+ var/mob/living/new_target = target
+
+ // If already active, cleanly drop the existing link before re-targeting.
+ if(active)
+ clear_link(manual = TRUE)
+
+ current_target = new_target
+ last_check = 0
+ active = TRUE
+
+ // Create a beam from user -> target. This mirrors medbeam.dm's Beam() lifecycle.
+ current_beam = user.Beam(current_target, icon_state = "light_beam", time = 10 MINUTES, maxdistance = target_range, beam_type = /obj/effect/ebeam/medical, beam_color = "#ddd166")
+ RegisterSignal(current_beam, COMSIG_QDELETING, PROC_REF(beam_died))
+ RegisterSignal(user, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+ RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+ target_glow = mutable_appearance('icons/mob/effects/genetics.dmi', "servitude", -MUTATIONS_LAYER)
+ current_target.add_overlay(target_glow)
+ active_effect = current_target.apply_status_effect(/datum/status_effect/power/burden_shared)
+ playsound(target, 'sound/effects/magic/staff_healing.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+
+ return TRUE
+
+/datum/action/cooldown/power/theologist/theologist_root/shared/process()
+ // So we're kind-of parroting the original, but we don't want to stop proccessing so no . = ..()
+ build_all_button_icons(UPDATE_BUTTON_STATUS)
+ if(!active)
+ if(!owner || (next_use_time - world.time) <= 0)
+ STOP_PROCESSING(SSfastprocess, src)
+ return
+
+ // If the owner vanishes or we no longer have a target, end it.
+ if(active)
+ // checks if we actually hve an owner or target
+ if(!owner || !isliving(owner) || !current_target)
+ clear_link()
+ return
+ // Checks if our owner or target are DEAD
+ if(current_target.stat == DEAD || owner.stat == DEAD)
+ to_chat(owner, span_warning("You cannot share burdens with dead people!"))
+ clear_link()
+ return
+
+ // checks if our owner or target got SNAPPED
+ if(QDELETED(owner) || QDELETED(current_target))
+ clear_link()
+ return
+
+ // checks if our owner is INCAPACITATED or KNOCKED DOWN
+ // Honestly more of a balance concern the latter, sorry paraplegic people.
+ if(HAS_TRAIT(owner, TRAIT_INCAPACITATED) || HAS_TRAIT(owner, TRAIT_FLOORED))
+ to_chat(owner, span_warning("You need to be standing!"))
+ clear_link()
+ return
+
+ if(world.time <= last_check + check_delay)
+ return
+ last_check = world.time
+
+ // LoS gate. If it fails, deleting the beam triggers beam_died() -> clear_link().
+ if(!los_check(get_atom_on_turf(owner), current_target))
+ QDEL_NULL(current_beam)
+ return
+
+ on_beam_tick(owner, current_target)
+ exchange_buildup()
+
+
+/// Maths out who needs to receive the healing and who needs to receive the damage.
+/datum/action/cooldown/power/theologist/theologist_root/shared/proc/on_beam_tick(mob/living/carbon/user, mob/living/target)
+ // Non carbons get their own equalization.
+ if(!iscarbon(target))
+ equalize_simple(user, target)
+ return
+
+ var/list/user_damage = get_damage_snapshot(user)
+ var/list/target_damage = get_damage_snapshot(target)
+
+ for(var/damage_type in user_damage)
+ var/user_amount = user_damage[damage_type]
+ var/target_amount = target_damage[damage_type]
+ if(target_amount > user_amount)
+ equalize(target, user, damage_type)
+ if(target_amount < user_amount)
+ equalize(user, target, damage_type)
+ else
+ continue
+ return
+
+/// Gets the damage of the affected creature.
+/datum/action/cooldown/power/theologist/theologist_root/shared/proc/get_damage_snapshot(mob/living/carbon/subject)
+ return list(
+ "brute" = subject.getBruteLoss(),
+ "burn" = subject.getFireLoss(),
+ "tox" = subject.getToxLoss(),
+ "oxy" = subject.getOxyLoss(),
+ )
+
+/// Actually calls the proper health adjustments
+/datum/action/cooldown/power/theologist/theologist_root/shared/proc/equalize(mob/living/carbon/giver, mob/living/carbon/taker, damage_type as text)
+// Given we have already determined who has more and who has less in on_beam_tick, we can always assume that giver has more than taker, and thus make the comparison sum using that.
+ var/amount
+ // To summarize; heals the target by the amount (which is capped at 5)
+ switch(damage_type)
+ if("brute")
+ amount = clamp((giver.getBruteLoss() - taker.getBruteLoss()) / heal_division_factor, heal_min, heal_max)
+ giver.adjustBruteLoss(-amount)
+ taker.adjustBruteLoss(amount)
+
+ if("burn")
+ amount = clamp((giver.getFireLoss() - taker.getFireLoss()) / heal_division_factor, heal_min, heal_max)
+ giver.adjustFireLoss(-amount)
+ taker.adjustFireLoss(amount)
+
+ if("tox")
+ amount = clamp((giver.getToxLoss() - taker.getToxLoss()) / heal_division_factor, heal_min, heal_max)
+ adjust_tox_noinvert(giver, -amount)
+ adjust_tox_noinvert(taker, amount)
+
+ if("oxy")
+ amount = clamp((giver.getOxyLoss() - taker.getOxyLoss()) / heal_division_factor, heal_min, heal_max)
+ giver.adjustOxyLoss(-amount)
+ taker.adjustOxyLoss(amount)
+
+ // Piety buildup increases/deductions
+ // you can't gain piety from taking burdens from a ckey-less creature (sorry pets), but you can lose piety from dumping onto a ckey-less creature.
+ if(taker == owner && giver.ckey)
+ piety_buildup += amount * THEOLOGIST_PIETY_HEALING_COEFFICIENT
+ else if(giver == owner)
+ piety_buildup -= amount * THEOLOGIST_PIETY_HEALING_COEFFICIENT
+
+ return
+
+/// Special version for when targeting non-carbon living creatures (usually simple_creatures)
+/datum/action/cooldown/power/theologist/theologist_root/shared/proc/equalize_simple(mob/living/carbon/user, mob/living/target)
+ // Since we are comparing living vs carbon, we are doing health on our target and brute on our guy.
+ var/user_missingHP = user.maxHealth - user.health
+ var/target_missingHP = target.maxHealth - target.health
+
+ /*
+ This section is really ugly. Due for a do-over.
+ */
+ if(user_missingHP > target_missingHP)
+ var/bruteloss = clamp((user.getBruteLoss() - target.bruteloss) / heal_division_factor, heal_min, heal_max)
+ var/fireloss = clamp((user.getFireLoss() - target.fireloss) / heal_division_factor, heal_min, heal_max)
+ var/toxloss = clamp((user.getToxLoss() - target.toxloss) / heal_division_factor, heal_min, heal_max)
+ var/oxyloss = clamp((user.getOxyLoss() - target.oxyloss) / heal_division_factor, heal_min, heal_max)
+ user.adjustBruteLoss(-bruteloss)
+ user.adjustFireLoss(-fireloss)
+ adjust_tox_noinvert(user, -toxloss)
+ user.adjustOxyLoss(-oxyloss)
+ target.bruteloss -= bruteloss
+ target.fireloss -= fireloss
+ target.toxloss -= toxloss
+ target.oxyloss -= oxyloss
+
+ return
+
+ // Yaaay, healing the animals :)
+ if(user_missingHP < target_missingHP)
+ var/bruteloss = clamp((target.bruteloss - user.getBruteLoss()) / heal_division_factor, heal_min, heal_max)
+ var/fireloss = clamp((target.fireloss - user.getFireLoss()) / heal_division_factor, heal_min, heal_max)
+ var/toxloss = clamp((target.toxloss - user.getToxLoss()) / heal_division_factor, heal_min, heal_max)
+ var/oxyloss = clamp((target.oxyloss - user.getOxyLoss()) / heal_division_factor, heal_min, heal_max)
+ user.adjustBruteLoss(bruteloss)
+ user.adjustFireLoss(fireloss)
+ adjust_tox_noinvert(user, toxloss)
+ user.adjustOxyLoss(oxyloss)
+ target.bruteloss += bruteloss
+ target.fireloss += fireloss
+ target.toxloss += toxloss
+ target.oxyloss += oxyloss
+ else
+ return
+
+// You know, if I was a smarter man I'd have made the status effect actually handle effects.
+// Largely here for alerts so people know they are being damage transfered.
+/datum/status_effect/power/burden_shared
+ id = "burden_shared"
+ duration = 5 MINUTES // If somehow it overestays its welcome
+ tick_interval = STATUS_EFFECT_NO_TICK
+ alert_type = /atom/movable/screen/alert/status_effect/burden_shared
+
+/atom/movable/screen/alert/status_effect/burden_shared
+ name = "A Burden Shared"
+ desc = "Damage is being equalized between you and the caster!"
+ icon = 'icons/mob/actions/actions_spells.dmi'
+ icon_state = "swap"
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm
new file mode 100644
index 00000000000000..48736f936f38bc
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm
@@ -0,0 +1,201 @@
+/datum/power/theologist_root/twisted
+ name = "A Burden Twisted"
+ desc = "Channel chaotic energies into another creature next to you. The target is healed over time in random amounts up to the maximum, then damaged for half that amount in random damage types. \
+ \nGives Piety proportional to the net-positive amount of damage healed. Works on synthetic bodyparts."
+ security_record_text = "Subject can rapidly transmute the wounds of a target into smaller, insubstantial wounds."
+ action_path = /datum/action/cooldown/power/theologist/theologist_root/twisted
+
+ value = 5
+
+/datum/action/cooldown/power/theologist/theologist_root/twisted
+ name = "A Burden Twisted"
+ desc = "Channel chaotic energies into another creature next to you. The target is healed over time in random amounts up to the maximum, then damaged for half that amount in random damage types. \
+ Gives Piety proportional to the net-positive amount of damage healed. Works on synthetic bodyparts"
+ button_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "hand"
+ cooldown_time = 150
+ target_range = 1
+ target_type = /mob/living
+ click_to_activate = TRUE
+ target_self = FALSE
+ unset_after_click = TRUE
+
+ /// How much we can heal max with twisted per use.
+ var/healing_max = THEOLOGIST_ROOT_HEALING
+ /// Tracks how much healing we did throughout the proccess.
+ var/healing_done = 0
+
+ /// Tracks how much damage we did throughout the process.
+ var/damage_done = 0
+
+ /// The beam effect when channeling
+ var/datum/beam/current_beam
+
+ /// The current target of the effect
+ var/mob/living/current_target
+
+ /// Tells the do_while loop to keep_going
+ var/keep_going
+
+/datum/action/cooldown/power/theologist/theologist_root/twisted/use_action(mob/living/user, mob/living/target)
+ // We define the target just for the on_dispel listener
+ current_target = target
+ // Because we have a do_while, it won't get to the usual unset_click_ability() until after the efffect resolves, so we have to run it here.
+ unset_click_ability(owner, FALSE)
+ keep_going = TRUE
+ owner.visible_message(span_warning("[owner.get_visible_name()] lays a hand on [target.get_visible_name()], twisting their injuries into other, smaller injuries!"), span_notice("You twist [target.get_visible_name()]'s injuries!"))
+ // Listeners for dispelling.
+ RegisterSignal(user, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+ RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+ active = TRUE
+ // I am going to shamelessly steal the red meditation spotlight for a moment.
+ target.apply_status_effect(/datum/status_effect/spotlight_light/twisted, 1200)
+ current_beam = owner.Beam(target, icon_state = "light_beam", time = 120 SECONDS, maxdistance = target_range, beam_type = /obj/effect/ebeam/medical, beam_color = "#cf2525")
+
+ // Does the healing and damage
+ do
+ if(do_after(owner, 25, target = target))
+ if(target_range)
+ var/turf/owner_turf = get_turf(owner)
+ var/turf/target_turf = get_turf(target)
+ if(owner_turf && target_turf && get_dist(owner_turf, target_turf) > target_range)
+ owner.balloon_alert(owner, "Out of range!")
+ break // we use break here instead cuase we don't want to heal them anymore.
+ if(target.health >= target.maxHealth)
+ to_chat(owner, span_notice("Your target's health is full!"))
+ keep_going = FALSE
+ if(target.health < target.maxHealth)
+ new /obj/effect/temp_visual/heal(get_turf(target), "#cf2525")
+ playsound(owner, 'sound/effects/magic/cosmic_expansion.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ var/healtodmgcap = heal_random_damage(target)
+ deal_random_damage(target, (healtodmgcap / 2))
+ if(healing_done >= healing_max)
+ to_chat(owner, span_notice("You have channeled the full effect of [name]!"))
+ keep_going = FALSE
+ else
+ keep_going = FALSE
+ while (keep_going)
+
+ // cleanup
+ active = FALSE
+ target.remove_status_effect(/datum/status_effect/spotlight_light/twisted)
+ QDEL_NULL(current_beam)
+
+ // unregister signal
+ UnregisterSignal(current_target, COMSIG_ATOM_DISPEL)
+ UnregisterSignal(owner, COMSIG_ATOM_DISPEL)
+
+ // Handles piety gain
+ var/piety_gained = max(0, floor(healing_done * THEOLOGIST_PIETY_HEALING_COEFFICIENT))
+ // resets for next time
+ healing_done = 0
+ damage_done = 0
+ if(target.ckey)
+ adjust_piety(piety_gained)
+ if(piety_gained >= 1)
+ to_chat(owner, span_notice("You Burden Twisted yielded [piety_gained] piety!"))
+ else
+ to_chat(owner, span_notice("Your Burden Twisted yielded no piety!"))
+ else
+ to_chat(owner, span_notice("Your Burden Twisted yielded no piety!"))
+
+ return TRUE
+
+/datum/action/cooldown/power/theologist/theologist_root/twisted/set_click_ability(mob/on_who)
+ . = ..()
+ to_chat(owner, span_notice("You ready yourself to twist the burden of others! Left-click a creature next to you to target them!"))
+
+/// Does the given amount of healing, entirely randomly. Very chaotic, very random.
+/datum/action/cooldown/power/theologist/theologist_root/twisted/proc/heal_random_damage(mob/living/target)
+ // Cap for how much our random healing can do.
+ var/rand_cap
+ //Used to save how much healing was done in that switch-case.
+ var/heal_done = 0
+
+ // Gets all damage types on target
+ var/list/damage_choices = list()
+ var/brute_damage = target.getBruteLoss()
+ var/burn_damage = target.getFireLoss()
+ var/tox_damage = target.getToxLoss()
+ var/oxy_damage = target.getOxyLoss()
+ // Checks if there's any injuries to heal b4 rolling the damage-type.
+ if(brute_damage > 0) damage_choices += "brute"
+ if(burn_damage > 0) damage_choices += "burn"
+ if(tox_damage > 0) damage_choices += "tox"
+ if(oxy_damage > 0) damage_choices += "oxy"
+ // Hey we already healed you to the max!
+ if(healing_done >= healing_max)
+ return 0
+ // Nothing to heal
+ if(!damage_choices.len)
+ return
+ var/damage_choice = pick(damage_choices)
+ switch(damage_choice)
+ if("brute")
+ rand_cap = min(healing_max - healing_done, brute_damage)
+ heal_done = target.adjustBruteLoss(-rand(1, rand_cap))
+ healing_done += heal_done
+ if("burn")
+ rand_cap = min(healing_max - healing_done, burn_damage)
+ heal_done = target.adjustFireLoss(-rand(1, rand_cap))
+ healing_done += heal_done
+ if("tox")
+ rand_cap = min(healing_max - healing_done, tox_damage)
+ heal_done = adjust_tox_noinvert(target, (-rand(1, rand_cap)))
+ healing_done += heal_done
+ if("oxy")
+ rand_cap = min(healing_max - healing_done, oxy_damage)
+ heal_done = target.adjustOxyLoss(-rand(1, rand_cap))
+ healing_done += heal_done
+ return heal_done
+
+/// Pretty similar to heal_random_damage but we're just hurting them.
+/datum/action/cooldown/power/theologist/theologist_root/twisted/proc/deal_random_damage(mob/living/target, damage_max)
+ // Tells the while loop to stop
+ var/no_more_damaging = FALSE
+ // Cap for how much our random damage we can do.
+ var/rand_cap
+ //Used to save how much damage was done in that switch-case
+ var/dam_done
+
+ while(!no_more_damaging)
+ // Dealt max amount of damage already.
+ if(damage_done >= damage_max)
+ no_more_damaging = TRUE
+ break
+ var/list/damage_choices = list("brute", "burn", "tox", "oxy")
+ rand_cap = min(damage_max - damage_done)
+ dam_done = rand(1, rand_cap)
+ var/damage_choice = pick(damage_choices)
+ switch(damage_choice)
+ if("brute")
+ target.adjustBruteLoss(dam_done)
+ if("burn")
+ target.adjustFireLoss(dam_done)
+ if("tox")
+ adjust_tox_noinvert(target, dam_done)
+ // The jackpot
+ if("oxy")
+ target.adjustOxyLoss(dam_done)
+ damage_done += dam_done
+ // Keep the net healing at the standard for roots by subtracting damage from total healing done.
+ healing_done = max(0, healing_done - dam_done)
+
+ no_more_damaging = FALSE
+ return TRUE
+
+/// Dispel effect
+/datum/action/cooldown/power/theologist/theologist_root/twisted/proc/on_dispel(mob/owner, atom/dispeller)
+ SIGNAL_HANDLER
+ if(!active)
+ return NONE
+ keep_going = FALSE
+ owner.visible_message(span_warning("The resonant link between [owner.get_visible_name()] and [current_target.get_visible_name()] is broken!!"), span_notice("Your [name] is dispelled!"))
+ StartCooldownSelf()
+ return DISPEL_RESULT_DISPELLED
+
+// Legacy subtype for other powers still referencing this path.
+/datum/status_effect/spotlight_light/twisted
+ id = "twisted_spotlight"
+ spotlight_color = "#cf2525"
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_unattended.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_unattended.dm
new file mode 100644
index 00000000000000..f7a3daa4334e33
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_unattended.dm
@@ -0,0 +1,6 @@
+/// Now that there's other piety gen that isn't per-say healing, I figured a root that gets you into theologist powers without needing to get a heal per-say would be prudent.
+/datum/power/theologist_root/unattended
+ name = "A Burden Unattended"
+ desc = "Alleviating the burdens of others is not your duty.\
+ \nGrants you access to Theologist powers without the heavier cost of the other Burden powers, but comes with no other benefits."
+ value = 0
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/culling.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/culling.dm
new file mode 100644
index 00000000000000..7690890427206a
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/culling.dm
@@ -0,0 +1,57 @@
+/// Grants Piety based on watching unholy mobs die.
+/datum/power/theologist/culling
+ name = "Cull the Unholy"
+ desc = "You are invested in a holy mission; to cleanse evil wherever it may take root. Defeating unholy mobs (most mining mobs, undead and cultist constructs) will grant you 1 Piety, capped to 20. \
+ \nElite Mining mobs grant 10 piety, whilst Sentient Elites and Megafauna grants you 25 Piety. Both of these are uncapped.\
+ \nYou do not need to participate in the kill: as long as you witness their death and are in their proximity, you will gain the Piety."
+ security_record_text = "Subject fuels their powers by slaying creatures of unholy disposition."
+ value = 2
+ required_powers = list(/datum/power/theologist_root)
+ required_allow_subtypes = TRUE
+
+ /// Reference to the owner's piety component
+ var/datum/component/theologist_piety/piety_component
+ /// Mobs that should never grant piety on death either for being weak or otherwise.
+ var/static/list/mob_blacklist = typecacheof(list(
+ /mob/living/basic/mining/legion_brood,
+ ))
+
+/datum/power/theologist/culling/add(client/client_source)
+ ..()
+ get_piety_component()
+ RegisterSignal(SSdcs, COMSIG_GLOB_MOB_DEATH, PROC_REF(on_death))
+
+/datum/power/theologist/culling/remove()
+ UnregisterSignal(SSdcs, COMSIG_GLOB_MOB_DEATH)
+
+/// Attempts to acquire the piety component
+/datum/power/theologist/culling/proc/get_piety_component()
+ piety_component = power_holder.GetComponent(/datum/component/theologist_piety)
+ if(!piety_component)
+ return FALSE
+ return TRUE
+
+/// Whenever a mob dies, we go through this listener.
+/datum/power/theologist/culling/proc/on_death(datum/source, mob/living/died, gibbed)
+ if(!piety_component && !get_piety_component())
+ return
+ if((died.z != power_holder.z) || !(died in view(power_holder))) // We need to see it.
+ return
+ if(is_type_in_typecache(died, mob_blacklist)) // not worth piety
+ return
+
+ // Attempts to give piety if the mob is on the unholy mob list
+ if(is_type_in_typecache(died, GLOB.unholy_mobs))
+ if(ismegafauna(died) || (istype(died, /mob/living/simple_animal/hostile/asteroid/elite) && died.mind)) // Sentient elites and megafauna grant 25
+ piety_component.adjust_piety(THEOLOGIST_PIETY_MAJOR)
+ to_chat(power_holder, span_boldnotice("Slaying a mighty foe has granted you a great amount of piety!"))
+ else if(istype(died, /mob/living/simple_animal/hostile/asteroid/elite)) // If the mob is an elite grant 10
+ piety_component.adjust_piety(THEOLOGIST_PIETY_MODERATE)
+ to_chat(power_holder, span_boldnotice("Slaying a strong foe has granted you a large amount of piety!"))
+ else if(piety_component.piety <= 20) // grants 1 if not at piety cap
+ piety_component.adjust_piety(THEOLOGIST_PIETY_TRIVIAL * 2)
+ else
+ return
+ // Sound effect to confirm you got piety
+ playsound(power_holder, 'sound/effects/magic/charge.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/divine_protection.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/divine_protection.dm
new file mode 100644
index 00000000000000..69d44f84ce5d40
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/divine_protection.dm
@@ -0,0 +1,77 @@
+/*
+ Grants a passive block chance equal to half your piety and diminishes it on hit (with minor gating)
+*/
+
+/datum/power/theologist/divine_protection
+ name = "Divine Protection"
+ desc = "You gain a block chance (separate from all other block chance) equal to half your piety; reduce Piety by 5 when this triggers."
+ security_record_text = "Subject tends to unpredictably and miraculously avoid harm."
+ security_threat = POWER_THREAT_MAJOR
+ value = 4
+
+ required_powers = list(/datum/power/theologist_root/)
+ required_allow_subtypes = TRUE
+ /// World time (in deciseconds) when piety drain last triggered
+ var/last_piety_drain = 0
+ /// World time (in deciseconds) when block effect last triggered
+ var/last_block_effect = 0
+ /// The ratio of piety to block.
+ var/piety_ratio = 0.5
+
+/datum/power/theologist/divine_protection/add()
+ RegisterSignal(power_holder, COMSIG_LIVING_CHECK_BLOCK, PROC_REF(check_block))
+
+/datum/power/theologist/divine_protection/remove()
+ UnregisterSignal(power_holder, COMSIG_LIVING_CHECK_BLOCK)
+
+/// When calling the block signaler, we do a custom check for people's block.
+/datum/power/theologist/divine_protection/proc/check_block(mob/living/blocking_user, atom/movable/hitby, damage, attack_text, attack_type, armour_penetration, damage_type)
+ SIGNAL_HANDLER
+
+ if(!blocking_user)
+ return NONE
+
+ var/datum/component/theologist_piety/piety_component = blocking_user.GetComponent(/datum/component/theologist_piety)
+ if(!piety_component)
+ return NONE
+
+ var/block_chance = clamp(round(piety_component.piety * piety_ratio), 0, 100)
+ if(block_chance <= 0 || !prob(block_chance))
+ return NONE
+
+ block_effect(blocking_user, attack_text, hitby, attack_type)
+ // We only allow piety loss once per 0.4 seconds so you don't get your piety nuked by a shotgun.
+ if(world.time >= last_piety_drain + 4)
+ piety_component.adjust_piety(-THEOLOGIST_PIETY_MINOR)
+ last_piety_drain = world.time
+ return SUCCESSFUL_BLOCK
+
+/// Special effects + feedback for the block.
+/datum/power/theologist/divine_protection/proc/block_effect(mob/living/blocking_user, attack_text, atom/movable/hitby, attack_type)
+ if(!blocking_user)
+ return
+ blocking_user.visible_message(
+ span_danger("[attack_text] bounces harmlessly off of [blocking_user]!"),
+ span_userdanger("[attack_text] is blocked by your Divine Protection!"),
+ )
+ var/mob/living/attacker = GET_ASSAILANT(hitby)
+ if(attacker && (attack_type == MELEE_ATTACK || attack_type == UNARMED_ATTACK || attack_type == LEAP_ATTACK || attack_type == OVERWHELMING_ATTACK))
+ if(istype(hitby, /obj/item))
+ attacker.do_attack_animation(blocking_user, used_item = hitby)
+ else
+ attacker.do_attack_animation(blocking_user)
+ // don't trigger the fx more than 1 second to prevent taking ear damage from being shotgunned.
+ if(world.time < last_block_effect + 10)
+ return
+ last_block_effect = world.time
+
+ var/mutable_appearance/holy_glow = mutable_appearance('icons/mob/effects/genetics.dmi', "servitude", -MUTATIONS_LAYER)
+ blocking_user.add_overlay(holy_glow)
+ addtimer(CALLBACK(blocking_user, TYPE_PROC_REF(/atom, cut_overlay), holy_glow), 1 SECONDS)
+ playsound(blocking_user, 'sound/effects/magic/magic_block_holy.ogg', 50, TRUE)
+
+/// Removes the glow effect afterwards
+/datum/power/theologist/divine_protection/proc/remove_holy_glow(mob/living/blocking_user, image/holy_glow_image)
+ if(!blocking_user || !holy_glow_image)
+ return
+ blocking_user.vis_contents -= holy_glow_image
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm
new file mode 100644
index 00000000000000..7cfb144aff11a5
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm
@@ -0,0 +1,155 @@
+/* This largely served as the first example power of Theologist outside the roots.
+Biggest takeaway is just use status effects if it's any form of lingering effect; and to borrow cool mechanics from other code.
+Entropic Mending removes wounds (sometimes) and speeds up the target's metabolism, hunger and blood regen by 3x.
+*/
+/datum/power/theologist/entropic_mending
+ name = "Entropic Mending"
+ desc = "Entropy's a long road, a few steps further along it will do you more good than harm. Spend 5 Piety to touch another humanoid and attempt to restore it's lingering wounds. \
+ Moderate wounds will be healed automatically; all other wounds have a random chance to depending on severity. \
+ Invoking this power will cause temporary, lingering entropic effects on the target; such as increased metabolism, hunger and blood replenishment, at triple pace."
+ security_record_text = "Subject can accelerate a target's bodily functions (e.g metabolism) to be thrice as fast, and mend lingering wounds."
+ action_path = /datum/action/cooldown/power/theologist/entropic_mending
+ value = 6
+
+ required_powers = list(/datum/power/theologist_root/twisted)
+
+/datum/action/cooldown/power/theologist/entropic_mending
+ name = "Entropic Mending"
+ desc = "Entropy's a long road, a few steps further along it will do one more good than harm. Spend 5 Piety to touch another humanoid and attempt to restore it's lingering wounds. \
+ Moderate wounds will be healed automatically; all other wounds have a random chance to depending on severity. \
+ Invoking this power will cause temporary, lingering entropic effects on the target; such as increased metabolism, hunger and blood replenishment, at triple pace."
+ button_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "manip"
+ cooldown_time = 150
+ target_range = 1
+ target_type = /mob/living/carbon/human
+ click_to_activate = TRUE
+ target_self = FALSE
+ unset_after_click = TRUE
+ cost = 5
+
+ /// Current instance of the status effect
+ var/datum/status_effect/power/entropic_mending/active_effect
+
+/datum/action/cooldown/power/theologist/entropic_mending/use_action(mob/living/user, mob/living/target)
+ to_chat(owner, span_boldnotice("You begin to mend [target.get_visible_name()]"))
+ if(active_effect)
+ qdel(active_effect)
+ active_effect = target.apply_status_effect(/datum/status_effect/power/entropic_mending, src)
+ active = TRUE
+ return TRUE
+
+/datum/action/cooldown/power/theologist/entropic_mending/set_click_ability(mob/on_who)
+ . = ..()
+ to_chat(owner, span_notice("You channel entropic energies into your hand! Left-click a creature next to you to target them!"))
+
+/// Callback from the status effect that updates the active state
+/datum/action/cooldown/power/theologist/entropic_mending/proc/effect_expired(amount)
+ //Always reset this after use.
+ active = FALSE
+ return
+
+// Status effect that Burden Revered applies
+/datum/status_effect/power/entropic_mending
+ id = "entropic_mending"
+ duration = 3 MINUTES
+ tick_interval = 1 SECONDS
+ alert_type = /atom/movable/screen/alert/status_effect/entropic_mending
+
+ /// The power responsible for this.
+ var/datum/action/cooldown/power/theologist/entropic_mending/entropic_mending
+
+ /// Because a lot of things here require static types.
+ var/mob/living/carbon/human/victim
+
+ /// How much we speed up blood regen with
+ var/blood_regen_rate = 3
+ /// How much we speed up metabolism with
+ var/metabolic_boost = 3
+ /// How much we speed up hunger gain with
+ var/hunger_rate = 3
+ //// Tracks if we've modified the physiology of the owner
+ VAR_PRIVATE/physiology_modified = FALSE
+ /// Tracks how many wounds were healed by this.
+ var/wounds_treated = 0
+
+
+/atom/movable/screen/alert/status_effect/entropic_mending
+ name = "Entropic Mending"
+ desc = "Your body's internal functions seem to be accelerated, for better or worse."
+ icon_state = "arrow8" // Placeholder
+
+// So given it is 'part of the effect', we actually handle the wound removal on here.
+/datum/status_effect/power/entropic_mending/on_apply()
+ victim = owner // Whilst I would like to set it on_creation, it doesn't always pass it along 4somerasinss.
+ playsound(owner, 'sound/effects/magic/staff_healing.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE)
+ // Attemps to remove wounds
+ for(var/datum/wound/wound in victim.all_wounds)
+ switch(wound.severity)
+ if(WOUND_SEVERITY_TRIVIAL, WOUND_SEVERITY_MODERATE)
+ handle_wound_heal_success(entropic_mending.owner, victim, wound)
+ wounds_treated++
+ if(WOUND_SEVERITY_SEVERE)
+ if(prob(60))
+ handle_wound_heal_success(entropic_mending.owner, victim, wound)
+ wounds_treated++
+ else
+ to_chat(entropic_mending.owner, span_warning("The restorative energies fail to treat the [wound.name]!"))
+ if(WOUND_SEVERITY_CRITICAL)
+ if(prob(30))
+ handle_wound_heal_success(entropic_mending.owner, victim, wound)
+ wounds_treated++
+ else
+ to_chat(entropic_mending.owner, span_warning("The restorative energies fail to treat the [wound.name]!"))
+ // Feedback to user
+ if(!LAZYLEN(victim.all_wounds)) // Not necessarily bad, you might use this for it's metabolize effect.
+ to_chat(entropic_mending.owner, span_notice("[victim.get_visible_name()] has no wounds to treat!"))
+ else if(wounds_treated <= 0)
+ to_chat(entropic_mending.owner, span_warning("[entropic_mending.name] failed to heal any of [victim.get_visible_name()]'s wounds!"))
+ else if(LAZYLEN(victim.all_wounds))
+ to_chat(entropic_mending.owner, span_notice("[entropic_mending.name] managed to heal some of [victim.get_visible_name()]'s wounds!"))
+ else
+ to_chat(entropic_mending.owner, span_notice("[entropic_mending.name] managed to heal all of [victim.get_visible_name()]'s' wounds!"))
+
+ // Makes our blood regenerate faster
+ if(!physiology_modified)
+ victim.physiology.blood_regen_mod *= blood_regen_rate
+ physiology_modified = TRUE
+
+ return TRUE
+
+/// Just there to quickly handle wound-healing + return values.
+/datum/status_effect/power/entropic_mending/proc/handle_wound_heal_success(caster, mob/living/victim, datum/wound/wound)
+ new /obj/effect/temp_visual/heal(get_turf(victim), "#cf2525")
+ wound.remove_wound()
+ to_chat(entropic_mending.owner, span_notice("The restorative energies manage to treat the [wound.name]!"))
+ to_chat(victim, span_notice("Your [wound.name] got healed!"))
+
+// Sets the link with the original action
+/datum/status_effect/power/entropic_mending/on_creation(mob/living/new_owner, datum/action/cooldown/power/theologist/entropic_mending/passed_power)
+ entropic_mending = passed_power
+ . = ..()
+
+/datum/status_effect/power/entropic_mending/on_remove()
+ // Removes the blood regen mult
+ if(physiology_modified)
+ victim.physiology.blood_regen_mod /= blood_regen_rate
+ physiology_modified = FALSE
+ expire()
+
+// We're not spelling it out but basically all the vibes of age-based healing.
+/datum/status_effect/power/entropic_mending/tick(seconds_between_ticks)
+ //Code that the metabolic boost virus symptom would shamelessly steal from us, 16 years in the past.
+ // Unlike metabolic boost we actually check if there's a liver
+ var/obj/item/organ/liver/liver = victim.get_organ_slot(ORGAN_SLOT_LIVER)
+ if(liver)
+ // Not totally accurate with the liver damage but WHO WILL NOTICE THIS DISCRAPANCY?! IS IT YOU, MR./MS. CODEDIVER?! ARE YOU GOING TO TRIVIA THIS LIKE VIGGO'S TOE?!
+ victim.reagents.metabolize(victim, (metabolic_boost - (liver.damage * 0.03)) * SSMOBS_DT, 0, can_overdose=TRUE)
+ victim.overeatduration = max(victim.overeatduration - 4 SECONDS, 0)
+ victim.adjust_nutrition(-hunger_rate * HUNGER_FACTOR) //Hunger depletes at 3x the normal speed
+
+/// Communicates back to the power that the effect has ended.
+/datum/status_effect/power/entropic_mending/proc/expire()
+ // Report back BEFORE deletion starts
+ if(entropic_mending)
+ entropic_mending.effect_expired()
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/flagellant.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/flagellant.dm
new file mode 100644
index 00000000000000..b621fb8654f441
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/flagellant.dm
@@ -0,0 +1,97 @@
+/// Grants Piety based on getting smacked.
+/datum/power/theologist/flagellant
+ name = "Flagellant Piety"
+ desc = "You suffer so others may live. You gain Piety from being hurt by creatures. The damage taken must be directly caused by a creature; some indirect methods of damaging you such as throwing explosives or using area-of-effect magics may not grant piety.\
+ \nThe Piety gained is based on the pre-mitigation damage (block, armor etc): if the source is yourself, it is instead based on the total damage you took."
+ security_record_text = "Subject fuels their powers by being hurt by others."
+ value = 4
+ required_powers = list(/datum/power/theologist_root)
+ required_allow_subtypes = TRUE
+
+ /// Reference to the holder's piety component.
+ var/datum/component/theologist_piety/piety_component
+ /// Total damage needed to gain 1 piety.
+ var/damage_per_piety = 15
+ /// Cap on how much piety can be reached. I originally designed this as a balancing factor but being smacked by someone else is kinda balanced already.
+ var/piety_cap = 100
+
+/datum/power/theologist/flagellant/post_add(client/client_source)
+ ..()
+ get_piety_component()
+ RegisterSignal(power_holder, COMSIG_LIVING_SUCCESSFUL_BLOCK, PROC_REF(on_successful_block))
+ RegisterSignal(power_holder, COMSIG_MOB_APPLY_DAMAGE, PROC_REF(on_apply_damage))
+
+/datum/power/theologist/flagellant/remove()
+ UnregisterSignal(power_holder, list(COMSIG_LIVING_SUCCESSFUL_BLOCK, COMSIG_MOB_APPLY_DAMAGE))
+
+/// Attempts to acquire the piety component.
+/datum/power/theologist/flagellant/proc/get_piety_component()
+ piety_component = power_holder.GetComponent(/datum/component/theologist_piety)
+ if(!piety_component)
+ return FALSE
+ return TRUE
+
+/// Main damage hook. Estimates base damage from blocked% and grants piety.
+/datum/power/theologist/flagellant/proc/on_apply_damage(datum/source, damage, damagetype, def_zone, blocked, wound_bonus, exposed_wound_bonus, sharpness, attack_direction, attacking_item, ...)
+ SIGNAL_HANDLER
+ if(!piety_component && !get_piety_component()) // fix piety component if it isnt there
+ return
+ if(piety_component.piety >= piety_cap) // if piety exceeds 20
+ return
+ if(!isnum(damage) || damage <= 0 || damage_per_piety <= 0) // don't run on 0 damage and prevents divide-by-zero scenarios.
+ return
+ if(!is_valid_attack_source(attacking_item)) // is the damage sourced from a mob?
+ return
+
+ var/mob/living/attack_source = get_attack_source_mob(attacking_item)
+ // Anti-cheese: if you are your own damage source, use post-mitigation damage. Otherwise, use pre-mitigation damage.
+ var/base_damage = (attack_source == power_holder) ? damage : estimate_unmitigated_from_blocked(damage, blocked)
+ if(base_damage <= 0) // what? how? better to protect against dividing by 0
+ return
+
+ piety_component.adjust_piety(base_damage / damage_per_piety)
+
+/// Checks if we are able to succesfuly determine a mob source.
+/datum/power/theologist/flagellant/proc/is_valid_attack_source(atom/hit_by)
+ return !isnull(get_attack_source_mob(hit_by))
+
+/// Resolves a mob source from mob/projectile/item damage sources.
+/datum/power/theologist/flagellant/proc/get_attack_source_mob(atom/hit_by)
+ if(ismob(hit_by))
+ return hit_by
+ if(istype(hit_by, /obj/projectile))
+ var/obj/projectile/projectile = hit_by
+ if(ismob(projectile.firer))
+ return projectile.firer
+ return null
+ if(istype(hit_by, /obj/item))
+ var/obj/item/item = hit_by
+ if(ismob(item.loc))
+ return item.loc
+ return null
+
+/// Maths out the unmitigated damage based on the blocked damage.
+/datum/power/theologist/flagellant/proc/estimate_unmitigated_from_blocked(damage, blocked)
+ if(!isnum(blocked))
+ return damage
+ var/block_multiplier = (100 - blocked) / 100
+ if(block_multiplier <= 0)
+ return damage
+ return damage / block_multiplier
+
+/// Successful block hook, emitted from /mob/living/proc/check_block().
+/// We maths out the damage on a succesful block as well since this is one of the few ways to do full mitigation.
+/datum/power/theologist/flagellant/proc/on_successful_block(datum/source, atom/hit_by, damage, attack_text, attack_type, armour_penetration, damage_type)
+ SIGNAL_HANDLER
+ if(!piety_component && !get_piety_component()) // fix piety component if it isnt there
+ return
+ if(piety_component.piety >= piety_cap) // if piety exceeds cap
+ return
+ if(!isnum(damage) || damage <= 0 || damage_per_piety <= 0) // don't run on 0 damage and prevents divide-by-zero scenarios.
+ return
+ if(!is_valid_attack_source(hit_by)) // is the damage sourced from a mob?
+ return
+ if(get_attack_source_mob(hit_by) == power_holder) // self-sourced full block uses post-mitigation (0), so no gain.
+ return
+
+ piety_component.adjust_piety(damage / damage_per_piety)
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm
new file mode 100644
index 00000000000000..0b93d11d0c2ab1
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm
@@ -0,0 +1,91 @@
+/* Piety generation for when its a quiet day.
+*/
+/datum/power/theologist/pious_prayer
+ name = "Pious Prayer"
+ desc = "Focus yourself into prayer. If you are in the Chapel, this grants you Piety unless you have 10 or more Piety. Performing prayers elsewhere only has a small chance to grant Piety. Being religious increases the efficiency of this skill."
+ security_record_text = "Subject fuels their powers with visits to the Chapel."
+ value = 2
+
+ action_path = /datum/action/cooldown/power/theologist/pious_prayer
+ required_powers = list(/datum/power/theologist_root)
+ required_allow_subtypes = TRUE
+
+/datum/power/theologist/pious_prayer/add()
+ ADD_TRAIT(power_holder, TRAIT_SEE_BLESSED_TILES, src)
+
+/datum/power/theologist/pious_prayer/remove()
+ REMOVE_TRAIT(power_holder, TRAIT_SEE_BLESSED_TILES, src)
+
+/datum/action/cooldown/power/theologist/pious_prayer
+ name = "Pious Prayer"
+ desc = "Perform a small prayer. If you are in the Chapel, this grants you Piety unless you have 10 or more Piety. Performing prayers elsewhere only has a small chance to grant Piety. Being religious increases the efficiency of this skill."
+ button_icon = 'icons/obj/antags/cult/structures.dmi'
+ button_icon_state = "tomealtar"
+ cooldown_time = 5
+
+ /// The maximum amount of piety you can get from praying
+ var/prayer_cap = 10
+
+/datum/action/cooldown/power/theologist/pious_prayer/New()
+ // Increase prayer cap based on various factors.
+ // Are you the Chaplain?
+ if(is_chaplain_job(usr.mind?.assigned_role))
+ prayer_cap = 15
+ return
+
+/datum/action/cooldown/power/theologist/pious_prayer/use_action(mob/living/user, atom/target)
+ ///Tells the do_while loop to keep_going
+ var/keep_going = TRUE
+ /// One time message for the prayer cap so we don't clog the chat.
+ var/cap_warning_given
+ /// The area we're praying in.
+ var/area/area = get_area(user)
+
+ user.visible_message(span_warning("[user] begins to pray!"), span_notice("You begin to pray!"))
+ active = TRUE
+ user.apply_status_effect(/datum/status_effect/spotlight_light/divine)
+ do
+ if(do_after(owner, 50, target = user))
+ if(get_piety() >= prayer_cap && !cap_warning_given)
+ // We don't actually stop people from praying cause this can be used for ROLEPLAAAAY
+ to_chat(user, span_warning("You cannot gain any more piety from prayer!"))
+ cap_warning_given = TRUE
+ else if(istype(area, /area/station/service/chapel) || prob(check_how_religious(user))) // If you're in the chapel or if fate aligns.
+ if(cap_warning_given)
+ continue
+ adjust_piety(1)
+ to_chat(user, span_notice("You feel more pious after your prayer."))
+ else
+ keep_going = FALSE
+ while (keep_going)
+ to_chat(user, span_notice("You stop praying."))
+ // cleanup
+ keep_going = FALSE
+ active = FALSE
+ user.remove_status_effect(/datum/status_effect/spotlight_light/divine)
+
+/// As the name implies, we take various factors that suggest a target's devotion, as well as a few misc. factors
+/datum/action/cooldown/power/theologist/pious_prayer/proc/check_how_religious(mob/living/user)
+ // Combined total chance.
+ var/total_chance = 10
+
+ // Are you the chaplain?
+ if(is_chaplain_job(user.mind?.assigned_role))
+ total_chance += 20
+ // Do you have the spiritual personality trait?
+ if(HAS_TRAIT(user, TRAIT_SPIRITUAL))
+ total_chance += 15
+ // Do you carry the bible on your person?
+ if(has_bible(user))
+ total_chance += 10
+ // Are you standing on a blessed tile? (Blessed with holy water).
+ if(locate(/obj/effect/blessing) in user.loc)
+ total_chance += 15
+
+ return total_chance
+
+/// Most people don't but it'd be cool if they did.
+/datum/action/cooldown/power/theologist/pious_prayer/proc/has_bible(mob/living/user)
+ if(!user)
+ return FALSE
+ return !!locate(/obj/item/book/bible) in user.get_all_contents()
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm
new file mode 100644
index 00000000000000..1ce0d9122f47d5
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm
@@ -0,0 +1,439 @@
+/*
+ Does 3 things;
+ - When targeting objects, it dispels it; and if it has a holy equivelant, it turns it into that equivelant.
+ - When targeting creatures, it dispels them.
+ - It will also remove all poisons contained in the item if it can hold reagents.
+ If it fails to do any of these 3 succesfully, it refunds the piety.
+*/
+
+/datum/power/theologist/purify
+ name = "Purify"
+ desc = "Cleanses impurity from objects and creatures in melee range. The chosen target is immediately dispelled and purified of all poisons. \
+ If the target is an object with a holy equivelant, it turns it into that (e.g water into holy water). Has varying piety costs, but usually defaults to 5."
+ security_record_text = "Subject can end magical effects on a target, nullify poisons and transmute objects into their holy variants with a touch."
+ security_threat = POWER_THREAT_MAJOR
+ action_path = /datum/action/cooldown/power/theologist/purify
+ value = 5
+
+ required_powers = list(/datum/power/theologist_root/shared)
+
+/datum/action/cooldown/power/theologist/purify
+ name = "Purify"
+ desc = "Cleanses impurity from objects and creatures in melee range. The chosen target is immediately dispelled and purified of all poisons. \
+ If the target is an object with a holy equivelant, it turns it into that (e.g water into holy water). Has varying piety costs, but usually defaults to 5."
+ button_icon = 'icons/obj/mining_zones/artefacts.dmi'
+ button_icon_state = "purified_soulstone"
+ cooldown_time = 60
+
+ target_range = 1
+ click_to_activate = TRUE
+ /// Accumulated piety cost for this use.
+ var/pending_piety_cost = 0
+
+/datum/action/cooldown/power/theologist/purify/InterceptClickOn(mob/living/clicker, params, atom/target)
+ . = ..()
+ // Makes it so we always override the base click. Don't want to use the item you are trying to purify.
+ return TRUE
+
+/datum/action/cooldown/power/theologist/purify/use_action(mob/living/user, atom/target)
+ if(!target)
+ return FALSE
+ pending_piety_cost = 0
+
+ // Special construct purification channel.
+ if(purify_construct(user, target))
+ return TRUE
+ var/success = FALSE
+
+ // General dispel on target
+ if(target.dispel(user))
+ success = TRUE
+
+ // Remove poison from a creature's bloodstream or an object's reagents.
+ if(target.reagents)
+ var/removed = target.reagents.remove_reagent(/datum/reagent/toxin, target.reagents.total_volume, include_subtypes = TRUE)
+ if(removed > 0)
+ success = TRUE
+
+ // Holy-equivalent conversions.
+ if(convert_objects(user, target))
+ success = TRUE
+ if(convert_reagents(user, target))
+ success = TRUE
+
+ if(success)
+ pending_piety_cost = max(pending_piety_cost, THEOLOGIST_PIETY_MINOR)
+ playsound(user, 'sound/effects/magic/magic_block_holy.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ else
+ user.balloon_alert(user, "nothing to be purified!")
+ return success
+
+/// Adds and processes the variable cost of the interaction.
+/datum/action/cooldown/power/theologist/purify/proc/try_add_cost(mob/living/user, cost)
+ if(cost <= 0)
+ return TRUE
+ if(get_piety() < (pending_piety_cost + cost))
+ user.balloon_alert(user, "needs [cost] piety!")
+ return FALSE
+ pending_piety_cost += cost
+ return TRUE
+
+/datum/action/cooldown/power/theologist/purify/on_action_success(mob/living/user, atom/target)
+ . = ..()
+ if(pending_piety_cost > 0)
+ adjust_piety(-pending_piety_cost)
+ pending_piety_cost = 0
+ return
+
+/// Deletes the old item and creates a new item in its place.
+/datum/action/cooldown/power/theologist/purify/proc/replace_target(atom/target, typepath, mob/living/user)
+ if(!target || !typepath)
+ return null
+
+ var/obj/old_obj = target
+ var/mob/living/holder
+ var/hand_index = 0
+ if(istype(old_obj, /obj/item) && ismob(old_obj.loc))
+ holder = old_obj.loc
+ hand_index = holder.get_held_index_of_item(old_obj)
+
+ var/obj/new_obj
+ if(hand_index && holder)
+ new_obj = new typepath(null)
+ holder.put_in_hand(new_obj, hand_index, forced = TRUE)
+ else
+ new_obj = new typepath(old_obj.loc)
+
+ qdel(old_obj)
+ return new_obj
+
+/// Copies the attributes of seeds to be passed along.
+/datum/action/cooldown/power/theologist/purify/proc/copy_seed_stats(obj/item/seeds/from_seed, obj/item/seeds/to_seed)
+ if(!from_seed || !to_seed)
+ return
+ to_seed.lifespan = from_seed.lifespan
+ to_seed.endurance = from_seed.endurance
+ to_seed.maturation = from_seed.maturation
+ to_seed.production = from_seed.production
+ to_seed.yield = from_seed.yield
+ to_seed.potency = from_seed.potency
+ to_seed.instability = from_seed.instability
+ to_seed.weed_rate = from_seed.weed_rate
+ to_seed.weed_chance = from_seed.weed_chance
+
+/// Converts reagents in different types.
+/datum/action/cooldown/power/theologist/purify/proc/convert_reagents(mob/living/user, atom/target)
+ if(!target?.reagents)
+ return FALSE
+
+ var/converted = FALSE
+
+ // (Unholy) Water -> Holy Water
+ var/water_amt = target.reagents.get_reagent_amount(/datum/reagent/water, REAGENT_STRICT_TYPE)
+ var/unholy_amt = target.reagents.get_reagent_amount(/datum/reagent/fuel/unholywater, REAGENT_STRICT_TYPE)
+ var/holy_source_amt = water_amt + unholy_amt
+ if(holy_source_amt > 0)
+ if(try_add_cost(user, THEOLOGIST_PIETY_MODERATE))
+ if(water_amt > 0)
+ target.reagents.remove_reagent(/datum/reagent/water, water_amt)
+ if(unholy_amt > 0)
+ target.reagents.remove_reagent(/datum/reagent/fuel/unholywater, unholy_amt)
+ target.reagents.add_reagent(/datum/reagent/water/holywater, holy_source_amt)
+ to_chat(user, span_notice("The water in [target] gleams into holy water."))
+ converted = TRUE
+
+ // Blood -> Godsblood
+ var/blood_amt = target.reagents.get_reagent_amount(/datum/reagent/blood, REAGENT_SUB_TYPE)
+ if(blood_amt > 0)
+ var/blood_cost = CEILING(blood_amt / 5, 5)
+ if(try_add_cost(user, blood_cost))
+ target.reagents.remove_reagent(/datum/reagent/blood, blood_amt, include_subtypes = TRUE)
+ target.reagents.add_reagent(/datum/reagent/medicine/omnizine/godblood, blood_amt)
+ to_chat(user, span_notice("The blood in [target] is sanctified into godsblood."))
+ converted = TRUE
+
+ return converted
+
+/// Converts objects into other objects!
+/datum/action/cooldown/power/theologist/purify/proc/convert_objects(mob/living/user, atom/target)
+ // Melon -> Holy Melon
+ if(istype(target, /obj/item/food/grown/watermelon) && !istype(target, /obj/item/food/grown/holymelon))
+ if(!try_add_cost(user, THEOLOGIST_PIETY_MINOR))
+ return FALSE
+ var/obj/item/food/grown/watermelon/melon = target
+ var/obj/item/seeds/old_seed = melon.get_plant_seed()
+ var/obj/item/food/grown/holymelon/new_melon = replace_target(melon, /obj/item/food/grown/holymelon, user)
+ if(new_melon && old_seed)
+ var/obj/item/seeds/new_seed = new /obj/item/seeds/watermelon/holy(null)
+ copy_seed_stats(old_seed, new_seed)
+ new_melon.seed = new_seed
+ to_chat(user, span_notice("Divine light transforms [melon] into a holymelon."))
+ return TRUE
+
+ // Soulstone -> Purified Soulstone
+ if(istype(target, /obj/item/soulstone))
+ var/obj/item/soulstone/stone = target
+ if(stone.theme == THEME_HOLY)
+ return FALSE
+ if(!try_add_cost(user, THEOLOGIST_PIETY_MINOR))
+ return FALSE
+ stone.required_role = null
+ stone.theme = THEME_HOLY
+ stone.update_appearance()
+ for(var/mob/shade_to_deconvert in stone.contents)
+ stone.assign_master(shade_to_deconvert, user)
+ UnregisterSignal(stone, COMSIG_BIBLE_SMACKED)
+ to_chat(user, span_notice("You purify [stone], its glow becoming serene."))
+ return TRUE
+
+ // Any book -> Bible (free)
+ if(istype(target, /obj/item/book) && !istype(target, /obj/item/book/bible))
+ if(!try_add_cost(user, 0))
+ return FALSE
+ replace_target(target, /obj/item/book/bible, user)
+ to_chat(user, span_notice("The pages reorder themselves into a bible."))
+ return TRUE
+
+ // Skateboard -> Holy Skateboard
+ if(istype(target, /obj/item/melee/skateboard) && !istype(target, /obj/item/melee/skateboard/holyboard))
+ if(!try_add_cost(user, THEOLOGIST_PIETY_MAJOR))
+ return FALSE
+ replace_target(target, /obj/item/melee/skateboard/holyboard, user)
+ to_chat(user, span_notice("The board hums and becomes a holy skateboard."))
+ return TRUE
+
+ // Bow -> Divine Bow
+ if(istype(target, /obj/item/gun/ballistic/bow) && !istype(target, /obj/item/gun/ballistic/bow/divine))
+ if(!try_add_cost(user, THEOLOGIST_PIETY_MAJOR))
+ return FALSE
+ replace_target(target, /obj/item/gun/ballistic/bow/divine, user)
+ to_chat(user, span_notice("The bow brightens, reshaping into a divine bow."))
+ return TRUE
+
+ // Arrow -> Holy Arrow
+ if(istype(target, /obj/item/ammo_casing/arrow) && !istype(target, /obj/item/ammo_casing/arrow/holy))
+ if(!try_add_cost(user, THEOLOGIST_PIETY_MINOR))
+ return FALSE
+ replace_target(target, /obj/item/ammo_casing/arrow/holy, user)
+ to_chat(user, span_notice("The arrow brightens with holy light."))
+ return TRUE
+
+ return FALSE
+
+/** Special interaction for hype moments and aura: Deacons (people converted by Chaplain Sects) and the Chaplain can deconvert constructs. It takes times and is interuptable.
+ * If there is a mind inside the construct, it retains it and is de-antag'd.
+ * If there isn't, it prompts a ghost to see if they want to be part of it.
+**/
+/datum/action/cooldown/power/theologist/purify/proc/purify_construct(mob/living/user, atom/target)
+ if(!isconstruct(target))
+ return FALSE
+
+ var/mob/living/basic/construct/construct_target = target
+ if(construct_target.theme == THEME_HOLY)
+ return FALSE
+ if(!are_we_a_holy_man(user))
+ user.balloon_alert(user, "you need to be a holy figure to purify that!")
+ return FALSE
+ if(get_piety() < THEOLOGIST_PIETY_CRUSHING)
+ user.balloon_alert(user, "needs [THEOLOGIST_PIETY_CRUSHING] piety!")
+ return FALSE
+
+ // Piety is spent regardless of success.
+ adjust_piety(-THEOLOGIST_PIETY_CRUSHING)
+
+ // End click targeting during the channel for clarity.
+ unset_click_ability(user, refund_cooldown = TRUE)
+
+ var/datum/beam/link = user.Beam(construct_target, icon_state = "kinesis", override_target_pixel_x = 0)
+ construct_target.SetStun(15 SECONDS, ignore_canstun = TRUE)
+ // normally you don't use userdanger for this but its a hype moment.
+ playsound(user, 'sound/effects/magic/forcewall.ogg', 50, TRUE)
+ user.visible_message(span_userdanger("[user] channels a beam of holy energy, attempting to purify any and all unholy qualities of [construct_target]!"))
+ var/channel_success = do_after(user, 15 SECONDS, target = construct_target)
+ construct_target.SetStun(0, ignore_canstun = TRUE)
+ QDEL_NULL(link)
+
+ if(!channel_success || QDELETED(construct_target))
+ return TRUE
+
+ var/typepath = get_purified_construct_type(construct_target)
+
+ // Fallback for the constructs that dont have a purified version e.g proteons
+ if(!typepath)
+ convert_construct_in_place(construct_target, user)
+ post_conversion(user, construct_target)
+ return TRUE
+
+ var/mob/living/basic/construct/new_construct = new typepath(construct_target.loc)
+ if(construct_target.mind)
+ construct_target.mind.remove_antag_datum(/datum/antagonist/cult)
+ construct_target.mind.remove_antag_datum(/datum/antagonist/shade_minion)
+ construct_target.mind.transfer_to(new_construct, force_key_move = TRUE)
+ else
+ enable_construct_ghost_control(new_construct)
+
+ post_conversion(user, new_construct)
+ qdel(construct_target)
+ return TRUE
+
+/// Applies post conversion fluff + curse.
+/datum/action/cooldown/power/theologist/purify/proc/post_conversion(mob/living/user, mob/living/target)
+ user.visible_message(span_notice("[user] purifies [target]!"))
+ playsound(user, 'sound/effects/his_grace/his_grace_ascend.ogg', 50, TRUE)
+ // Special feedback to the construct
+ to_chat(target, span_blue("The Geometer's presence in your mind fades, what was once your own freewill slips back into the forefront. You look down at your body; and while it is still the dark steel that adorns your body, you can move it of your own free will. Your freedom is returned; but still tethered forevermore to this body."))
+ // Special feedback to the caster
+ to_chat(user, span_blue("As you have shown yourself to be pious, to take on the burdens of others; you now take on the greatest burden of another. Whoever the vile entity responsible may be, you take it away from this enslaved tool, and bury the energy responsible deep inside you. You have freed it; but this darkness seems to be eating away at you."))
+ user.apply_status_effect(/datum/status_effect/debt_to_the_geometer, src)
+ to_chat(user, span_cult_bold("You have been cursed; your actions carry a price, and you shall be made to pay it."))
+
+/// Are we a chaplain or deacon (chaplain sect convertee)?
+/datum/action/cooldown/power/theologist/purify/proc/are_we_a_holy_man(mob/living/user)
+ if(is_chaplain_job(user.mind?.assigned_role))
+ return TRUE
+ return user.mind?.holy_role == HOLY_ROLE_DEACON
+
+/// Matches the purified construct type
+/datum/action/cooldown/power/theologist/purify/proc/get_purified_construct_type(mob/living/basic/construct/target)
+ if(istype(target, /mob/living/basic/construct/artificer))
+ return /mob/living/basic/construct/artificer/angelic
+ if(istype(target, /mob/living/basic/construct/wraith))
+ return /mob/living/basic/construct/wraith/angelic
+ if(istype(target, /mob/living/basic/construct/juggernaut))
+ return /mob/living/basic/construct/juggernaut/angelic
+ return null
+
+/// Adds the ghost control component, allowing someone to play a purified construct.
+/datum/action/cooldown/power/theologist/purify/proc/enable_construct_ghost_control(mob/living/basic/construct/target)
+ target.AddComponent(\
+ /datum/component/ghost_direct_control,\
+ poll_candidates = TRUE,\
+ poll_question = "Do you want to play as a Theologist's purified construct?",\
+ role_name = "purified construct",\
+ poll_ignore_key = POLL_IGNORE_CONSTRUCT,\
+ assumed_control_message = "You are a purified construct, freed from the Geometer's influence. Your will is now your own.",\
+ )
+
+/// In the event that its a construct without a purified equivelant e.g protean, we do this instead.
+/datum/action/cooldown/power/theologist/purify/proc/convert_construct_in_place(mob/living/basic/construct/target, mob/living/user)
+ var/old_theme = target.theme
+ target.theme = THEME_HOLY
+ target.faction = list(FACTION_HOLY)
+ ADD_TRAIT(target, TRAIT_ANGELIC, INNATE_TRAIT)
+ // Neutralize hostile AI (e.g., lavaland proteons).
+ if(target.ai_controller)
+ target.ai_controller = null
+ var/datum/component/ai_retaliate_advanced/retaliate = target.GetComponent(/datum/component/ai_retaliate_advanced)
+ if(retaliate)
+ qdel(retaliate)
+ if(target.icon_state)
+ target.cut_overlay("glow_[target.icon_state]_[old_theme]")
+ target.add_overlay("glow_[target.icon_state]_[target.theme]")
+ target.update_appearance()
+ target.mind?.remove_antag_datum(/datum/antagonist/cult)
+ target.mind?.remove_antag_datum(/datum/antagonist/shade_minion)
+ if(!target.mind)
+ enable_construct_ghost_control(target)
+ user.visible_message(span_notice("[user] purifies [target]!"))
+ return
+
+// Purifying constructs invokes you a curse. You have to pay the bloodtithe; all of your blood. Payed in installments.
+/datum/status_effect/debt_to_the_geometer
+ id = "debt_to_the_geometer"
+ alert_type = /atom/movable/screen/alert/status_effect/debt_to_the_geometer
+ /// Total blood required to pay off the debt.
+ var/debt_goal = 600
+ /// Blood paid so far.
+ var/debt_paid = 0
+ /// Blood lost per second while the effect is active.
+ var/bleed_per_second = 5
+ // The curse is starting to tithe blood.
+ var/curse_has_started = FALSE
+
+/datum/status_effect/debt_to_the_geometer/on_apply()
+ . = ..()
+ if(!.)
+ return FALSE
+ RegisterSignal(owner, COMSIG_LIVING_DEATH, PROC_REF(on_owner_death))
+ return TRUE
+
+/datum/status_effect/debt_to_the_geometer/tick(seconds_between_ticks)
+ if(!owner)
+ return
+
+ // We give a bit of an unpredictable buffer before we start bleeding the person. A bit of space to have them RP.
+ if(!curse_has_started)
+ if(prob(0.5))
+ curse_has_started = TRUE
+ to_chat(owner, span_cult_bold("Blood is starting to ooze from every part of your body!"))
+ else
+ return
+
+ // You pissed off the Geometer herself. If Nar'Sie exists, ensure we are her current target.
+ if(GLOB.cult_narsie)
+ var/turf/owner_turf = get_turf(owner)
+ if(owner_turf && owner_turf.z == GLOB.cult_narsie.z)
+ var/datum/component/singularity/singularity_component = GLOB.cult_narsie.singularity?.resolve()
+ if(singularity_component && singularity_component.target != owner)
+ GLOB.cult_narsie.acquire(owner)
+
+ // Visual feedback.
+ spawn_blood_splatter()
+
+ // Periodic blood loss and tracking.
+ if(istype(owner, /mob/living/carbon))
+ var/mob/living/carbon/carbon_owner = owner
+ // Because ticks & blood are fucky we do before and after for cost mapping
+ var/before = carbon_owner.blood_volume
+ carbon_owner.bleed(bleed_per_second * seconds_between_ticks)
+ var/removed = max(before - carbon_owner.blood_volume, 0)
+ debt_paid += removed
+
+ if(debt_paid >= debt_goal)
+ qdel(src)
+ return
+
+/// Creates a blood splatter of varying sizes.
+/datum/status_effect/debt_to_the_geometer/proc/spawn_blood_splatter()
+ var/turf/bleed_turf = get_turf(owner)
+ if(!bleed_turf)
+ return
+ var/amt
+ // random blood splatters
+ switch(rand(1, 100))
+ if(1 to 80)
+ amt = rand(1, 8)
+ if(81 to 95)
+ amt = rand(9, 15)
+ else
+ amt = rand(16, 25)
+ owner.add_splatter_floor(bleed_turf, amt)
+
+/datum/status_effect/debt_to_the_geometer/on_remove()
+ if(owner)
+ to_chat(owner, span_cult_bold("The Geometer's notice is no longer upon you."))
+ return ..()
+
+/// When the owner dies, spray them out like a juicebox.
+/datum/status_effect/debt_to_the_geometer/proc/on_owner_death(datum/source, gibbed)
+ SIGNAL_HANDLER
+ if(!owner)
+ return
+ if(istype(owner, /mob/living/carbon))
+ var/mob/living/carbon/carbon_owner = owner
+ carbon_owner.blood_volume = 0
+
+ var/turf/center = get_turf(owner)
+ if(center)
+ for(var/turf/T in range(1, center))
+ owner.add_splatter_floor(T, FALSE)
+ owner.visible_message(span_boldwarning("[owner]'s body bursts open, showering blood everywhere!"))
+ playsound(owner, 'sound/effects/wounds/splatter.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ qdel(src)
+
+/atom/movable/screen/alert/status_effect/debt_to_the_geometer
+ name = "Debt to the Geometer"
+ desc = "The Geometer demands you pay the blood price for your actions."
+ icon = 'icons/obj/mining_zones/artefacts.dmi'
+ icon_state = "soulstone2" // Placeholder
+ alerttooltipstyle = "cult"
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm
new file mode 100644
index 00000000000000..845ea800c8aab0
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm
@@ -0,0 +1,205 @@
+/datum/power/theologist/smiting_strike
+ name = "Smiting Strike"
+ desc = "Channel energy into the item you are currently holding. Your next attack that hits with it against a creature deals 15 additional burn damage and sends them flying backwards 4 spaces. This deals extra damage to unholy creatures. \
+ This knockback cannot stun or damage on impact. Costs 5 Piety to use. This effect ends if the item leaves your hands."
+ security_record_text = "Subject can bless their own weapons to knock back foes and sear their bodies."
+ security_threat = POWER_THREAT_MAJOR
+ action_path = /datum/action/cooldown/power/theologist/smiting_strike
+ value = 4
+
+ required_powers = list(/datum/power/theologist_root/revered)
+
+/datum/action/cooldown/power/theologist/smiting_strike
+ name = "Smiting Strike"
+ desc = "Channel energy into the item you are currently holding. Your next attack that hits with it against a creature deals 15 additional burn damage and sends them flying backwards 4 spaces. This deals extra damage to unholy creatures. \
+ This knockback cannot stun or damage on impact. Costs 5 Piety to use. This effect ends if the item leaves your hands."
+ button_icon = 'icons/mob/actions/actions_cult.dmi'
+ button_icon_state = "sword_fling"
+ cooldown_time = 60
+ cost = 5
+
+ /// How much damage the smite element will do
+ var/smite_damage = 15
+ /// How much distance the smite element will knock back
+ var/smite_knockback = 4
+ /// How much bonus damage does it do to unholy targets.
+ var/unholy_smite_bonus = 10
+ ///If the upgrade to imbue multiple items is unlocked.
+ var/can_imbue_multiples
+ ///If it singular, which one is the one currently imbued?
+ var/obj/currently_imbued
+
+/datum/action/cooldown/power/theologist/smiting_strike/use_action(mob/living/user, atom/target)
+ var/obj/item/potential_smite = owner.get_active_held_item()
+ if(!potential_smite)
+ if(owner.get_inactive_held_item())
+ to_chat(owner, span_warning("You must hold the desired item in your hands to imbue it!"))
+ else
+ to_chat(owner, span_warning("You aren't holding anything that can be imbued!"))
+ return FALSE
+
+ // To prevent you from smiting with something that doesn't normally want you to attack wtih it.
+ if(potential_smite.force <= 0)
+ to_chat(owner, span_warning("Item is too weak"))
+ return FALSE
+ // In order to detect our buff, we pass along a trait to the host item.
+ if(HAS_TRAIT(potential_smite, TRAIT_HAS_SMITING_STRIKE))
+ to_chat(owner, span_warning("The item is already imbued!"))
+ return FALSE
+ if(potential_smite.item_flags & ABSTRACT)
+ return FALSE
+ if(SEND_SIGNAL(potential_smite, COMSIG_ITEM_MARK_RETRIEVAL, src, owner) & COMPONENT_BLOCK_MARK_RETRIEVAL)
+ return FALSE
+
+ if(!can_imbue_multiples)
+ imbue_singular(potential_smite)
+ else
+ imbue_global(potential_smite)
+ return TRUE
+
+/// Applies the element to the item and removes it from any other actively imbued item.
+/datum/action/cooldown/power/theologist/smiting_strike/proc/imbue_singular(obj/to_imbue)
+ if(currently_imbued)
+ var/thingholdingimbued = currently_imbued.loc
+ if(ismob(thingholdingimbued))
+ to_chat(thingholdingimbued, span_warning("The smiting energies leave [currently_imbued]"))
+ currently_imbued.RemoveElement(/datum/element/theologist_smite)
+ currently_imbued = null
+ currently_imbued = to_imbue
+ currently_imbued.AddElement(/datum/element/theologist_smite, smite_damage, smite_knockback, FALSE, TRUE, TRUE)
+ to_chat(owner, span_notice("You infuse smiting energies into [currently_imbued]"))
+
+/// Applies the element and does not remove any existing ones.
+/datum/action/cooldown/power/theologist/smiting_strike/proc/imbue_global(obj/to_imbue)
+ to_imbue.AddElement(/datum/element/theologist_smite, smite_damage, smite_knockback, FALSE, TRUE, FALSE)
+ to_chat(owner, span_notice("You infuse smiting energies into [to_imbue]"))
+
+// Whilst I originally considered adding just the knockback element, we kind-of want more control over when the smite fades.
+/datum/element/theologist_smite
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ /// extra damage the smite does
+ var/smite_damage
+ /// extra damage to unholy targets
+ var/unholy_smite_bonus
+ /// distance the atom will be thrown
+ var/throw_distance
+ /// whether this can throw anchored targets (tables, etc)
+ var/throw_anchored
+ /// whether this is a gentle throw (default false means people thrown into walls are stunned / take damage)
+ var/throw_gentle
+ /// whether dropping this item ends the element
+ var/self_terminate_on_drop
+ /// The person assigned to be the holder of the object.
+ var/mob/living/holder
+ /// the bless effect
+ var/image/bless_overlay
+
+
+// This is basically the knockback code but hybridized. Sue me.
+/datum/element/theologist_smite/Attach(atom/target, smite_damage = 1, unholy_smite_bonus = 1, throw_distance = 1, throw_anchored = FALSE, throw_gentle = FALSE, self_terminate_on_drop = FALSE)
+// While the balancer inside me suggests we restrict this to melee hits... I kind of want to see the fun of ranged smites.
+// For the future person to balance this; really just remove projectile_hit() and the first if in this sequence if you want to axe ranged.
+ . = ..()
+ if(isgun(target) || isprojectilespell(target)) // turrets, etc
+ RegisterSignal(target, COMSIG_PROJECTILE_ON_HIT, PROC_REF(projectile_hit))
+ else if(isitem(target))
+ RegisterSignal(target, COMSIG_ITEM_AFTERATTACK, PROC_REF(item_afterattack))
+ else
+ return ELEMENT_INCOMPATIBLE
+
+ ADD_TRAIT(target, TRAIT_HAS_SMITING_STRIKE, src)
+ src.smite_damage = smite_damage
+ src.unholy_smite_bonus = unholy_smite_bonus
+ src.throw_distance = throw_distance
+ src.throw_anchored = throw_anchored
+ src.throw_gentle = throw_gentle
+ src.self_terminate_on_drop = self_terminate_on_drop
+
+ if(isitem(target) && self_terminate_on_drop) // No point tracking this if we aren't going to self_terminate on drop
+ RegisterSignal(target, COMSIG_ITEM_DROPPED, PROC_REF(on_item_dropped))
+
+ // Applies the sparkling bless effect
+ // I'd normally do mutable_appearance but overlays are uppity with elements and this method works with rust soooo I am using it like this.
+ bless_overlay = image(icon = 'icons/effects/effects.dmi', icon_state = "blessed", layer = target.layer - 0.1)
+ RegisterSignal(target, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(apply_bless_overlay))
+ target.update_appearance()
+ // dispel listener
+ RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+
+/// Checks if the item is no longer in our hands. If so, remove this element.
+/datum/element/theologist_smite/proc/on_item_dropped(datum/source, mob/user)
+ SIGNAL_HANDLER
+ if(self_terminate_on_drop)
+ Detach(source)
+ return
+
+/// Applies the overlay effect
+/datum/element/theologist_smite/proc/apply_bless_overlay(atom/parent_atom, list/overlays)
+ SIGNAL_HANDLER
+
+ if(bless_overlay)
+ overlays += bless_overlay
+
+// Prevents signalers from loitering.
+/datum/element/theologist_smite/Detach(atom/source)
+ UnregisterSignal(source, COMSIG_ATOM_DISPEL)
+ UnregisterSignal(source, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_POST_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT, COMSIG_ATOM_UPDATE_OVERLAYS))
+ if(self_terminate_on_drop)
+ UnregisterSignal(source, COMSIG_ITEM_DROPPED)
+ REMOVE_TRAIT(source, TRAIT_HAS_SMITING_STRIKE, src)
+ source.update_appearance()
+ holder = null
+ return ..()
+
+
+/// triggered after an item attacks something
+/datum/element/theologist_smite/proc/item_afterattack(obj/item/source, atom/target, mob/user, list/modifiers)
+ SIGNAL_HANDLER
+
+ if(!isliving(target))
+ return
+ if(on_hit(target, user, get_dir(source, target)))
+ Detach(source)
+
+
+/// triggered after a projectile hits something
+/datum/element/theologist_smite/proc/projectile_hit(datum/fired_from, atom/movable/firer, atom/target, Angle)
+ SIGNAL_HANDLER
+
+ if(!isliving(target))
+ return
+ if(on_hit(target, null, angle2dir(Angle)))
+ Detach(fired_from)
+
+/// The on hit effect
+/datum/element/theologist_smite/proc/on_hit(mob/living/target, mob/thrower, throw_dir)
+ //Knockback code
+ if(!ismovable(target) || throw_dir == null)
+ return FALSE
+ if(target.anchored && !throw_anchored)
+ return FALSE
+ // Magic immune
+ if(target.can_block_resonance(1))
+ // deliberately eats your smite.
+ return TRUE
+ if(throw_distance < 0)
+ throw_dir = REVERSE_DIR(throw_dir)
+ throw_distance *= -1
+ var/atom/throw_target = get_edge_target_turf(target, throw_dir)
+ target.safe_throw_at(throw_target, throw_distance, 1, thrower, gentle = throw_gentle)
+ new /obj/effect/temp_visual/electricity(get_turf(target), "#ddd166")
+ playsound(target, 'sound/effects/magic/magic_block_holy.ogg', 75, TRUE)
+ //If the mob is in the unholy mob typecache, they take more damage from smite.
+ if(is_type_in_typecache(target, GLOB.unholy_mobs))
+ target.adjustFireLoss(smite_damage + unholy_smite_bonus)
+ else
+ target.adjustFireLoss(smite_damage)
+ to_chat(target, span_userdanger("You are knocked back by a burning, resonant energy!"))
+ return TRUE
+
+/// The on dispel effect
+/datum/element/theologist_smite/proc/on_dispel(atom/source, atom/dispeller)
+ SIGNAL_HANDLER
+ Detach(source)
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm
new file mode 100644
index 00000000000000..bd60c20c6b5cd2
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm
@@ -0,0 +1,26 @@
+/* Simple example of an upgrade, though more like a sidegrade here.
+Most of the effects are already baked into the existing power for convenience.
+*/
+/datum/power/theologist/imbue_armaments
+ name = "Imbue Armaments"
+ desc = "Changes Smiting Strike to no longer be removed when it passes hands, and allows you to have an unlimited amount of items blessed. Reduces the smite effect's knockback by 2 and damage by 5."
+ security_record_text = "Subject can bless the weapons of others to enhance their lethality."
+ security_threat = POWER_THREAT_MAJOR
+ value = 3
+
+ required_powers = list(/datum/power/theologist/smiting_strike)
+
+/datum/power/theologist/imbue_armaments/post_add()
+ . = ..()
+ var/datum/power/theologist/smiting_strike/smiting_strike = power_holder.get_power(/datum/power/theologist/smiting_strike)
+ var/datum/action/cooldown/power/theologist/smiting_strike/smite_action = smiting_strike.action_path // I really should find a better way to get the variables of actions.
+ smite_action.smite_damage -= 5
+ smite_action.smite_knockback -= 2
+ smite_action.can_imbue_multiples = TRUE
+
+/datum/power/theologist/imbue_armaments/remove()
+ var/datum/power/theologist/smiting_strike/smiting_strike = power_holder.get_power(/datum/power/theologist/smiting_strike)
+ var/datum/action/cooldown/power/theologist/smiting_strike/smite_action = smiting_strike.action_path
+ smite_action.smite_damage += 5
+ smite_action.smite_knockback += 2
+ smite_action.can_imbue_multiples = FALSE
diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm
new file mode 100644
index 00000000000000..395fa7b7e6f9b4
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers_action.dm
@@ -0,0 +1,322 @@
+/*
+ Custom action system for supporting the powers system. Use this anytime you add actions to a power.
+ Almost all archetypes have their own subtype to handle their own resources and mechanics.
+ This one is largely responsible for the actions framework itself.
+
+ Largely modeled after changeling_power.dm
+*/
+/datum/action/cooldown/power
+ name = "abstract power action - ahelp this"
+ background_icon_state = "bg_revenant"
+ overlay_icon_state = "bg_revenant_border"
+ button_icon = 'icons/mob/actions/backgrounds.dmi'
+ active_overlay_icon_state = "bg_spell_border_active_red"
+ ranged_mousepointer = 'icons/effects/mouse_pointers/weapon_pointer.dmi'
+
+ /// Maximum state of consciousness before the ability is blocked.
+ /// For example, `UNCONSCIOUS` prevents it from being used when in hard crit or dead,
+ /// while `DEAD` allows the ability to be used on any stat values.
+ var/req_stat = CONSCIOUS
+ /// If your power has an active state of any type, use this.
+ var/active
+ /// Is this a resonant ability (read: magical)? Determines if this ability stop working if you are silenced and if we check against target magic immunites.
+ var/resonant = TRUE
+ /// Does this ability stop working if you are incapacitated?
+ var/disabled_by_incapacitate = TRUE
+ /// What power is the origin?
+ var/origin_power
+ /// Can only humans use this power?
+ var/human_only = TRUE
+ /// Can we target ourselves?
+ var/target_self = TRUE
+ /// Do we need our hands free?
+ var/need_hands_free = TRUE
+
+ /// If set, we must wait this long before use_action executes. Cast time basically.
+ var/use_time = 0
+ /// Flags passed to do_after during use_time (e.g. IGNORE_HELD_ITEM, IGNORE_USER_LOC_CHANGE).
+ var/use_time_flags = NONE
+ /// Optional overlay to show on the user during use_time.
+ var/use_time_overlay_type
+
+ /// Maximum targeting range (in tiles) for click_to_activate powers. Set to 0 or null for no range limit.
+ var/target_range
+ /// If set, clicked target MUST be of this type (or subtype).
+ var/target_type
+ /// If aim assist is used for click targeting. Disable to disable.
+ var/aim_assist = TRUE
+ /// Do we check for anti magic on the target when we target them? Basically if your action targets but doesn't do anything directly magical to them immediately (like projectiles), this should be false.
+ var/anti_magic_on_target = TRUE
+ /// Magic resistance flags checked on target during try_use. This should mostly just be holy and mental, since normal magic resistance is checked in can_block_resonance()
+ var/magic_resistance_types
+
+/// Attempts to actively use the action by pathing through validation, antimagic, do_use_time and finally use_action
+/datum/action/cooldown/power/proc/try_use(mob/living/user, atom/target)
+ SHOULD_CALL_PARENT(TRUE)
+ if(!can_use(user, target))
+ return FALSE
+ // Checking for anti-resonance/anti-magic below which really is a pain.
+ if(anti_magic_on_target && ismob(target) && target != user) // If the spell checks antimagic, and if the target is a mob, and if the target is not us.
+ var/mob/mob_target = target
+ if(resonant && mob_target.can_block_resonance(1)) // Resonance checks are handled by the resonant var.
+ // I would like to deduct resources on spell fail, but I have no good way of implementing it during the validation layer when most costs happen in the on_action_success layer. TODO for the future chap who wants this.
+ return FALSE
+ // Checks against magic resistances beyond the standard above.
+ if(resonant && magic_resistance_types && mob_target.can_block_magic(magic_resistance_types, charge_cost = 0))
+ return FALSE
+ if(!do_use_time(user, target))
+ return FALSE
+ // on_use_action signaler, emitted from the user so listeners can hook once on the mob.
+ SEND_SIGNAL(user, COMSIG_POWER_ACTION_USED, src, target)
+ if(use_action(user, target))
+ // on_action_success signaler, emitted from the user so listeners can hook once on the mob.
+ SEND_SIGNAL(user, COMSIG_POWER_ACTION_SUCCESS, src, target)
+ on_action_success(user, target)
+ return TRUE
+ return FALSE
+
+/// Validates the action can be used at all.
+/// All validation should exist in here. If your action or path has custom validation, override the proc and add it to can_use()
+/datum/action/cooldown/power/proc/can_use(mob/living/user, atom/target)
+ SHOULD_CALL_PARENT(TRUE)
+ if(!can_be_used_by(user)) // Runs can_be_used_by below
+ return FALSE
+ if(disabled_by_incapacitate && HAS_TRAIT(user, TRAIT_INCAPACITATED))
+ owner.balloon_alert(user, "incapacitated!")
+ return FALSE
+ if(resonant && HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED))
+ owner.balloon_alert(user, "silenced!")
+ return FALSE
+ if(need_hands_free && HAS_TRAIT(user, TRAIT_HANDS_BLOCKED))
+ owner.balloon_alert(user, "restrained!")
+ return FALSE
+ if(req_stat < user.stat) // Whilst this seems similiar to trait_incapacitated, it is also used to check if you're dead in the event that disable_by_incapacitate is false. No corpses using powers!
+ owner.balloon_alert(user, "incapacitated!")
+ return FALSE
+ return TRUE
+
+/// Checks if we exist (wow) and are human.
+/datum/action/cooldown/power/proc/can_be_used_by(mob/living/user)
+ SHOULD_CALL_PARENT(TRUE)
+ if(QDELETED(user))
+ return FALSE
+ if(!ishuman(user) && human_only)
+ owner.balloon_alert(user, "not a human!")
+ return FALSE
+ return TRUE
+
+/// This is where ALL THE MAGIC HAPPENS. An action should ALWAYS route through here for its primary mechanics, even if you use multiple different inputs.
+/// Make sure you return TRUE or FALSE to tell the power that it has succesfully (or unsuccesfully) been used and trigger on_action_success.
+/datum/action/cooldown/power/proc/use_action(mob/living/user, atom/target)
+ return TRUE
+
+/// Handles optional channel time before the action goes off, defined by use_time.
+/// If use_time_overlay_type is defined, it also puts an overlay on the mob.
+/datum/action/cooldown/power/proc/do_use_time(mob/living/user, atom/target)
+ if(use_time <= 0)
+ return TRUE
+ var/atom/use_target = target ? target : user
+ var/mutable_appearance/use_overlay
+ if(use_time_overlay_type)
+ var/atom/overlay_obj = new use_time_overlay_type(null)
+ use_overlay = new /mutable_appearance(overlay_obj)
+ qdel(overlay_obj)
+ user.add_overlay(use_overlay)
+ if(click_to_activate && unset_after_click) // unsets the mouse on use time.
+ unset_click_ability(user)
+ var/success = do_after(user, use_time, target = use_target, timed_action_flags = use_time_flags)
+ if(use_overlay && !QDELETED(user))
+ user.cut_overlay(use_overlay)
+ return success
+
+/// Anything that should happen as a result of use_action returning TRUE.
+/// Cost systems for archetypes to name an example.
+/datum/action/cooldown/power/proc/on_action_success(mob/living/user, atom/target)
+ return
+
+/// Applies damage to a living target, automatically applying an armor check.
+/// Returns the amount of damage dealt (as per apply_damage).
+/datum/action/cooldown/power/proc/apply_damage_with_armor(mob/living/target, damage, damage_type = BRUTE, attack_flag = MELEE, def_zone = null, armour_penetration = 0, weak_against_armour = FALSE, silent = TRUE)
+ if(!target)
+ return 0
+
+ var/armor_block = target.run_armor_check(
+ def_zone = def_zone,
+ attack_flag = attack_flag,
+ armour_penetration = armour_penetration,
+ weak_against_armour = weak_against_armour,
+ silent = silent,
+ )
+ return target.apply_damage(damage, damage_type, def_zone, armor_block)
+
+/**
+ * Actions run through the following pipeline:
+ * Trigger() -> PreActivate(owner) -> Activate(owner) -> try_use(user, target)
+ * Click-activated powers DO NOT route through this; they use InterceptClickOn below.
+ */
+/datum/action/cooldown/power/Activate(atom/target)
+ var/mob/living/user = owner
+ if(!user)
+ return FALSE
+
+ // Non-targeted powers just use immediately.
+ if(!try_use(user, target = null))
+ return FALSE
+
+ StartCooldown()
+ return TRUE
+
+/** Intercepts client owner clicks to activate the ability.
+ * Called by the base click intercept system on left click.
+ * Whilst /datum/action/cooldown does have click support, it doesn't support range-detecting and target filtering, so we are overriding that with our own.
+ * Returning only tells the game if we consume the normal click behavior (if true) or not (if false)
+ */
+/datum/action/cooldown/power/InterceptClickOn(mob/living/clicker, params, atom/target)
+ if(!IsAvailable(feedback = TRUE))
+ return FALSE
+ if(!target)
+ return FALSE
+ if(aim_assist)
+ var/atom/aim_assist_target = aim_assist(clicker, target, target_type)
+ if(aim_assist_target)
+ target = aim_assist_target
+
+ // Checks if we are allowed to actually target that type.
+ if(target_type && !istype(target, target_type))
+ return FALSE
+
+ // Check if we are allowed to target ourselves.
+ if(!target_self && target == clicker)
+ owner.balloon_alert(clicker, "Can't target self!")
+ return FALSE
+
+ // Range gate (only applies if target_range is non-zero).
+ if(target_range)
+ var/turf/clicker_turf = get_turf(clicker)
+ var/turf/target_turf = get_turf(target)
+ if(clicker_turf && target_turf && get_dist(clicker_turf, target_turf) > target_range)
+ owner.balloon_alert(clicker, "Out of range!")
+ return FALSE
+
+ // If the power can't be used, refuse the click and keep intercept state as-is.
+ if(!try_use(clicker, target))
+ // fixes the overlay from cast time getting stuck.
+ if(clicker?.click_intercept == src && unset_after_click)
+ unset_click_ability(clicker, refund_cooldown = TRUE)
+ return FALSE
+ StartCooldown()
+
+ // Successful click.
+ if(unset_after_click)
+ unset_click_ability(clicker, refund_cooldown = FALSE)
+
+ clicker.next_click = world.time + click_cd_override
+ return TRUE
+
+/// Optional aim assist for click targeting. Override for custom behavior.
+/datum/action/cooldown/power/proc/aim_assist(mob/living/clicker, atom/target, target_type_path)
+ if(!isturf(target) && !istype(target_type, /turf)) // only auto aims if you click turfs; or if the auto-aim type is a turf.
+ return
+
+ // If we have a specific type we're targeting, we're targeting that instead.
+ if(target_type_path)
+ return locate(target_type_path) in target
+
+ // Otherwise, find any human, or if that fails, any living target
+ return locate(/mob/living/carbon/human) in target || locate(/mob/living) in target
+
+// We override the click abilities to fix an issue with the active_overlay_icon_state not appearing when it should.
+/datum/action/cooldown/power/set_click_ability(mob/on_who)
+ . = ..()
+ if(.)
+ build_all_button_icons(UPDATE_BUTTON_STATUS | UPDATE_BUTTON_OVERLAY)
+ return .
+
+/datum/action/cooldown/power/unset_click_ability(mob/on_who, refund_cooldown = TRUE)
+ . = ..()
+ if(.)
+ for(var/datum/action/A as anything in on_who.actions)
+ for(var/datum/hud/H as anything in A.viewers)
+ var/atom/movable/screen/movable/action_button/B = A.viewers[H]
+ if(B)
+ B.cut_overlay(B.button_overlay)
+ B.button_overlay = null
+ B.active_overlay_icon_state = null
+ on_who.update_mob_action_buttons(UPDATE_BUTTON_OVERLAY, TRUE)
+ return .
+
+/*
+Projectile action code down below
+*/
+
+/// Fires the configured or given projectile at the clicked target.
+/// This assumes you are shooting just one projectile. Override if you need multi-shot, spread, special spawn logic, etc.
+/// Requires click_to_activate = TRUE to do mouse based targeting.
+/datum/action/cooldown/power/proc/fire_projectile(mob/living/user, atom/target, obj/projectile/projectile)
+ SHOULD_CALL_PARENT(TRUE)
+
+ var/projectile_path = projectile
+ if(!projectile_path || !user || !target)
+ return FALSE
+
+ var/turf/user_turf = get_turf(user)
+ if(!user_turf)
+ return FALSE
+
+ // If no clicked target was provided (non-click cast), shoot forward.
+ if(!target)
+ var/turf/aim_turf = user_turf
+ var/aim_range = target_range ? target_range : 7
+
+ for(var/step_count in 1 to aim_range)
+ var/turf/next_turf = get_step(aim_turf, user.dir)
+ if(!next_turf)
+ break
+ aim_turf = next_turf
+
+ target = aim_turf
+
+ // Still validate after we possibly auto-filled target
+ if(!target)
+ return FALSE
+
+ var/obj/projectile/projectile_instance = new projectile_path(user_turf)
+ ready_projectile(projectile_instance, target, user)
+
+ projectile_instance.fire()
+ return TRUE
+
+/// Sets up a projectile for firing.
+/// Mirrors cooldown/spell/pointed/projectile
+/datum/action/cooldown/power/proc/ready_projectile(obj/projectile/projectile_instance, atom/target, mob/living/user)
+ SHOULD_CALL_PARENT(TRUE)
+
+ if(!projectile_instance)
+ return
+
+ projectile_instance.firer = user
+ projectile_instance.fired_from = src
+ projectile_instance.aim_projectile(target, user)
+
+ // Saves an instance of the creating power for reference. Usually you want this to check for affinity scaling on Thaumaturge.
+ // This only works on resonant projectiles. If you have a non-resonant projectile that needs this for some reason, override the proc.
+ if(istype(projectile_instance, /obj/projectile/resonant))
+ var/obj/projectile/resonant/resonant_proj = projectile_instance
+ resonant_proj.creating_power = src
+ resonant_proj.antimagic_flags = magic_resistance_types
+
+ // If you want “on hit” logic for your power, hook it here.
+ RegisterSignal(projectile_instance, COMSIG_PROJECTILE_SELF_ON_HIT, PROC_REF(on_power_projectile_hit))
+
+/// Signal handler for projectile hits; relays into an overridable proc.
+/datum/action/cooldown/power/proc/on_power_projectile_hit(datum/source, mob/firer, atom/target, angle, hit_limb)
+ SIGNAL_HANDLER
+
+ on_projectile_hit(source, firer, target, angle, hit_limb)
+
+// Override in specific powers if you want “on hit” effects that connect back to the spell, e.g some-sort of ongoing effect.
+/// Anything that should otherwise happen normally on projectile hit should preferably be handled in /obj/projectile/.../on_hit
+/datum/action/cooldown/power/proc/on_projectile_hit(datum/source, mob/firer, atom/target, angle, hit_limb)
+ return
+
+
diff --git a/modular_doppler/modular_powers/code/powers_antimagic.dm b/modular_doppler/modular_powers/code/powers_antimagic.dm
new file mode 100644
index 00000000000000..fef188d3ce3d14
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers_antimagic.dm
@@ -0,0 +1,142 @@
+/*
+ I originally considered overriding the original /mob/proc/can_block_magic but really keeping it modular is the name of the game.
+
+*/
+
+/// Proc that cheks if a mob can block any resonance-based magic.
+/// This checks against both resonant antimagic and normal antimagic.
+/// This does NOT check against special antimagics like unholy and mental and must be checked against seperately.
+/mob/proc/can_block_resonance(charge_cost = 1)
+ var/list/antimagic_sources = list()
+ var/is_resonance_blocked = FALSE
+
+ if(SEND_SIGNAL(src, COMSIG_MOB_RECEIVE_MAGIC, MAGIC_RESISTANCE, charge_cost, antimagic_sources) & COMPONENT_MAGIC_BLOCKED) // Normal magic immunity applies too.
+ is_resonance_blocked = TRUE
+ if(HAS_TRAIT(src, TRAIT_ANTIMAGIC)) // Normal magic immunity.
+ is_resonance_blocked = TRUE
+ if(HAS_TRAIT(src, TRAIT_ANTIRESONANCE)) // Resonance based magic immunity.
+ is_resonance_blocked = TRUE
+
+ if(is_resonance_blocked && charge_cost > 0 && !HAS_TRAIT(src, TRAIT_RECENTLY_BLOCKED_MAGIC))
+ on_block_resonance_effects(antimagic_sources)
+ return is_resonance_blocked
+
+/// Called when we succesfully block a resonant effect..
+/mob/proc/on_block_resonance_effects()
+ return
+
+/mob/living/on_block_resonance_effects(list/antimagic_sources)
+ ADD_TRAIT(src, TRAIT_RECENTLY_BLOCKED_MAGIC, MAGIC_TRAIT)
+ addtimer(TRAIT_CALLBACK_REMOVE(src, TRAIT_RECENTLY_BLOCKED_MAGIC, MAGIC_TRAIT), 6 SECONDS)
+
+ var/mutable_appearance/antimagic_effect
+ var/antimagic_color
+ var/atom/antimagic_source = length(antimagic_sources) ? pick(antimagic_sources) : src
+
+ visible_message(
+ span_warning("[src] pulses blue as [ismob(antimagic_source) ? p_they() : antimagic_source] absorbs resonant energy!"),
+ span_userdanger("An intense resonant aura pulses around [ismob(antimagic_source) ? "you" : antimagic_source] as it dissipates into the air!"),
+ )
+ antimagic_effect = mutable_appearance('icons/effects/effects.dmi', "shield-old", MOB_SHIELD_LAYER)
+ antimagic_color = LIGHT_COLOR_DARK_BLUE
+ playsound(src, 'sound/effects/magic/magic_block.ogg', 50, TRUE)
+
+ mob_light(range = 2, power = 2, color = antimagic_color, duration = 5 SECONDS)
+ add_overlay(antimagic_effect)
+ addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, cut_overlay), antimagic_effect), 5 SECONDS)
+
+/*
+Dispel proc handler
+*/
+
+/// Dispel proc that signals the handler, and plays a sound if the handlers returns a success.
+/atom/proc/dispel(atom/dispeller, dispel_flags = 0)
+ var/signal_result = handle_dispel(dispeller, dispel_flags)
+ // SFX that a dispel occurred.
+ if(signal_result)
+ playsound(src, 'sound/effects/magic/smoke.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE)
+ return signal_result
+
+/// Global handler that sends the dispel signal to the item. If the signal returns DISPEL_RESULT_DISPELLED, it will return TRUE. Otherwise not.
+/atom/proc/handle_dispel(atom/dispeller, dispel_flags = 0)
+ var/signal_result = SEND_SIGNAL(src, COMSIG_ATOM_DISPEL, dispeller)
+ return signal_result
+
+/// Mob specific version of handle dispel, with the ability to have a cascade that checks all the mob's personal items.
+/mob/living/handle_dispel(atom/dispeller, dispel_flags = 0)
+ var/signal_result = SEND_SIGNAL(src, COMSIG_ATOM_DISPEL, dispeller)
+ // Only cascade if explicitly requested.
+ if(dispel_flags & DISPEL_CASCADE_CARRIED)
+ for(var/obj/item/held_item in held_items)
+ if(held_item.dispel(dispeller))
+ signal_result = TRUE
+
+ for(var/obj/item/worn_item in get_equipped_items())
+ if(worn_item.dispel(dispeller))
+ signal_result = TRUE
+ return signal_result
+
+/*
+ Adds dispel on hit for the null rod.
+*/
+/obj/item/nullrod/Initialize(mapload)
+ . = ..()
+ AddElement(/datum/element/resonant_dispel_hit, TRUE)
+
+/*
+ Component to make something dispel on smack.
+*/
+
+/datum/element/resonant_dispel_hit
+ element_flags = ELEMENT_BESPOKE
+ argument_hash_start_idx = 2
+ // does it cascade it's dispel (Everything on the target)
+ var/cascade_dispels
+
+/datum/element/resonant_dispel_hit/Attach(datum/target, cascade = FALSE)
+ . = ..()
+ target.AddElementTrait(TRAIT_ON_HIT_EFFECT, REF(src), /datum/element/on_hit_effect)
+ RegisterSignal(target, COMSIG_ON_HIT_EFFECT, PROC_REF(dispel_on_hit))
+ if(cascade)
+ cascade_dispels = TRUE
+
+/datum/element/resonant_dispel_hit/Detach(datum/source)
+ UnregisterSignal(source, COMSIG_ON_HIT_EFFECT)
+ REMOVE_TRAIT(source, TRAIT_ON_HIT_EFFECT, REF(src))
+ return ..()
+
+/// Sends out the dispel signal onto the target hit by the item.
+/datum/element/resonant_dispel_hit/proc/dispel_on_hit(datum/source, atom/attacker, atom/damage_target, hit_zone, throw_hit)
+ SIGNAL_HANDLER
+ damage_target.dispel(attacker, cascade_dispels ? DISPEL_CASCADE_CARRIED : null)
+
+/*
+ Very simple wiz spell to test dispel functionality, plus for admeme purposes.
+*/
+
+/datum/action/cooldown/spell/pointed/resonant_dispel
+ name = "Dispel Resonance"
+ desc = "Ends the weaker, resonance-based magics on the target and anything contained on or within. Doesn't dispel any ADVANCED magic!"
+ button_icon_state = "emp"
+
+ sound = 'sound/effects/magic/disable_tech.ogg'
+ school = SCHOOL_EVOCATION
+ cooldown_time = 10 SECONDS
+ cooldown_reduction_per_rank = 2 SECONDS
+
+ invocation = "WE-AK." // I am glad I did not add invocations to Thaumaturge because my creativity with these would ruin server prop.
+ invocation_type = INVOCATION_SHOUT
+ spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC
+
+ active_msg = "You prepare to dispel a target..."
+
+/datum/action/cooldown/spell/pointed/resonant_dispel/cast(atom/cast_on)
+ . = ..()
+ if(ismob(cast_on))
+ var/mob/living/living_target = cast_on
+ if(living_target.can_block_magic(antimagic_flags))
+ to_chat(owner, span_warning("Your dispel failed to work!"))
+ return FALSE
+
+ cast_on.dispel(owner, DISPEL_CASCADE_CARRIED)
+ return TRUE
diff --git a/modular_doppler/modular_powers/code/powers_helpers.dm b/modular_doppler/modular_powers/code/powers_helpers.dm
new file mode 100644
index 00000000000000..46ee2781d9d64a
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers_helpers.dm
@@ -0,0 +1,18 @@
+
+/// Equation that sorts powers in alphabetical order, with roots in ascending order..
+/proc/cmp_powers_asc(datum/power/first_power, datum/power/second_power)
+ var/first_priority_val = SSpowers.power_priorities.Find(first_power.priority)
+ var/second_priority_val = SSpowers.power_priorities.Find(second_power.priority)
+
+ var/a_name = first_power::name
+ var/b_name = second_power::name
+
+ if(first_priority_val != second_priority_val)
+ // Unknown priorities are always sorted after known priorities.
+ if(!first_priority_val)
+ return 1
+ if(!second_priority_val)
+ return -1
+ return first_priority_val - second_priority_val
+
+ return sorttext(b_name, a_name)
diff --git a/modular_doppler/modular_powers/code/powers_living.dm b/modular_doppler/modular_powers/code/powers_living.dm
new file mode 100644
index 00000000000000..7279de7cb19d8b
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers_living.dm
@@ -0,0 +1,156 @@
+
+/**
+ * All the additional procs/vars we need on /mob/living for powers to function.
+ */
+
+/mob/living
+ /// List of all powers we currently have.
+ var/list/powers = list()
+
+/**
+ * Adds the passed power to the mob
+ *
+ * Arguments
+ * * power_type - Power typepath to add to the mob
+ * * client_source - Client to use for power initialization and preference reads.
+ * * add_unique - TRUE/FALSE that determines if add_unique() gets called on powers.
+ * If not passed, defaults to this mob's client.
+ *
+ * Returns TRUE on success, FALSE on failure (already has the power, etc)
+ */
+/mob/living/proc/add_archetype_power(datum/power/power_type, client/client_source, add_unique = TRUE)
+ if(has_archetype_power(power_type))
+ return FALSE
+ var/qname = initial(power_type.name)
+ if(!SSpowers || !SSpowers.powers[qname])
+ return FALSE
+ var/datum/power/new_power = new power_type()
+ if(new_power.add_to_holder(new_holder = src, client_source = client_source, unique = add_unique))
+ return TRUE
+ qdel(new_power)
+ return FALSE
+
+/**
+ * Removes the passed power from the mob
+ *
+ * Arguments
+ * * power_type - Power typepath to remove from to the mob
+ *
+ * Returns TRUE on success, FALSE on failure (power isnt there)
+ */
+/mob/living/proc/remove_archetype_power(power_type)
+ for(var/datum/power/power in powers)
+ if(power.type == power_type)
+ qdel(power)
+ return TRUE
+ return FALSE
+
+/**
+ * Checks the existence of a power on a mob.
+ *
+ * Arguments
+ * * power_type - Power typepath to check on the mob
+ *
+ * Returns TRUE if its there, FALSE if not.
+ */
+/mob/living/proc/has_archetype_power(power_type)
+ for(var/datum/power/power in powers)
+ if(power.type == power_type)
+ return TRUE
+ return FALSE
+
+/**
+ * Checks whether the mob has any power on a given path.
+ *
+ * Arguments:
+ * * power_path - The path identifier to check against, e.g. POWER_PATH_THAUMATURGE
+ *
+ * Returns TRUE if any owned power matches the path, FALSE otherwise.
+ */
+/mob/living/proc/has_power_in_path(power_path)
+ for(var/datum/power/power in powers)
+ if(power.path == power_path)
+ return TRUE
+ return FALSE
+
+/**
+ * Getter function for a mob's power
+ *
+ * Arguments:
+ * * power_type - the type of the power to acquire e.g. /datum/power/some_power
+ *
+ * Returns the mob's power datum if the mob this is called on has the power, null on failure
+ */
+/mob/living/proc/get_power(power_type)
+ for(var/datum/power/power in powers)
+ if(power.type == power_type)
+ return power
+ return null
+
+/**
+ * get_power_string() is used to get a printable string of all powers this mob has.
+ *
+ * Arguments:
+ * * security - If TRUE, uses each power's security record text. If FALSE, uses the power names.
+ * * category - Which threat categories of powers should be included.
+ * * include_empty_text - If FALSE, returns an empty string when no entries match.
+ */
+/mob/living/proc/get_power_string(security = FALSE, category = CAT_POWER_ALL, include_empty_text = TRUE)
+ var/list/dat = list()
+ for(var/datum/power/candidate as anything in powers)
+ if(security && !candidate.include_in_security_records)
+ continue
+
+ switch(category)
+ if(CAT_POWER_MINOR_THREAT)
+ if(candidate.security_threat != POWER_THREAT_MINOR)
+ continue
+ if(CAT_POWER_MAJOR_THREAT)
+ if(candidate.security_threat != POWER_THREAT_MAJOR)
+ continue
+
+ if(security)
+ var/security_text = candidate.get_security_record_text()
+ if(!isnull(security_text) && security_text != "")
+ dat += security_text
+ else
+ dat += candidate.name
+
+ if(!length(dat))
+ if(!include_empty_text)
+ return ""
+ return security ? "No powers declared." : "None"
+
+ return security ? dat.Join(" ") : dat.Join(", ")
+
+/// Compatibility helper for security record formatting.
+/mob/living/proc/get_sec_power_string(category = CAT_POWER_ALL, include_empty_text = TRUE)
+ return get_power_string(TRUE, category, include_empty_text)
+
+/// Refreshes the sec records when powers are added/removed.
+/mob/living/proc/refresh_security_power_records()
+ var/lookup_name = name
+ if(ishuman(src))
+ var/mob/living/carbon/human/human_self = src
+ lookup_name = human_self.real_name
+
+ var/datum/record/crew/target = find_record(lookup_name)
+ if(!target)
+ return
+
+ target.power_notes = get_sec_power_string(CAT_POWER_ALL)
+ target.power_notes_minor = get_sec_power_string(CAT_POWER_MINOR_THREAT, include_empty_text = FALSE)
+ target.power_notes_major = get_sec_power_string(CAT_POWER_MAJOR_THREAT, include_empty_text = FALSE)
+
+/// Removes all powers from the mob.
+/mob/living/proc/cleanse_power_datums()
+ QDEL_LIST(powers)
+
+/// Removes all powers from a mob and transfers them to the new target instead.
+/mob/living/proc/transfer_power_datums(mob/living/to_mob)
+ // We could be done before the client was moved or after the client was moved
+ var/datum/preferences/to_pass = client || to_mob.client
+
+ for(var/datum/power/power as anything in powers)
+ power.remove_from_current_holder(power_transfer = TRUE)
+ power.add_to_holder(to_mob, power_transfer = TRUE, client_source = to_pass)
diff --git a/modular_doppler/modular_powers/code/powers_prefs.dm b/modular_doppler/modular_powers/code/powers_prefs.dm
new file mode 100644
index 00000000000000..005800cf973a39
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers_prefs.dm
@@ -0,0 +1,63 @@
+
+/**
+ * All the additional procs/vars we need on /datum/preferences for powers to function.
+ */
+
+/datum/preferences
+ /// List of all our powers, by name.
+ var/list/all_powers = list()
+
+/// Clears all powers and related augment assignments.
+/datum/preferences/proc/nuke_powers_prefs(reasons)
+ all_powers = list()
+
+ // This is a bit messy with how augmented is implemented but we can't skip these.
+ if(GLOB.preference_entries[/datum/preference/choiced/augment_left])
+ write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], AUGMENTED_NO_AUGMENT)
+ if(GLOB.preference_entries[/datum/preference/choiced/augment_right])
+ write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], AUGMENTED_NO_AUGMENT)
+
+ // No reason
+ if(!islist(reasons))
+ reasons = isnull(reasons) ? list("unspecified reason") : list(reasons)
+
+ // Have a reason: Logged in the game and told to the user.
+ if(length(reasons))
+ var/list/feedback
+ LAZYADD(feedback, "Your powers were removed because of the following reasons:")
+ LAZYADD(feedback, reasons)
+ if(LAZYLEN(feedback))
+ // This doesn't work if the player joins the game with an invalid file. SAD!
+ to_chat(parent, span_greentext(jointext(feedback, "\n")))
+
+ var/ckey_to_log = parent?.ckey || "unknown"
+ log_game("[ckey_to_log]'s powers preferences were nuked: [jointext(reasons, "; ")]")
+
+ save_character()
+ return TRUE
+
+/// Runs sanitization for powers by going through filter_invalid_poewrs() and also authentication that the save entry is proper.
+/// If sanitization fails, removes all the player's power prefs and returns a message as to why.
+/datum/preferences/proc/sanitize_powers()
+ var/list/new_powers = SSpowers.filter_invalid_powers(all_powers, parent)
+ var/list/powers_removed = SSpowers.powers_removed
+ var/invalid_reason = null
+
+ for(var/power_name in all_powers)
+ if(!istext(power_name) || !ispath(SSpowers.powers[power_name]))
+ invalid_reason = "Invalid power entry: [power_name]"
+ break
+
+ // If filter_invalid_powers came back with removed powers, we apply the changes and give feedback
+ if(invalid_reason)
+ nuke_powers_prefs(invalid_reason)
+ return TRUE
+
+ if(LAZYLEN(powers_removed) && !length(new_powers))
+ nuke_powers_prefs(powers_removed)
+ return TRUE
+
+ if(new_powers.len != all_powers.len)
+ all_powers = new_powers
+ return TRUE
+ return FALSE
diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm
new file mode 100644
index 00000000000000..538f6bc17e6076
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm
@@ -0,0 +1,531 @@
+
+/**
+ * This place is a message... and part of a system of messages... pay attention to it!
+ * Sending this message was important to us. We considered ourselves to be a powerful culture.
+ * This place is not a place of honor... no highly esteemed deed is commemorated here... nothing valued is here.
+ * What is here was dangerous and repulsive to us. This message is a warning about danger.
+ * The danger is in a particular location... it increases towards a center... the center of danger is here... of a particular size and shape, and below us.
+ * The danger is still present, in your time, as it was in ours.
+ * The danger is to the body, and it can kill.
+ * The form of the danger is an emanation of energy.
+ * The danger is unleashed only if you substantially disturb this place physically. This place is best shunned and left uninhabited.
+ */
+
+/datum/preference_middleware/powers
+ action_delegations = list(
+ "give_power" = PROC_REF(give_power),
+ "remove_power" = PROC_REF(remove_power),
+ "set_augment_arm" = PROC_REF(set_augment_arm),
+ )
+
+/datum/preference_middleware/powers/post_set_preference(mob/user, preference, value)
+ preferences.sanitize_powers()
+
+/datum/preference_middleware/powers/get_ui_data(mob/user)
+ var/list/data = list()
+
+ var/list/thaumaturge = list()
+ var/list/enigmatist = list()
+ var/list/theologist = list()
+
+ var/list/psyker = list()
+ var/list/cultivator = list()
+ var/list/aberrant = list()
+
+ var/list/warfighter = list()
+ var/list/expert = list()
+ var/list/augmented = list()
+
+ var/current_points = 0
+ for(var/power_name in preferences.all_powers)
+ var/datum/power/power_type = SSpowers.powers[power_name]
+ if(!ispath(power_type)) // Something is here that shouldn't be here.
+ preferences.nuke_powers_prefs("Invalid power entry detected while building powers UI: [power_name]")
+ return data
+ current_points += power_type.value
+
+ var/datum/species/mob_species = preferences.read_preference(/datum/preference/choiced/species)
+
+ for(var/power_name in SSpowers.powers)
+ var/datum/power/power_type = SSpowers.powers[power_name]
+
+ var/has_given_power = (power_name in preferences.all_powers)
+ var/species_allowed = is_species_appropriate(power_type, mob_species)
+
+ // TODO: GRAY OUT powers you:
+ // Don't have the requirements for.
+ // Have powers building upon.
+ // Have an incompatible power for.
+ // ^ must touch tgui to set a new state/colour for this shit
+
+ var/locked_in = FALSE
+ if(has_given_power)
+ if(get_requiring_power(power_type))
+ locked_in = TRUE
+ else
+ if(!species_allowed || get_incompatible_power(power_type) || length(get_required_power(power_type)) || would_exceed_path_limit(power_type))
+ locked_in = TRUE
+
+ var/state
+ var/word
+ var/color
+ var/powertype
+ var/rootpower = null
+
+ if(power_type.priority == POWER_PRIORITY_ROOT)
+ powertype = "crown"
+ else
+ powertype = ""
+ rootpower = power_type.archetype
+
+ if(has_given_power)
+ word = "Forget"
+ state = "bad"
+ if(locked_in)
+ color = "0.5"
+ else
+ if(locked_in || ((power_type.value + current_points) > MAXIMUM_POWER_POINTS))
+ state = "transparent"
+ word = "N/A"
+ color = "0.5"
+ else
+ state = "good"
+ word = "Learn"
+ color = "1"
+
+ var/augment_info = build_augment_ui_info(power_type, preferences)
+ var/datum/power_constant_data/constant_data = GLOB.all_power_constant_data[power_type]
+ var/list/customization_options = constant_data?.get_customization_data()
+
+ // Gets the powers required per power and adds their names, to display when hovered over.
+ var/list/required_power_types = GLOB.powers_requirements_list[power_type]
+ var/list/required_power_names = list()
+ if(length(required_power_types))
+ for(var/datum/power/required_power_type as anything in required_power_types)
+ var/required_power_name = required_power_type.name
+ // Trims abstract from abstract roots.
+ if(length(required_power_name) >= 9 && lowertext(copytext(required_power_name, 1, 10)) == "abstract ")
+ required_power_name = copytext(required_power_name, 10)
+ required_power_names += required_power_name
+ // Gets special requirements such as allow any and allow subtypes
+ var/required_allow_any = power_type.required_allow_any
+ var/required_allow_subtypes = power_type.required_allow_subtypes
+
+ var/final_list = list(list(
+ "description" = power_type.desc,
+ "name" = power_type.name,
+ "cost" = power_type.value,
+ "has_power" = has_given_power,
+ "state" = state,
+ "word" = word,
+ "color" = color,
+ "powertype" = powertype,
+ "rootpower" = rootpower,
+ "required_powers" = required_power_names,
+ "required_allow_any" = required_allow_any,
+ "required_allow_subtypes" = required_allow_subtypes,
+ "augment" = augment_info,
+ "customizable" = constant_data?.is_customizable(),
+ "customization_options" = customization_options,
+ ))
+
+ switch(power_type.path)
+ if(POWER_PATH_THAUMATURGE)
+ thaumaturge += final_list
+ if(POWER_PATH_ENIGMATIST)
+ enigmatist += final_list
+ if(POWER_PATH_THEOLOGIST)
+ theologist += final_list
+ if(POWER_PATH_PSYKER)
+ psyker += final_list
+ if(POWER_PATH_CULTIVATOR)
+ cultivator += final_list
+ if(POWER_PATH_ABERRANT)
+ aberrant += final_list
+ if(POWER_PATH_WARFIGHTER)
+ warfighter += final_list
+ if(POWER_PATH_EXPERT)
+ expert += final_list
+ if(POWER_PATH_AUGMENTED)
+ augmented += final_list
+
+
+ data["total_power_points"] = MAXIMUM_POWER_POINTS
+ data["thaumaturge"] = thaumaturge
+ data["enigmatist"] = enigmatist
+ data["theologist"] = theologist
+ data["psyker"] = psyker
+ data["cultivator"] = cultivator
+ data["aberrant"] = aberrant
+ data["warfighter"] = warfighter
+ data["expert"] = expert
+ data["augmented"] = augmented
+ data["power_points"] = current_points
+
+ return data
+
+/// Snowflake proc to allow Augments to have their own selectable arm section in the UI.
+/datum/preference_middleware/powers/proc/build_augment_ui_info(
+ datum/power/power_type,
+ datum/preferences/preferences
+)
+ // Snowflake code for Augments: expose arm assignment + location.
+ var/augment_location
+ var/is_arm_augment
+ var/augment_assignment
+ var/arm_left_blocked
+ var/arm_right_blocked
+ if(ispath(power_type, /datum/power/augmented))
+ var/datum/power/augmented/power_instance = new power_type
+ augment_location = power_instance.get_augment_location_label()
+ is_arm_augment = (augment_location == "Arms")
+ qdel(power_instance)
+ if(is_arm_augment)
+ var/augment_left = preferences.read_preference(/datum/preference/choiced/augment_left)
+ var/augment_right = preferences.read_preference(/datum/preference/choiced/augment_right)
+ arm_left_blocked = (augment_left && augment_left != AUGMENTED_NO_AUGMENT && augment_left != power_type.name)
+ arm_right_blocked = (augment_right && augment_right != AUGMENTED_NO_AUGMENT && augment_right != power_type.name)
+ if(augment_left == power_type.name && augment_right == power_type.name)
+ augment_assignment = "Both"
+ else if(augment_left == power_type.name)
+ augment_assignment = "Left"
+ else if(augment_right == power_type.name)
+ augment_assignment = "Right"
+ return list(
+ "location" = augment_location,
+ "is_arm" = is_arm_augment,
+ "assignment" = augment_assignment,
+ "left_blocked" = arm_left_blocked,
+ "right_blocked" = arm_right_blocked,
+ )
+ return null
+
+/**
+ * Gives a power to a character using the params list provided by tgui.
+ * Runs through multiple checks to ensure that the power can be learned.
+ */
+/datum/preference_middleware/powers/proc/give_power(list/params, mob/user)
+ var/power_name = params["power_name"]
+ var/datum/power/power_type = SSpowers.powers[power_name]
+ if(isnull(preferences.all_powers))
+ preferences.all_powers = list()
+
+ if(isnull(power_type))
+ return FALSE // Not a power.
+
+ if(power_name in preferences.all_powers)
+ return FALSE // Already have this power.
+
+ // Cehcks against the species blacklist.
+ var/datum/species/mob_species = preferences.read_preference(/datum/preference/choiced/species)
+ if(!is_species_appropriate(power_type, mob_species))
+ to_chat(user, span_boldwarning("[power_name] is not available to your species!"))
+ return FALSE
+
+ // Make sure we don't exceed 2 distinct paths.
+ if(length(preferences.all_powers))
+ var/list/unique_paths = list()
+ // Collect the distinct paths the player already has
+ for(var/power_key in preferences.all_powers)
+ var/datum/power/existing_power = SSpowers.powers[power_key]
+ if(!existing_power)
+ continue
+ unique_paths[existing_power.path] = TRUE
+ // If the new power's path isn't already present, it would add a new path
+ if(!(power_type.path in unique_paths) && length(unique_paths) >= 2)
+ to_chat(user, span_boldwarning("You can only have powers from two paths!"))
+ return FALSE
+
+ // Make sure we have the required powers.
+ var/list/missing_required_powers = get_required_power(power_type)
+ if(length(missing_required_powers))
+ var/list/required_names = list()
+ for(var/datum/power/required_option as anything in missing_required_powers)
+ required_names += required_option.name
+ if(power_type.required_allow_any)
+ to_chat(user, span_boldwarning("[power_name] requires any of: [english_list(required_names)]!"))
+ else
+ to_chat(user, span_boldwarning("[power_name] requires: [english_list(required_names)]!"))
+ return FALSE
+
+ // Make sure we don't select an incompatible power.
+ var/datum/power/incompatible_power_type = get_incompatible_power(power_type)
+ if(incompatible_power_type)
+ to_chat(user, span_boldwarning("[power_name] is incompatible with [incompatible_power_type.name]!"))
+ return FALSE
+
+ // Make sure we don't go over our point cap.
+ var/point_balance = power_type.value
+ for(var/existing_power_name in preferences.all_powers)
+ var/datum/power/existing_power_type = SSpowers.powers[existing_power_name]
+ point_balance += existing_power_type.value
+ if(point_balance > MAXIMUM_POWER_POINTS)
+ to_chat(user, span_boldwarning("[power_name] costs too much!"))
+ return FALSE
+
+ // Augmented specific validation.
+ if(!validate_augment(power_type, power_name, user))
+ return FALSE
+
+ preferences.all_powers += power_name
+ return TRUE
+
+/// If a power is able to be selected for the mob's species.
+/datum/preference_middleware/powers/proc/is_species_appropriate(datum/power/power_type, datum/species/mob_species)
+ if(isnull(mob_species))
+ return TRUE
+ // Gets the power from the power_species_restriction global list if its in there.
+ var/list/species_restrictions = GLOB.powers_species_restrictions[power_type]
+ if(!islist(species_restrictions)) // not in there? cool skip this step.
+ return TRUE
+ var/list/species_blacklist = species_restrictions["list"]
+ var/is_whitelist = species_restrictions["whitelist"]
+ if(!islist(species_blacklist) || !species_blacklist.len)
+ return TRUE
+ var/is_listed = (mob_species in species_blacklist)
+ // whitelist inverts
+ if(is_whitelist)
+ return is_listed
+ // if its in there, yes/no.
+ return !is_listed
+
+/// A lot of validation specifically for augmented, given they're very snowflakey in their restrictions.
+/datum/preference_middleware/powers/proc/validate_augment(datum/power/power_type, power_name, mob/user)
+ if(!ispath(power_type, /datum/power/augmented))
+ return TRUE
+
+ var/datum/power/augmented/power_instance = new power_type
+ var/augment_location = power_instance.get_augment_location_label()
+ qdel(power_instance)
+ if(augment_location == "Arms") // Arm augment validation + auto-assign missing arm.
+ var/augment_left = preferences.read_preference(/datum/preference/choiced/augment_left)
+ var/augment_right = preferences.read_preference(/datum/preference/choiced/augment_right)
+ var/left_taken = (augment_left && augment_left != AUGMENTED_NO_AUGMENT && augment_left != power_name)
+ var/right_taken = (augment_right && augment_right != AUGMENTED_NO_AUGMENT && augment_right != power_name)
+ if(left_taken && right_taken)
+ to_chat(user, span_boldwarning("Both arms already have augments assigned."))
+ return FALSE
+ if(!right_taken)
+ to_chat(user, span_notice("[power_name] will be assigned to your right arm."))
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], power_name)
+ else if(!left_taken)
+ to_chat(user, span_notice("[power_name] will be assigned to your left arm."))
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], power_name)
+ else // Non-arm validation; just goes off of slots and looks if there's any others.
+ var/obj/item/organ/new_augment_path = initial(power_instance.augment)
+ if(new_augment_path)
+ var/new_slot = initial(new_augment_path.slot)
+ for(var/existing_power_name in preferences.all_powers)
+ var/datum/power/augmented/existing_power_type = SSpowers.powers[existing_power_name]
+ if(!ispath(existing_power_type, /datum/power/augmented))
+ continue
+ var/obj/item/organ/existing_augment_path = initial(existing_power_type.augment)
+ if(!existing_augment_path)
+ continue
+ var/existing_slot = initial(existing_augment_path.slot)
+ if(existing_slot && existing_slot == new_slot)
+ to_chat(user, span_boldwarning("[power_name] conflicts with [existing_power_name] (same organ slot)."))
+ return FALSE
+
+ return TRUE
+
+/**
+ * Remove Power
+ *
+ * Removes a power from a character using the params list provided by tgui.
+ */
+/datum/preference_middleware/powers/proc/remove_power(list/params, mob/user)
+ var/power_name = params["power_name"]
+ var/datum/power/power_type = SSpowers.powers[power_name]
+ if(isnull(preferences.all_powers))
+ preferences.all_powers = list()
+ return FALSE // We don't have any powers.
+
+ if(isnull(power_type))
+ return FALSE // Not a power.
+
+ if(!(power_name in preferences.all_powers))
+ return FALSE // We don't have this power.
+
+ // Make sure none of our other powers need this power.
+
+ var/datum/power/requiring_power_type = get_requiring_power(power_type)
+ if(requiring_power_type)
+ to_chat(user, span_boldwarning("[power_name] is needed by [requiring_power_type.name]!"))
+ return FALSE
+
+ preferences.all_powers -= power_name
+ if(ispath(power_type, /datum/power/augmented))
+ var/augment_left = preferences.read_preference(/datum/preference/choiced/augment_left)
+ var/augment_right = preferences.read_preference(/datum/preference/choiced/augment_right)
+ if(augment_left == power_name)
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], AUGMENTED_NO_AUGMENT)
+ if(augment_right == power_name)
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], AUGMENTED_NO_AUGMENT)
+ return TRUE
+
+/**
+ * Assign an arm augment to left/right/both for the global arm loadout.
+ */
+/datum/preference_middleware/powers/proc/set_augment_arm(list/params, mob/user)
+ var/power_name = params["power_name"]
+ var/side = params["side"]
+ var/datum/power/power_type = SSpowers.powers[power_name]
+ if(isnull(power_type))
+ return FALSE
+ if(!(power_name in preferences.all_powers))
+ to_chat(user, span_boldwarning("You must learn [power_name] before assigning it to an arm."))
+ return FALSE
+ if(!ispath(power_type, /datum/power/augmented))
+ return FALSE
+
+ // Verify arm augment
+ var/datum/power/augmented/power_instance = new power_type
+ var/augment_location = power_instance.get_augment_location_label()
+ qdel(power_instance)
+ if(augment_location != "Arms")
+ to_chat(user, span_boldwarning("[power_name] is not an arm augment."))
+ return FALSE
+
+ var/augment_left = preferences.read_preference(/datum/preference/choiced/augment_left)
+ var/augment_right = preferences.read_preference(/datum/preference/choiced/augment_right)
+ var/left_blocked = (augment_left && augment_left != AUGMENTED_NO_AUGMENT && augment_left != power_name)
+ var/right_blocked = (augment_right && augment_right != AUGMENTED_NO_AUGMENT && augment_right != power_name)
+
+ var/side_lower = lowertext(side)
+ if(side_lower == "left")
+ if(left_blocked)
+ to_chat(user, span_boldwarning("Your left arm already has an augment assigned."))
+ return FALSE
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], power_name)
+ if(augment_right == power_name)
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], AUGMENTED_NO_AUGMENT)
+ else if(side_lower == "right")
+ if(right_blocked)
+ to_chat(user, span_boldwarning("Your right arm already has an augment assigned."))
+ return FALSE
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], power_name)
+ if(augment_left == power_name)
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], AUGMENTED_NO_AUGMENT)
+ else if(side_lower == "both")
+ if(left_blocked || right_blocked)
+ to_chat(user, span_boldwarning("Both arms must be free to assign this augment to both."))
+ return FALSE
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], power_name)
+ preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], power_name)
+ else
+ to_chat(user, span_boldwarning("Invalid arm selection."))
+ return FALSE
+
+ return TRUE
+
+/**
+ * Checks whether we are missing required powers for a given power type.
+ * Returns a list of missing requirements (empty if satisfied).
+ * If required_allow_any is TRUE, the list contains all valid options when none are satisfied.
+ */
+/datum/preference_middleware/powers/proc/get_required_power(datum/power/power_type)
+ var/list/required_powers = GLOB.powers_requirements_list[power_type]
+ if(!length(required_powers))
+ return list()
+
+ var/allow_any = power_type.required_allow_any
+ var/allow_subtypes = power_type.required_allow_subtypes
+ var/list/missing_required = list()
+
+ for(var/datum/power/required_power_type as anything in required_powers)
+ var/required_power_name = required_power_type.name
+
+ // Exact requirement satisfied
+ if(required_power_name in preferences.all_powers)
+ if(allow_any)
+ return list()
+ continue
+
+ // Optional: allow subtypes, decided by the power we're trying to learn
+ if(allow_subtypes)
+ var/required_typepath = ispath(required_power_type) ? required_power_type : required_power_type.type
+ var/found_subtype = FALSE
+
+ for(var/selected_power_name in preferences.all_powers)
+ var/datum/power/selected_power_type = SSpowers.powers[selected_power_name]
+ if(!selected_power_type)
+ continue
+
+ if(ispath(selected_power_type.type, required_typepath))
+ found_subtype = TRUE
+ break
+
+ if(found_subtype)
+ if(allow_any)
+ return list()
+ continue
+
+ if(!allow_any)
+ missing_required += required_power_type
+
+ if(allow_any)
+ return required_powers
+
+ return missing_required
+
+
+/**
+ * Checks whether at least one of our powers requires the given power type,
+ * and returns the first one encountered if so.
+ */
+/datum/preference_middleware/powers/proc/get_requiring_power(datum/power/power_type)
+ var/list/powers_requiring_this = GLOB.powers_inverse_requirements_list[power_type]
+ if(!length(powers_requiring_this))
+ return
+ for(var/datum/power/requiring_power_type as anything in powers_requiring_this)
+ if(requiring_power_type.name in preferences.all_powers)
+ return requiring_power_type
+
+/**
+ * Checks whether a given power type is incompatible with our selected powers,
+ * and returns the first one encountered if so.
+ */
+/datum/preference_middleware/powers/proc/get_incompatible_power(datum/power/power_type)
+ // checks for blacklist
+ for(var/list/blacklist as anything in GLOB.powers_blacklist)
+ if(!(power_type in blacklist))
+ continue
+ for(var/datum/power/other_power_type as anything in blacklist)
+ if(other_power_type.name in preferences.all_powers)
+ return other_power_type
+ // checks for multiple roots of same path
+ if(power_type.priority == POWER_PRIORITY_ROOT)
+ for(var/existing_power_name in preferences.all_powers)
+ var/datum/power/existing_power_type = SSpowers.powers[existing_power_name]
+ if(!existing_power_type)
+ continue
+ if(existing_power_type.priority == POWER_PRIORITY_ROOT && existing_power_type.path == power_type.path)
+ return existing_power_type
+
+/**
+ * Returns TRUE if selecting power_type would exceed the 2-path limit.
+ */
+/datum/preference_middleware/powers/proc/would_exceed_path_limit(datum/power/power_type)
+ var/list/unique_paths = list()
+ for(var/existing_power_name in preferences.all_powers)
+ var/datum/power/existing_power_type = SSpowers.powers[existing_power_name]
+ if(!existing_power_type)
+ continue
+ unique_paths[existing_power_type.path] = TRUE
+
+ // If this power adds a third distinct path, block it.
+ if(!(power_type.path in unique_paths) && length(unique_paths) >= 2)
+ return TRUE
+
+/datum/asset/simple/powers
+ assets = list(
+ "gear.png" = 'modular_doppler/modular_powers/icons/ui/powers/gear.png',
+ "heart.png" = 'modular_doppler/modular_powers/icons/ui/powers/heart.png',
+ "seal.png" = 'modular_doppler/modular_powers/icons/ui/powers/seal.png'
+ )
+
+/datum/preference_middleware/powers/get_ui_assets()
+ return list(
+ get_asset_datum(/datum/asset/simple/powers),
+ )
diff --git a/modular_doppler/modular_powers/code/powers_status_effect.dm b/modular_doppler/modular_powers/code/powers_status_effect.dm
new file mode 100644
index 00000000000000..1cf3bd591ba5ea
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers_status_effect.dm
@@ -0,0 +1,4 @@
+// base status effect type, just for the unit test. Might do something with this later.
+/datum/status_effect/power
+ id = "power_abstract"
+ alert_type = null
diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm
new file mode 100644
index 00000000000000..c5de8a417b214b
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers_subsystem.dm
@@ -0,0 +1,288 @@
+
+// These lists are shifted to glob so they are generated at world start instead of risking players doing preference stuff before the subsystem inits.
+/// Glob Blacklist that blocks specific combinations of powers.
+GLOBAL_LIST_INIT_TYPED(powers_blacklist, /list/datum/power, list(
+ list(/datum/power/aberrant/shapechange_spider, /datum/power/aberrant/shapechange_wolf),
+))
+
+/// Glob list of what powers require what other powers. Format is power -> required power
+GLOBAL_LIST_INIT(powers_requirements_list, generate_powers_requirements_list())
+
+/// Glob list of the parent power that is required by certain powers. Format is required power -> power
+GLOBAL_LIST_INIT(powers_inverse_requirements_list, generate_powers_inverse_requirements_list())
+
+/// Glob list of powers that have species restrictions.
+GLOBAL_LIST_INIT(powers_species_restrictions, generate_powers_species_restrictions())
+
+/// Gets a power and all their requirements and adds it to the requirements list.
+/proc/generate_powers_requirements_list()
+ var/list/requirements_list = list()
+ var/list/all_powers_list = subtypesof(/datum/power)
+
+ for(var/datum/power/power_type as anything in all_powers_list)
+ if(power_type.abstract_parent_type == power_type)
+ continue
+ var/datum/power/power_instance = new power_type
+ if(!length(power_instance.required_powers))
+ continue
+ for(var/datum/power/required_power_type as anything in power_instance.required_powers)
+ LAZYADDASSOCLIST(requirements_list, power_type, required_power_type)
+ qdel(power_instance)
+
+ return requirements_list
+
+/// Gets a power and all their requirements and adds it to the inverted requirements list.
+/// The inverted list is in essence the same table as powers_requirements_list, just with the columns inverted.
+/proc/generate_powers_inverse_requirements_list()
+ var/list/inverse_requirements_list = list()
+ var/list/all_powers_list = subtypesof(/datum/power)
+
+ for(var/datum/power/power_type as anything in all_powers_list)
+ if(power_type.abstract_parent_type == power_type)
+ continue
+ var/datum/power/power_instance = new power_type
+ if(!length(power_instance.required_powers))
+ continue
+ for(var/datum/power/required_power_type as anything in power_instance.required_powers)
+ LAZYADDASSOCLIST(inverse_requirements_list, required_power_type, power_type)
+ qdel(power_instance)
+
+ return inverse_requirements_list
+
+/// Gets all the powers that have a species blacklist.
+/proc/generate_powers_species_restrictions()
+ var/list/restrictions = list()
+ for(var/datum/power/power_type as anything in subtypesof(/datum/power))
+ if(initial(power_type.abstract_parent_type) == power_type)
+ continue
+ var/datum/power/power_instance = new power_type
+ if(islist(power_instance.species_blacklist) && power_instance.species_blacklist.len)
+ restrictions[power_type] = list(
+ "list" = power_instance.species_blacklist,
+ "whitelist" = power_instance.species_blacklist_is_whitelist,
+ )
+ qdel(power_instance)
+ return restrictions
+
+
+//Used to process and handle roundstart powers
+// - Power strings are used for faster checking in code
+// - Power datums are stored and hold different effects, as well as being a vector for applying trait string
+PROCESSING_SUBSYSTEM_DEF(powers)
+ name = "Powers"
+ flags = SS_BACKGROUND
+ runlevels = RUNLEVEL_GAME
+ wait = 1 SECONDS
+
+ /// Whether newly spawned mobs should receive preference-selected powers this round.
+ var/spawn_powers_enabled = TRUE
+ /// Assoc. list of all roundstart power datum types; "name" = /path/
+ var/list/powers = list()
+ /// List of all power priorities in order.
+ var/list/power_priorities = list(
+ POWER_PRIORITY_ROOT,
+ POWER_PRIORITY_BASIC,
+ POWER_PRIORITY_ADVANCED,
+ )
+ /// List of powers removed from players by the powers sanitization.
+ var/list/powers_removed
+
+/datum/controller/subsystem/processing/powers/Initialize()
+ get_powers()
+ return SS_INIT_SUCCESS
+
+/// Returns the list of possible powers
+/datum/controller/subsystem/processing/powers/proc/get_powers()
+ RETURN_TYPE(/list)
+ if(!powers.len)
+ setup_powers()
+
+ return powers
+
+/// Calls the sorting alghorithm and sorts powers alphabetically.
+/datum/controller/subsystem/processing/powers/proc/setup_powers()
+ // Sort by priority from Root to Advanced, and then by name
+ var/list/powers_list = sort_list(subtypesof(/datum/power), GLOBAL_PROC_REF(cmp_powers_asc))
+
+ for(var/datum/power/power_type as anything in powers_list)
+ if(initial(power_type.abstract_parent_type) == power_type)
+ continue
+ powers[initial(power_type.name)] = power_type
+
+/// Assigns all powers in the player's preferences onto the mob.
+/datum/controller/subsystem/processing/powers/proc/assign_powers(mob/living/user, client/applied_client)
+ // No powers are given if the admins have turned on power spawning.
+ if(!spawn_powers_enabled)
+ return
+
+ var/bad_power = FALSE
+ var/list/powers_by_priority = list()
+ for(var/power_name in applied_client.prefs.all_powers)
+ var/datum/power/power_type = powers[power_name]
+ if(!ispath(power_type))
+ stack_trace("Invalid power \"[power_name]\" in client [applied_client.ckey] preferences")
+ applied_client.prefs.all_powers -= power_name
+ bad_power = TRUE
+ continue
+ if(!power_type.priority)
+ stack_trace("Power with invalid priority \"[power_name]\" in client [applied_client.ckey] preferences")
+ applied_client.prefs.all_powers -= power_name
+ bad_power = TRUE
+ continue
+ LAZYADDASSOCLIST(powers_by_priority, power_type.priority, power_type)
+
+ if(bad_power)
+ applied_client.prefs.save_character()
+
+ for(var/power_priority in power_priorities)
+ var/list/priority_powers = powers_by_priority[power_priority]
+ if(isnull(priority_powers))
+ continue
+ for(var/datum/power/power_type as anything in priority_powers)
+ if(!user.add_archetype_power(power_type, client_source = applied_client))
+ continue
+ SSblackbox.record_feedback("tally", "powers_taken", 1, "[power_type.name]")
+
+/// Takes a list of power names,
+/// and returns a new list of powers that would be valid.
+/// If no changes need to be made, will return the same list.
+/// Expects all power names to be unique, but makes no other expectations.
+/datum/controller/subsystem/processing/powers/proc/filter_invalid_powers(list/powers_to_check, client/applied_client)
+ powers_removed = list()
+ var/current_balance = 0
+ var/maximum_balance = MAXIMUM_POWER_POINTS
+ var/list/intermediary_powers = list()
+ var/list/all_powers = get_powers()
+ var/datum/species/mob_species = applied_client?.prefs?.read_preference(/datum/preference/choiced/species)
+
+ // Track distinct paths we accept while filtering this batch
+ var/list/unique_paths = list()
+
+ // Track distinct roots we accept.
+ var/list/root_by_path = list()
+
+ for(var/power_name in powers_to_check)
+ var/datum/power/power_type = all_powers[power_name]
+ if(!ispath(power_type))
+ continue
+
+ // Checks against hte power's species blacklist.
+ if(!isnull(mob_species) && !is_species_appropriate(power_type, mob_species))
+ LAZYADD(powers_removed, "[power_name] is not available to your species.")
+ continue
+
+ // Checks if the power exceeds the max.
+ current_balance += power_type.value
+ if(current_balance > maximum_balance)
+ LAZYADD(powers_removed, "Power point limit exceeded.")
+ return list()
+
+ // Make sure we only have up to two distinct paths.
+ if(!(power_type.path in unique_paths))
+ if(length(unique_paths) >= 2)
+ continue // Third distinct path, discard.
+ unique_paths[power_type.path] = TRUE
+
+ // Block multiple root powers on the same path
+ if(power_type.priority == POWER_PRIORITY_ROOT)
+ if(root_by_path[power_type.path])
+ continue // another root of this path already accepted
+ root_by_path[power_type.path] = power_type
+
+ // Make sure we don't have incompatible powers
+ var/blacklisted = FALSE
+ for(var/list/blacklist as anything in GLOB.powers_blacklist)
+ if(!(power_type in blacklist))
+ continue
+ for(var/other_power in blacklist)
+ if(other_power in intermediary_powers)
+ blacklisted = TRUE
+ break
+ if(blacklisted)
+ break
+ if(blacklisted)
+ continue // Incompatible, discard.
+
+ // Succes = add power
+ intermediary_powers += power_name
+
+ // Build a set of selected power types.
+ var/list/selected_types = list()
+ for(var/power_name in intermediary_powers)
+ var/datum/power/power_type = all_powers[power_name]
+ selected_types[power_type] = TRUE
+
+ // If ANY selected power is missing ANY requirement, nuke the entire list.
+ for(var/power_name in intermediary_powers)
+ var/datum/power/power_type = all_powers[power_name]
+ var/list/required = GLOB.powers_requirements_list[power_type]
+ if(!length(required))
+ continue
+
+ var/allow_any = power_type.required_allow_any
+ var/allow_subtypes = power_type.required_allow_subtypes
+ var/any_satisfied = FALSE
+
+ for(var/datum/power/req_type as anything in required)
+ // Exact requirement satisfied
+ if(selected_types[req_type])
+ any_satisfied = TRUE
+ if(allow_any) // check to end early if any requirements are validated and allow_any is true.
+ break
+ continue
+
+ // Optional: allow subtypes
+ if(allow_subtypes)
+ var/required_typepath = ispath(req_type) ? req_type : req_type.type
+ for(var/datum/power/selected_type as anything in selected_types)
+ if(ispath(selected_type, required_typepath))
+ any_satisfied = TRUE
+ break
+
+ if(any_satisfied) // check to end early if any requirements are validated and allow_any is true.
+ if(allow_any)
+ break
+ continue
+
+ // If we require all, any missing invalidates.
+ if(!allow_any)
+ LAZYADD(powers_removed, "[power_name]\" requires [req_type], which was not present.")
+ return list()
+
+ // If we require one and we don't have any.
+ if(allow_any && !any_satisfied)
+ LAZYADD(powers_removed, "[power_name]\" requires any of [required], none were present.")
+ return list()
+
+ // Everything is fine = return as normal
+ if(intermediary_powers.len == powers_to_check.len)
+ return powers_to_check
+ return intermediary_powers
+
+/// If a power is able to be selected for the mob's species.
+/datum/controller/subsystem/processing/powers/proc/is_species_appropriate(datum/power/power_type, datum/species/mob_species)
+ if(isnull(mob_species))
+ return TRUE
+ // Gets the power from the power_species_restriction global list if its in there.
+ var/list/species_restrictions = GLOB.powers_species_restrictions[power_type]
+ if(!islist(species_restrictions)) // not in there? cool skip this step.
+ return TRUE
+ var/list/species_blacklist = species_restrictions["list"]
+ var/is_whitelist = species_restrictions["whitelist"]
+ if(!islist(species_blacklist) || !species_blacklist.len)
+ return TRUE
+ var/is_listed = (mob_species in species_blacklist)
+ // whitelist inverts
+ if(is_whitelist)
+ return is_listed
+ // if its in there, yes/no.
+ return !is_listed
+
+/// Admin verb that disables players from getting powers on spawn based on their prefs. This in essence prevents powers application except through VV.
+ADMIN_VERB(toggle_spawn_powers, R_ADMIN, "Toggle Spawn with Powers", "Toggles whether newly spawned players receive powers from their preferences this round.", ADMIN_CATEGORY_GAME)
+ SSpowers.spawn_powers_enabled = !SSpowers.spawn_powers_enabled
+
+ to_chat(user, span_adminnotice("Newly spawned players will [SSpowers.spawn_powers_enabled ? "now" : "no longer"] receive powers this round."), confidential = TRUE)
+ message_admins(span_adminnotice("[key_name_admin(user)] has toggled spawn power assignment [SSpowers.spawn_powers_enabled ? "ON" : "OFF"] for this round."))
+ log_admin("[key_name(user)] toggled spawn power assignment [SSpowers.spawn_powers_enabled ? "ON" : "OFF"] for this round.")
+ SSblackbox.record_feedback("nested tally", "admin_toggle", 1, list("Toggle Spawn Powers", "[SSpowers.spawn_powers_enabled ? "Enabled" : "Disabled"]"))
diff --git a/modular_doppler/modular_powers/code/powers_vv.dm b/modular_doppler/modular_powers/code/powers_vv.dm
new file mode 100644
index 00000000000000..16f2066d4f51d3
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers_vv.dm
@@ -0,0 +1,90 @@
+/*
+ We hook into the process normally located in human.dm for adding verbs.
+ This is all extremely similar to how quirks does it.
+*/
+
+// Adds it to the list of dropdowns
+/mob/living/carbon/human/vv_get_dropdown()
+ . = ..()
+ VV_DROPDOWN_OPTION(VV_HK_MOD_POWERS, "Add/Remove Powers")
+
+// Adds the actual verb that gets executed when selected.
+/mob/living/carbon/human/vv_do_topic(list/href_list)
+ . = ..()
+ if(href_list[VV_HK_MOD_POWERS])
+ if(!check_rights(R_SPAWN))
+ return
+ var/list/options = list("Clear"="Clear")
+ for(var/listedpower in subtypesof(/datum/power))
+ var/datum/power/power = listedpower
+ var/name = initial(power.name)
+ options[src.has_power(power) ? "[name] (Remove)" : "[name] (Add)"] = power
+ var/result = input(usr, "Choose power to add/remove","Power Mod") as null|anything in sort_list(options)
+ if(result)
+ if(result == "Clear")
+ for(var/datum/power/toberemoved in powers)
+ remove_power(toberemoved.type)
+ else
+ var/chosen = options[result]
+ if(has_power(chosen))
+ remove_power(chosen)
+ else
+ // Choice menu for augmented, specifically arms (again) that lets you dictate which arm it goes on.
+ var/list/power_init_vars
+ if(ispath(chosen, /datum/power/augmented))
+ var/datum/power/augmented/aug_type = chosen
+ var/obj/item/organ/augment_path = initial(aug_type.augment)
+ if(augment_path)
+ var/zone = initial(augment_path.zone)
+ if(zone in GLOB.arm_zones)
+ var/arm_choice = input(usr, "Install this augment on which arm?", "Arm Selection") as null|anything in list("Left", "Right", "Both", "Cancel")
+ if(!arm_choice || arm_choice == "Cancel")
+ return
+ var/arm_override = AUGMENTED_ARM_USE_PREFS
+ switch(arm_choice)
+ if("Left")
+ arm_override = AUGMENTED_ARM_LEFT
+ if("Right")
+ arm_override = AUGMENTED_ARM_RIGHT
+ if("Both")
+ arm_override = AUGMENTED_ARM_BOTH
+ power_init_vars = list("arm_override" = arm_override)
+ // Add to sec records + adds power
+ var/include_in_security_records = (alert(usr, "Also include this power in security records?", "Power Mod", "No", "Yes") == "Yes")
+ add_power(chosen, include_in_security_records = include_in_security_records, power_init_vars = power_init_vars)
+
+/// Checks if a power is on the selected target
+/mob/living/carbon/proc/has_power(powertype)
+ for(var/datum/power/power in powers)
+ if(power.type == powertype)
+ return TRUE
+ return FALSE
+
+/// Adds a power by calling the power subsystem.
+/mob/living/carbon/proc/add_power(datum/power/powertype, power_transfer = FALSE, client/client_source, unique = TRUE, include_in_security_records = TRUE, list/power_init_vars)
+ if(has_power(powertype))
+ return FALSE
+ var/pname = initial(powertype.name)
+ if(!SSpowers || !SSpowers.powers[pname])
+ return FALSE
+ var/datum/power/power = new powertype()
+ if(islist(power_init_vars))
+ for(var/varname in power_init_vars)
+ if(varname in power.vars)
+ power.vars[varname] = power_init_vars[varname]
+ power.include_in_security_records = include_in_security_records
+ if(!power.add_to_holder(new_holder = src, power_transfer = power_transfer, client_source = client_source, unique = unique))
+ qdel(power)
+ return FALSE
+ refresh_security_power_records()
+ return TRUE
+
+/// Removes a power.
+/mob/living/carbon/proc/remove_power(powertype)
+ for(var/datum/power/power in powers)
+ if(power.type != powertype)
+ continue
+ qdel(power)
+ refresh_security_power_records()
+ return TRUE
+ return FALSE
diff --git a/modular_doppler/modular_powers/code/security/reality_anchor.dm b/modular_doppler/modular_powers/code/security/reality_anchor.dm
new file mode 100644
index 00000000000000..8015c0d383f4a8
--- /dev/null
+++ b/modular_doppler/modular_powers/code/security/reality_anchor.dm
@@ -0,0 +1,129 @@
+/obj/structure/reality_anchor
+ name = "miniature reality anchor"
+ desc = "The chiseled out Eschatite remains of an anchor, smoothed and cobbled together. Crude machinery is managing to keep it docile; but when enabled, it will start enforcing normality back in a large area around it."
+ icon = 'modular_doppler/modular_powers/icons/items/reality_anchor.dmi'
+ icon_state = "reality_anchor"
+ density = TRUE
+ max_integrity = 600 // tonky
+
+ /// Is it on/off
+ var/active = FALSE
+
+ /// Pulse interval
+ var/pulse_interval = 6 SECONDS
+ /// Time until the next pulse
+ var/next_pulse_time = 0
+
+ /// Range in turfs
+ var/pulse_range = 6
+
+ /// Ripple filter while active.
+ var/ripple_filter_id = "reality_anchor_ripple"
+
+/obj/structure/reality_anchor/Destroy()
+ STOP_PROCESSING(SSobj, src)
+ apply_ripple_filter(FALSE)
+ . = ..()
+
+// Turns the thing on or off after the do_after.
+/obj/structure/reality_anchor/attack_hand(mob/user, list/modifiers)
+ . = ..()
+ if(.)
+ return
+ var/action_word = active ? "deactivate" : "activate"
+ var/action_word_past_tense = active ? "deactivating" : "activating"
+ user.visible_message(
+ span_warning("[user] begins to [action_word] the reality anchor..."),
+ span_warning("You begin to [action_word] the reality anchor...")
+ )
+ if(!do_after(user, 3 SECONDS, target = src))
+ return
+ user.visible_message(
+ span_warning("[user] finishes [action_word_past_tense] the reality anchor."),
+ span_warning("You finish [action_word_past_tense] the reality anchor.")
+ )
+ toggle_anchor(user)
+
+/// Switches it on or off.
+/obj/structure/reality_anchor/proc/toggle_anchor(mob/user)
+ active = !active
+ if(active)
+ anchored = TRUE
+ apply_ripple_filter(TRUE)
+ playsound(src, 'sound/effects/magic/repulse.ogg', 75, TRUE)
+ pulse()
+ next_pulse_time = world.time + pulse_interval
+ START_PROCESSING(SSobj, src)
+ return
+ anchored = FALSE
+ apply_ripple_filter(FALSE)
+ STOP_PROCESSING(SSobj, src)
+
+// Countdown til dispel pulse.
+/obj/structure/reality_anchor/process(seconds_per_tick)
+ if(!active)
+ return
+ if(world.time < next_pulse_time)
+ return
+ pulse()
+ next_pulse_time = world.time + pulse_interval
+
+/// Dispel AoE effect.
+/obj/structure/reality_anchor/proc/pulse()
+ var/turf/center = get_turf(src)
+ if(!center)
+ return
+ var/obj/effect/temp_visual/circle_wave/reality_anchor/pulse_fx = new(center)
+ pulse_fx.amount_to_scale = pulse_range + 2 // falls short without the +1
+ // We get EVERYTHING in range and dispel it. This shouldn't be too much of a lag-machine (hopefully)
+ for(var/atom/movable/target in range(pulse_range, center))
+ if(ismob(target))
+ var/mob/living/living_target = target
+ living_target.dispel(src, DISPEL_CASCADE_CARRIED)
+ living_target.apply_status_effect(/datum/status_effect/power/reality_anchor_silenced)
+ else if(isobj(target))
+ target.dispel(src)
+
+/// Applies a rippling effect.
+/obj/structure/reality_anchor/proc/apply_ripple_filter(active_state)
+ if(active_state)
+ add_filter(ripple_filter_id, 2, list("type" = "ripple", "flags" = WAVE_BOUNDED, "radius" = 0, "size" = 2))
+ var/filter = get_filter(ripple_filter_id)
+ if(filter)
+ animate(filter, radius = 0, time = 0.2 SECONDS, size = 2, easing = JUMP_EASING, loop = -1, flags = ANIMATION_PARALLEL)
+ animate(radius = 32, time = 1.5 SECONDS, size = 0)
+ return
+ remove_filter(ripple_filter_id)
+
+// Status effect responsible for silencing.
+/datum/status_effect/power/reality_anchor_silenced
+ id = "reality_anchor_silenced"
+ status_type = STATUS_EFFECT_REFRESH
+ alert_type = /atom/movable/screen/alert/status_effect/reality_anchor_silenced
+ show_duration = TRUE
+ duration = 10 SECONDS
+ tick_interval = STATUS_EFFECT_NO_TICK
+
+/datum/status_effect/power/reality_anchor_silenced/on_apply()
+ ADD_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAIT_STATUS_EFFECT(id))
+ return TRUE
+
+/datum/status_effect/power/reality_anchor_silenced/on_remove()
+ REMOVE_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAIT_STATUS_EFFECT(id))
+ return
+
+/atom/movable/screen/alert/status_effect/reality_anchor_silenced
+ name = "Silenced"
+ desc = "Resonant powers are supressed around the reality anchor!"
+ icon = 'modular_doppler/modular_powers/icons/items/reality_anchor.dmi'
+ icon_state = "reality_anchor"
+
+// The effect from reality anchors
+/obj/effect/temp_visual/circle_wave/reality_anchor
+ color = COLOR_SILVER
+ max_alpha = 20
+ duration = 0.5 SECONDS
+ amount_to_scale = 7
+
+/obj/structure/reality_anchor/update_overlays()
+ . = ..()
diff --git a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm
new file mode 100644
index 00000000000000..1dbb7af38eddc3
--- /dev/null
+++ b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm
@@ -0,0 +1,38 @@
+// Antiresonant cuffs. They're like normal cuffs but slightly worse and put a dampener on resonant folk.
+/obj/item/restraints/handcuffs/antiresonant
+ name = "eschatite handcuffs"
+ desc = "Handcuffs laced with a smooth, dark material similar to magnetite called Eschatite, harvested from a reality anchor. Capable of suppressing resonant powers on whoever is made to wear them. Slightly less sturdy than regular handcuffs."
+ icon = 'modular_doppler/modular_powers/icons/items/restraints.dmi'
+ icon_state = "anti_resonant_cuffs"
+ breakouttime = 50 SECONDS
+ handcuff_time = 4.5 SECONDS
+ custom_price = PAYCHECK_COMMAND
+
+ /// we save the mob so we don't end up orphaning the silence remover
+ var/mob/living/cuffed_mob
+
+/obj/item/restraints/handcuffs/antiresonant/attempt_to_cuff(mob/living/carbon/victim, mob/living/user)
+ . = ..()
+ playsound(victim, 'sound/effects/magic/magic_block.ogg', 75, TRUE, -2)
+
+/obj/item/restraints/handcuffs/antiresonant/equipped(mob/living/user, slot)
+ . = ..()
+ if(slot == ITEM_SLOT_HANDCUFFED)
+ to_chat(user, span_warning("A shudder goes down your spine; [name] seem to suppress resonant powers!"))
+ user.dispel(src)
+ ADD_TRAIT(user, TRAIT_RESONANCE_SILENCED, src)
+ cuffed_mob = user
+
+/obj/item/restraints/handcuffs/antiresonant/on_uncuffed(datum/source, mob/living/wearer)
+ ..()
+ if(cuffed_mob)
+ REMOVE_TRAIT(cuffed_mob, TRAIT_RESONANCE_SILENCED, src)
+ cuffed_mob = null
+
+/obj/item/restraints/handcuffs/antiresonant/Destroy(force)
+ if(cuffed_mob)
+ REMOVE_TRAIT(cuffed_mob, TRAIT_RESONANCE_SILENCED, src)
+ cuffed_mob = null
+ return ..()
+
+// Vendor entry lives in modular_vending/code/tg_vendors/sectech.dm
diff --git a/modular_doppler/modular_powers/code/signalers/martial_arts_signal.dm b/modular_doppler/modular_powers/code/signalers/martial_arts_signal.dm
new file mode 100644
index 00000000000000..5a4ce9a271c061
--- /dev/null
+++ b/modular_doppler/modular_powers/code/signalers/martial_arts_signal.dm
@@ -0,0 +1,20 @@
+/* Emits the same unarmed-hit signal as the default species punch path, so power riders also fire for martial arts.
+* Sent by send_unarmed_hit_signal() in code\datums\martial\_martial.dm
+*/
+/datum/martial_art/proc/send_unarmed_hit_signal(mob/living/attacker, mob/living/defender)
+ PROTECTED_PROC(TRUE)
+ if(!attacker || !defender)
+ return
+
+ var/obj/item/bodypart/affecting = defender.get_bodypart(defender.get_random_valid_zone(attacker.zone_selected))
+ var/armor_block = 0
+ if(affecting)
+ armor_block = defender.run_armor_check(affecting, MELEE)
+
+ var/obj/item/bodypart/attacking_limb = get_attacking_limb(attacker, defender)
+ if(!attacking_limb && ishuman(attacker))
+ var/mob/living/carbon/human/human_attacker = attacker
+ attacking_limb = human_attacker.get_active_hand()
+
+ var/limb_sharpness = attacking_limb?.unarmed_sharpness
+ SEND_SIGNAL(attacker, COMSIG_HUMAN_UNARMED_HIT, defender, affecting, 0, armor_block, limb_sharpness)
diff --git a/modular_doppler/modular_powers/icons/items/organs.dmi b/modular_doppler/modular_powers/icons/items/organs.dmi
new file mode 100644
index 00000000000000..e780c1ae1c5910
Binary files /dev/null and b/modular_doppler/modular_powers/icons/items/organs.dmi differ
diff --git a/modular_doppler/modular_powers/icons/items/reality_anchor.dmi b/modular_doppler/modular_powers/icons/items/reality_anchor.dmi
new file mode 100644
index 00000000000000..fde5ffc3bd95f7
Binary files /dev/null and b/modular_doppler/modular_powers/icons/items/reality_anchor.dmi differ
diff --git a/modular_doppler/modular_powers/icons/items/restraints.dmi b/modular_doppler/modular_powers/icons/items/restraints.dmi
new file mode 100644
index 00000000000000..a7add805fd051a
Binary files /dev/null and b/modular_doppler/modular_powers/icons/items/restraints.dmi differ
diff --git a/modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi b/modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi
new file mode 100644
index 00000000000000..4168ba412f4520
Binary files /dev/null and b/modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi differ
diff --git a/modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi b/modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi
new file mode 100644
index 00000000000000..24986bca021914
Binary files /dev/null and b/modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi differ
diff --git a/modular_doppler/modular_powers/icons/powers/actions_icons.dmi b/modular_doppler/modular_powers/icons/powers/actions_icons.dmi
new file mode 100644
index 00000000000000..0147cb2a232c68
Binary files /dev/null and b/modular_doppler/modular_powers/icons/powers/actions_icons.dmi differ
diff --git a/modular_doppler/modular_powers/powers/_powers.dm b/modular_doppler/modular_powers/powers/_powers.dm
deleted file mode 100644
index 754df9e42dde2e..00000000000000
--- a/modular_doppler/modular_powers/powers/_powers.dm
+++ /dev/null
@@ -1,169 +0,0 @@
-/mob/living
- var/list/all_powers = list()
-
-/**
- * Power Handler
- *
- * Ensures that all powers are properly applied when a cremember spawns in.
- */
-GLOBAL_DATUM_INIT(power_handler, /datum/power_handler, new)
-
-/obj/item/organ/resonant/
- slot = ORGAN_SLOT_RESONANT
-
-/datum/power_handler/New()
- RegisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED, PROC_REF(handle_new_player))
-
-
-/datum/power_handler/proc/handle_new_player(datum/source, mob/living/carbon/human/new_crewmember, rank)
- SIGNAL_HANDLER
-
- // sanity checking because we really do not want to be causing any runtimes
- if(!istype(new_crewmember))
- return
- if(isnull(new_crewmember.mind))
- return
-
- var/datum/preferences/prefs = new_crewmember.client?.prefs
-
- if(isnull(prefs))
- return
-
- apply_powers(new_crewmember, prefs)
-
-/datum/power_handler/proc/apply_powers(mob/living/carbon/human/target, datum/preferences/preferences, visuals_only = FALSE)
- var/list/power_types = list()
-
- for(var/power_name in preferences.powers)
- var/datum/power/power_to_add = preferences.powers[power_name]
- power_to_add = new power_to_add()
- power_to_add.apply_to_human(target)
- var/core_power_type = get_path_type(power_to_add.power_type)
-
- if(core_power_type && !(core_power_type in power_types))
- power_types += core_power_type
-
- qdel(power_to_add)
-
- for(var/core_power in power_types)
- var/datum/power/powah_to_add = GLOB.path_core_powers[core_power]
- powah_to_add = new powah_to_add()
- powah_to_add.apply_to_human(target)
- qdel(powah_to_add)
-
-/datum/power_handler/Destroy()
- ..()
- UnregisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED)
-
-/**
- * Power datum. Used to contain and handle all information required for both TGUI and applying powers to a player.
- */
-
-/datum/power
-
- var/name
-
- var/desc
-
- // The relevant cost of the power in question. Must be an integer, not a string.
- var/cost
-
- // The path subtype this power falls under. Is also a trait.
- var/power_type
-
- // Whether or not the power is advanced, meaning if it can be taken with powers from other
- var/advanced = FALSE
-
- // Traits to be added when a power is applied to a mob.
- var/list/power_traits = list()
-
- // The power's root power. If the power is a root power, this should be the power datum itself, otherwise it should be it's respective root power's datum.
- var/datum/power/root_power
-
- // A list of power datums that CANNOT be taken alongside this power. This only checks if the blacklist variable is true, so all power's must be vice versa added to their respective blacklists.
- var/list/blacklist = list()
-
- // This value determines whether or not a power is initalized in the global list of powers used for the tgui menu. ONLY core powers should have this variable set to true.
- var/is_accessible = TRUE
-
- // A string that is send to the user's chat when they gain this power.
- var/gain_text
-
- // A list of power datums that MUST be taken for this power to be available.
- var/list/required_powers = list()
-
-
-/**
- * Apply To Human.
- *
- * The initial checks ran when a power is added. Makes sure the target is valid and does not already have said power, before adding the relevant traits, displaying gain text and then running the power's add proc.
- */
-/datum/power/proc/apply_to_human(mob/living/carbon/human/target)
- if(!target)
- CRASH("Power attempted to be added to null mob.")
-
- if(target.has_powerz(type))
- CRASH("Power attempted to be added to mob which already has this power.")
-
- target.all_powers += src
-
- if(power_traits)
- for(var/add_trait in power_traits)
- ADD_TRAIT(target, add_trait, TRAIT_POWER)
-
- if(gain_text)
- to_chat(target, gain_text)
-
- ADD_TRAIT(target, power_type, TRAIT_POWER)
-
- ADD_TRAIT(target, get_path_type(power_type), TRAIT_POWER)
-
- add(target)
-
-/**
- * Checks if a mob already has the provided power.
- */
-/mob/living/proc/has_powerz(power_type)
-
- for(var/datum/power/power in all_powers)
-
- if(power.type == power_type)
- return TRUE
-
- return FALSE
-
-/**
- * Proc ran whenever a power is added to a mob. Should be used for unique effects that cannot be easily automated, such as organ insertion and action learning.
- */
-/datum/power/proc/add(mob/living/carbon/human/target)
- return
-
-/**
- * Item power. Used to grant an item, wherein give_item_to_holder() should be added to the end of it's add proc.
- */
-
-/datum/power/item
- var/list/where_items_spawned
-
- var/open_backpack
-
-
-/datum/power/item/proc/give_item_to_holder(mob/living/carbon/human/target, obj/item/power_item, list/valid_slots, flavour_text = null, default_location = "at your feet", notify_player = TRUE)
- if(ispath(power_item))
- power_item = new power_item(get_turf(target))
-
- var/where = target.equip_in_one_of_slots(power_item, valid_slots, qdel_on_fail = FALSE, indirect_action = TRUE) || default_location
-
- if(where == LOCATION_BACKPACK)
- open_backpack = TRUE
-
- if(notify_player)
- LAZYADD(where_items_spawned, span_boldnotice("You have \a [power_item] [where]. [flavour_text]"))
-
- if(open_backpack && target.back)
- target.back.atom_storage.show_contents(target)
-
- for(var/chat_string in where_items_spawned)
- to_chat(target, chat_string)
-
- where_items_spawned = null
diff --git a/modular_doppler/modular_powers/powers/core_powers.dm b/modular_doppler/modular_powers/powers/core_powers.dm
deleted file mode 100644
index 30d6175cd89092..00000000000000
--- a/modular_doppler/modular_powers/powers/core_powers.dm
+++ /dev/null
@@ -1,62 +0,0 @@
-// Mortal
-
-/datum/power/tenacious
- name = "Tenacious"
- desc = "Try to remember some of the basics of CQC."
- is_accessible = FALSE
- power_traits = list(TRAIT_POWER_TENACIOUS)
-
-// Sorcerous
-
-/datum/power/prestidigitation
- name = "Prestidigitation"
- desc = "Allows a Sorcerous individual to perform magical tricks"
- root_power = /datum/power/prestidigitation
- power_type = TRAIT_PATH_SUBTYPE_THAUMATURGE
- is_accessible = FALSE
-
-/datum/power/prestidigitation/add(mob/living/carbon/human/target)
- var/datum/action/new_action = new /datum/action/cooldown/spell/prestidigitation(target.mind || target)
- new_action.Grant(target)
-
-/datum/action/cooldown/spell/prestidigitation
- name = "Prestidigitation"
- desc = "The knowledge required to perform a variety of magical tricks."
- button_icon_state = "arcane_barrage"
-
- school = SCHOOL_CONJURATION
- cooldown_time = 12 SECONDS
- cooldown_reduction_per_rank = 2.5 SECONDS
- spell_requirements = NONE
-
- invocation_type = INVOCATION_EMOTE
-
- invocation = "Someone starts performing magic tricks!"
- invocation_self_message = "You start performing magic tricks."
-
-// Resonant
-
-/datum/power/meditate
- name = "Meditate"
- desc = "ooughhh im meditating"
- is_accessible = FALSE
- power_type = TRAIT_PATH_SUBTYPE_PSYKER
-
-/datum/power/meditate/add(mob/living/carbon/human/target)
- var/datum/action/new_action = new /datum/action/cooldown/spell/meditate(target.mind || target)
- new_action.Grant(target)
-
-/datum/action/cooldown/spell/meditate
- name = "Meditate"
- desc = "This state of internal focus allows them to replenish any reserves they have and purge any impurities dredged up by abusing Nature's law."
- button_icon_state = "nose"
-
- school = SCHOOL_CONJURATION
- cooldown_time = 12 SECONDS
- cooldown_reduction_per_rank = 2.5 SECONDS
- spell_requirements = NONE
-
- invocation_type = INVOCATION_EMOTE
-
- invocation = "Someone starts meditating."
- invocation_self_message = "You start meditating"
diff --git a/modular_doppler/modular_powers/powers/mortal_powers/augmented.dm b/modular_doppler/modular_powers/powers/mortal_powers/augmented.dm
deleted file mode 100644
index 0312067e21b43f..00000000000000
--- a/modular_doppler/modular_powers/powers/mortal_powers/augmented.dm
+++ /dev/null
@@ -1,24 +0,0 @@
-/**
- * Root powers
- */
-
-/datum/power/spinal_tap
- name = "Spinal CNS Tap"
- desc = "Vulnerable to EMPs. Acts as a frame for other augments."
- cost = 6
- root_power = /datum/power/spinal_tap
- power_type = TRAIT_PATH_SUBTYPE_AUGMENTED
-
-/datum/power/titanium
- name = "Titanium Endoskeleton"
- desc = "Makes you more resistant to bone wounds and better at tackling. Acts as a frame for other augments."
- cost = 6
- root_power = /datum/power/titanium
- power_type = TRAIT_PATH_SUBTYPE_AUGMENTED
-
-/datum/power/pneumatic
- name = "Pneumatic Reservoir"
- desc = "Vulnerable to EMPs. Acts as a frame for other augments."
- cost = 6
- root_power = /datum/power/pneumatic
- power_type = TRAIT_PATH_SUBTYPE_AUGMENTED
diff --git a/modular_doppler/modular_powers/powers/mortal_powers/expert.dm b/modular_doppler/modular_powers/powers/mortal_powers/expert.dm
deleted file mode 100644
index d70106d5066858..00000000000000
--- a/modular_doppler/modular_powers/powers/mortal_powers/expert.dm
+++ /dev/null
@@ -1,56 +0,0 @@
-/**
- * Root Powers
- */
-/datum/power/medical
- name = "Medical Specialty"
- desc = "Your specialty lies in caring for the wounded. Applying sutures and other similar items is faster."
- cost = 6
- root_power = /datum/power/medical
- power_type = TRAIT_PATH_SUBTYPE_EXPERT
- power_traits = list(TRAIT_POWER_MEDICAL)
-
-/datum/power/engineering
- name = "Engineering Specialty"
- desc = "Your specialty lies in construction and deconstruction. You're slightly faster at using all tools."
- cost = 6
- root_power = /datum/power/engineering
- power_type = TRAIT_PATH_SUBTYPE_EXPERT
- power_traits = list(TRAIT_POWER_ENGINEERING)
-
-/datum/power/service
- name = "Service Speciality"
- desc = "Your speciality lies in supporting the station."
- cost = 6
- root_power = /datum/power/service
- power_type = TRAIT_PATH_SUBTYPE_EXPERT
- power_traits = list(TRAIT_POWER_SERVICE)
-
-/**
- * Basic powers
- */
-/datum/power/seasoned_chef
- name = "Seasoned Chef"
- desc = "You are a seasoned chef."
- cost = 4
- root_power = /datum/power/service
- power_type = TRAIT_PATH_SUBTYPE_EXPERT
-
-/datum/power/green_thumb
- name = "Green Thumb"
- desc = "You are a green thumb."
- cost = 2
- root_power = /datum/power/service
- power_type = TRAIT_PATH_SUBTYPE_EXPERT
-
-/**
- * Advanced powers
- */
-
-/datum/power/master_chef
- name = "Master Chef"
- desc = "You are a master chef. Requires Green Thumb and Master Chef"
- cost = 8
- root_power = /datum/power/service
- power_type = TRAIT_PATH_SUBTYPE_EXPERT
- advanced = TRUE
- required_powers = list(/datum/power/green_thumb, /datum/power/seasoned_chef)
diff --git a/modular_doppler/modular_powers/powers/mortal_powers/warfighter.dm b/modular_doppler/modular_powers/powers/mortal_powers/warfighter.dm
deleted file mode 100644
index b8732b8c92953e..00000000000000
--- a/modular_doppler/modular_powers/powers/mortal_powers/warfighter.dm
+++ /dev/null
@@ -1,62 +0,0 @@
-#define MARTIALART_MARTIALART "martialart"
-#define MARTIALART_CQB 'cqb"'
-
-/**
- * Root powers
- */
-
-/datum/martial_art/martialart
- name = "Martial Art"
- id = MARTIALART_MARTIALART
-
-/datum/power/martialart
- name = "Martial Art"
- desc = "While not as advanced as the Resonant arts of Cultivators, with enough training, anyone can pack a punch. \
- This style boosts melee damage and lets the user block unarmed attacks by enabling throw mode."
- cost = 6
- root_power = /datum/power/martialart
- power_type = TRAIT_PATH_SUBTYPE_WARFIGHTER
-
-/datum/power/martialart/add(mob/living/carbon/human/target)
- var/datum/martial_art/martial_to_learn = new /datum/martial_art/martialart()
- if(!martial_to_learn.teach(target))
- to_chat(target, span_warning("You attempt to learn [martial_to_learn.name],\
- but your current knowledge of martial arts conflicts with the new style, so it just doesn't stick with you."))
-
-/datum/power/cqb
- name = "CQB"
- desc = "Carbines, shotguns, and pistols. CQB is used in boarding actions or room clearing: as a result of their training, \
- users of CQB do significantly more damage when melee-attacking with firearms; e.x., pistolwhipping."
- cost = 6
- root_power = /datum/power/cqb
- power_type = TRAIT_PATH_SUBTYPE_WARFIGHTER
- power_traits = list(TRAIT_POWER_CQB)
-
-/datum/power/precision_killer
- name = "Precision Killer"
- desc = "Snipers and their spotters. Most people who have fought these individuals do not know who killed them. \
- After being scoped in for four seconds, users of this style deal ten extra damage."
- cost = 6
- root_power = /datum/power/precision_killer
- power_type = TRAIT_PATH_SUBTYPE_WARFIGHTER
- power_traits = list(TRAIT_POWER_SNIPER)
-
-/datum/power/leadership
- name = "Leadership"
- desc = "Expressed in many ways, from an iron fist to selfless responsibility. Grants the Designate Ally ability, \
- which lets you select up to 3 people as allies. Helping your allies (shaking them to their feet, CPR, fireman carrying) \
- is faster, to include your allies helping each other or you."
- cost = 3
- root_power = /datum/power/leadership
- power_type = TRAIT_PATH_SUBTYPE_WARFIGHTER
-
-/datum/power/leadership/add(mob/living/carbon/human/target)
- var/datum/action/cooldown/mob_cooldown/designate_ally/designate = new(src)
- designate.Grant(target)
-
-/datum/action/cooldown/mob_cooldown/designate_ally
- name = "Designate Ally"
- button_icon = 'icons/mob/actions/actions_items.dmi'
- button_icon_state = "rcl_gui"
- desc = "Some time in the future, this might let you designate allies. Maybe"
- cooldown_time = 10 SECONDS
diff --git a/modular_doppler/modular_powers/powers/resonant_powers/aberrant.dm b/modular_doppler/modular_powers/powers/resonant_powers/aberrant.dm
deleted file mode 100644
index d7e01d0ec55d43..00000000000000
--- a/modular_doppler/modular_powers/powers/resonant_powers/aberrant.dm
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Root powers
- */
-
-/datum/power/cuprous_heart
- name = "Cuprous Heart"
- desc = "Your heart and blood are Living Copper. Your wounds and injuries naturally seal themselves, and you're resistant \
- to Resonant effects, but your blood does not replenish naturally and is hard to synthesize."
- cost = 5
- root_power = /datum/power/cuprous_heart
- power_type = TRAIT_PATH_SUBTYPE_ABERRANT
-
-/obj/item/organ/heart/resonant/copper
- name = "cuprous heart"
- desc = "This heart appears to be made out of pure copper. You could scrap this for a fair amount of dosh."
-
-/datum/power/cuprous_heart/add(mob/living/carbon/human/target)
-
- var/obj/item/organ/heart/copper_heart = null
- var/obj/item/organ/heart/old_heart = target.get_organ_slot(ORGAN_SLOT_HEART)
- if(old_heart && IS_ORGANIC_ORGAN(old_heart))
- copper_heart = /obj/item/organ/heart/resonant/copper
- if(!isnull(copper_heart))
- copper_heart = new copper_heart
- copper_heart.Insert(target, special = TRUE)
-
-/datum/power/muscly
- name = "Condensed Musculature"
- desc = "You're far more athletic than the average person."
- cost = 5
- root_power = /datum/power/muscly
- power_type = TRAIT_PATH_SUBTYPE_ABERRANT
- power_traits = list(TRAIT_POWER_MUSCLY)
-
-/datum/power/bestial
- name = "Latent Bestial Traits"
- desc = "Your hearing is sharper than normal, but loud noises hurt your ears much more."
- root_power = /datum/power/bestial
- power_type = TRAIT_PATH_SUBTYPE_ABERRANT
- cost = 5
- power_traits = list(TRAIT_POWER_BESTIAL)
diff --git a/modular_doppler/modular_powers/powers/resonant_powers/cultivator.dm b/modular_doppler/modular_powers/powers/resonant_powers/cultivator.dm
deleted file mode 100644
index 0d81970763b097..00000000000000
--- a/modular_doppler/modular_powers/powers/resonant_powers/cultivator.dm
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * Root Powers
- */
-
-/datum/power/astral_dantian
- name = "Astral Dantian"
- desc = "An organ entirely made of Resonance located just behind the navel. It seems to be a battery of some sort. \
- Meditation now requires direct view of the stars to be productive. You can only have one Dantian."
- cost = 5
- root_power = /datum/power/astral_dantian
- blacklist = list(/datum/power/umbral_dantian, /datum/power/paracausal)
- power_type = TRAIT_PATH_SUBTYPE_CULTIVATOR
-
-/obj/item/organ/resonant/astral_dantian
- name = "astral dantian"
- desc = "An organ entirely made of Resonance located just behind the navel. It seems to be a battery of some sort. Meditation now requires direct view of the stars to be productive. You can only have one Dantian."
- icon_state = "tongueplasma"
- w_class = WEIGHT_CLASS_TINY
-
-/datum/power/astral_dantian/add(mob/living/carbon/human/target)
- var/obj/item/organ/resonant/astral_dantian/astrawl = new ()
- astrawl.Insert(target, special = TRUE)
-
-/datum/power/umbral_dantian
- name = "Umbral Dantian"
- desc = "An organ entirely made of Resonance located just behind the navel. It seems to be a battery of some sort, \
- and grants the user night vision at the cost of requiring more food. Meditation requires absolute darkness to be productive. You can only have one Dantian."
- cost = 5
- root_power = /datum/power/umbral_dantian
- blacklist = list(/datum/power/astral_dantian, /datum/power/paracausal)
- power_type = TRAIT_PATH_SUBTYPE_CULTIVATOR
-
-/obj/item/organ/resonant/umbral_dantian
- name = "Umbral dantian"
- desc = "An organ entirely made of Resonance located just behind the navel. It seems to be a battery of some sort, and grants the user night vision at the cost of requiring more food. Meditation requires absolute darkness to be productive. You can only have one Dantian."
- icon_state = "tongueplasma"
- w_class = WEIGHT_CLASS_TINY
-
-/datum/power/umbral_dantian/add(mob/living/carbon/human/target)
- var/obj/item/organ/resonant/umbral_dantian/umbrawl = new ()
- umbrawl.Insert(target, special = TRUE)
diff --git a/modular_doppler/modular_powers/powers/resonant_powers/psyker.dm b/modular_doppler/modular_powers/powers/resonant_powers/psyker.dm
deleted file mode 100644
index e596daae2d7cdc..00000000000000
--- a/modular_doppler/modular_powers/powers/resonant_powers/psyker.dm
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-/datum/power/paracausal
- name = "Paracausal Gland"
- desc = "An organ found only in the central nervous systems of Psykers that doesn't entirely exist on our plane of existence. \
- Technically a Deviancy; however, due to its nature, this gland does not interfere with advanced psychic abilities. Violently interferes with a Dantian."
- cost = 5
- root_power = /datum/power/paracausal
- power_type = TRAIT_PATH_SUBTYPE_PSYKER
- blacklist = list(/datum/power/astral_dantian, /datum/power/umbral_dantian)
-
-/obj/item/organ/resonant/paracausal
- name = "paracausal gland"
- desc = "An organ found only in the central nervous systems of Psykers that doesn't entirely exist on our plane of existence. Technically a Deviancy; however, due to its nature, this gland does not interfere with advanced psychic abilities. Violently interferes with a Dantian."
- icon_state = "tongueplasma"
- w_class = WEIGHT_CLASS_TINY
-
-/datum/power/paracausal/add(mob/living/carbon/human/target)
- var/obj/item/organ/resonant/paracausal/para_organ = new ()
- para_organ.Insert(target, special = TRUE)
diff --git a/modular_doppler/modular_powers/powers/sorcerous_powers/enigmatist.dm b/modular_doppler/modular_powers/powers/sorcerous_powers/enigmatist.dm
deleted file mode 100644
index b9318172b2bd38..00000000000000
--- a/modular_doppler/modular_powers/powers/sorcerous_powers/enigmatist.dm
+++ /dev/null
@@ -1,30 +0,0 @@
-#define CAT_ENIGMATIST "Enigmatist"
-
-/**
- * Root powers
- */
-
-/datum/power/chalk
-
- name = "Produce Resonant Chalk"
- desc = "Allows a Sorcerous individual to prepare and use a spellbook, which can be re-skinned as a spell focus or a bag of materials. All Thaumaturge abilities require the use of a spellbook."
- cost = 5
- root_power = /datum/power/chalk
- power_type = TRAIT_PATH_SUBTYPE_ENIGMATIST
-
-/datum/power/chalk/add(mob/living/carbon/human/target)
- target.mind.teach_crafting_recipe(/datum/crafting_recipe/resonant_chalk)
-
-/datum/crafting_recipe/resonant_chalk
- name = "Resonant Chalk"
- result = /obj/item/toy/crayon/purple/resonant_chalk
- reqs = list(
- /obj/item/stack/sheet/mineral/plasma = 1,
- /obj/item/toy/crayon = 1,
- )
- time = 5 SECONDS
- category = CAT_ENIGMATIST
- crafting_flags = CRAFT_MUST_BE_LEARNED
-
-/obj/item/toy/crayon/purple/resonant_chalk
- name = "Resonant Chalk"
diff --git a/modular_doppler/modular_powers/powers/sorcerous_powers/thaumaturge.dm b/modular_doppler/modular_powers/powers/sorcerous_powers/thaumaturge.dm
deleted file mode 100644
index 98e06f40799716..00000000000000
--- a/modular_doppler/modular_powers/powers/sorcerous_powers/thaumaturge.dm
+++ /dev/null
@@ -1,23 +0,0 @@
-/**
- * Root powers
- */
-
-/datum/power/item/spellprep
-
- name = "Spell Preparation"
- desc = "Allows a Sorcerous individual to prepare and use a spellbook, which can be re-skinned as a spell focus or a bag of materials. All Thaumaturge abilities require the use of a spellbook."
- cost = 5
- root_power = /datum/power/item/spellprep
- power_type = TRAIT_PATH_SUBTYPE_THAUMATURGE
- gain_text = span_notice("You appear to have accidentaly picked up some random book instead of your spellbook...")
-
-/datum/power/item/spellprep/add(mob/living/carbon/human/target)
- var/obj/item/book/random/spellbook = new(get_turf(target))
- spellbook.name = "[target.real_name]'s spellbook"
- give_item_to_holder(target, spellbook, list(
- LOCATION_LPOCKET,
- LOCATION_RPOCKET,
- LOCATION_BACKPACK,
- LOCATION_HANDS,
- ),
- )
diff --git a/modular_doppler/modular_powers/powers/sorcerous_powers/theologist.dm b/modular_doppler/modular_powers/powers/sorcerous_powers/theologist.dm
deleted file mode 100644
index b152fe166e2888..00000000000000
--- a/modular_doppler/modular_powers/powers/sorcerous_powers/theologist.dm
+++ /dev/null
@@ -1,119 +0,0 @@
-/**
- * Root powers
- */
-
-/datum/power/burden_shared
-
- name = "A Burden Shared"
- desc = "A channeled ability. Every four seconds, attempt to equalize both your and the target's health, in increments of 10 damage. \
- Has a cooldown of 2 minutes after use. Grants 1 Piety after health is equalized if you were at least 10 points less damaged than the target, \
- and takes 1 Piety if you were at least 10 points more damaged. Mutually exclusive with A Burden Twisted and A Burden Revered."
- cost = 5
- root_power = /datum/power/burden_shared
- power_type = TRAIT_PATH_SUBTYPE_THEOLOGIST
- blacklist = list(/datum/power/burden_twist, /datum/power/burden_revered)
-
-
-/datum/power/burden_shared/add(mob/living/carbon/human/target)
- var/datum/action/new_action = new /datum/action/cooldown/spell/burden_shared(target.mind || target)
- new_action.Grant(target)
-
-/datum/action/cooldown/spell/burden_shared
- name = "A Burden Shared"
- desc = "The knowledge required to share one's burden."
- button_icon_state = "arcane_barrage"
-
- school = SCHOOL_CONJURATION
- cooldown_time = 2 MINUTES
- cooldown_reduction_per_rank = 2.5 SECONDS
- spell_requirements = NONE
-
- invocation_type = INVOCATION_EMOTE
-
- invocation = "Someone starts sharing their burden!"
- invocation_self_message = "You start sharing your burden."
-
-/datum/power/burden_twist
-
- name = "A Burden Twisted"
- desc = "A channeled ability. Every ten seconds, heal an adjacent carbon for up to 30 damage, then deal half of that damage back to them as \
- a random proportion of brute, burn, and oxygen damage. Has a cooldown of 2 minutes after use. Has a chance to give Piety when used on someone \
- with more than 30 damage. Mutually exclusive with A Burden Shared and A Burden Revered."
- cost = 5
- root_power = /datum/power/burden_twist
- power_type = TRAIT_PATH_SUBTYPE_THEOLOGIST
- blacklist = list(/datum/power/burden_shared, /datum/power/burden_revered)
-
-
-/datum/power/burden_twist/add(mob/living/carbon/human/target)
- var/datum/action/new_action = new /datum/action/cooldown/spell/burden_twist(target.mind || target)
- new_action.Grant(target)
-
-/datum/action/cooldown/spell/burden_twist
- name = "A Burden Twisted"
- desc = "The knowledge required to twist one's burden."
- button_icon_state = "arcane_barrage"
-
- school = SCHOOL_CONJURATION
- cooldown_time = 2 MINUTES
- cooldown_reduction_per_rank = 2.5 SECONDS
- spell_requirements = NONE
-
- invocation_type = INVOCATION_EMOTE
-
- invocation = "Someone starts twisting their burden!"
- invocation_self_message = "You start twisting your burden."
-
-/datum/power/burden_revered
-
- name = "A Burden Revered"
- desc = "Use on an adjacent carbon or yourself to nullify their pain and heal up to 30 damage over a long duration of time. \
- Grants Piety based on how injured the target was. Mutually exclusive with A Burden Shared and A Burden Twisted."
- cost = 5
- root_power = /datum/power/burden_revered
- power_type = TRAIT_PATH_SUBTYPE_THEOLOGIST
- blacklist = list(/datum/power/burden_twist, /datum/power/burden_shared)
-
-
-/datum/power/burden_revered/add(mob/living/carbon/human/target)
- var/datum/action/new_action = new /datum/action/cooldown/spell/burden_revered(target.mind || target)
- new_action.Grant(target)
-
-/datum/action/cooldown/spell/burden_revered
- name = "A Burden Revered."
- desc = "The knowledge required to revere one's burden."
- button_icon_state = "arcane_barrage"
-
- school = SCHOOL_CONJURATION
- cooldown_time = 2 MINUTES
- cooldown_reduction_per_rank = 2.5 SECONDS
- spell_requirements = NONE
-
- invocation_type = INVOCATION_EMOTE
-
- invocation = "Someone starts revering their burden!"
- invocation_self_message = "You start revering your burden."
-
-/datum/power/check_piety
-
- name = "Check Piety"
- desc = "Tells you your current Piety."
- cost = 0
- root_power = /datum/power/check_piety
- power_type = TRAIT_PATH_SUBTYPE_THEOLOGIST
-
-/datum/power/check_piety/add(mob/living/carbon/human/target)
- var/datum/action/new_action = new /datum/action/cooldown/mob_cooldown/check_piety(target.mind || target)
- new_action.Grant(target)
-
-
-/datum/action/cooldown/mob_cooldown/check_piety
- name = "Check Piety"
- button_icon = 'icons/mob/actions/actions_items.dmi'
- button_icon_state = "scan_mode"
- desc = "Allows you to check your piety."
- cooldown_time = 1.5 SECONDS
-
-/datum/action/cooldown/mob_cooldown/dash/Activate(atom/target_atom)
- to_chat(owner, span_notice("You have no piety. You are NOT pious. You will NEVER be pious . . . "))
- return TRUE
diff --git a/modular_doppler/modular_powers/preferences/powers_middleware.dm b/modular_doppler/modular_powers/preferences/powers_middleware.dm
deleted file mode 100644
index 34666e2d787b19..00000000000000
--- a/modular_doppler/modular_powers/preferences/powers_middleware.dm
+++ /dev/null
@@ -1,310 +0,0 @@
-/datum/preference_middleware/powers
- var/static/list/name_to_powers
- action_delegations = list(
- "give_power" = PROC_REF(give_power),
- "remove_power" = PROC_REF(remove_power),
- )
-
-/datum/preference_middleware/powers/get_ui_data(mob/user)
-
- if(length(name_to_powers) != length(GLOB.all_powers))
- initialize_names_to_powers()
-
- var/list/data = list()
-
- var/list/thaumaturge = list()
- var/list/enigmatist = list()
- var/list/theologist = list()
-
- var/list/psyker = list()
- var/list/cultivator = list()
- var/list/aberrant = list()
-
- var/list/warfighter = list()
- var/list/expert = list()
- var/list/augmented = list()
-
- var/max_power_points = MAXIMUM_POWER_POINTS
-
- var/current_points = point_check()
-
- for(var/power_name in GLOB.all_powers)
- var/datum/power/power = GLOB.power_datum_instances[power_name]
-
- var/state
- var/word
- var/color
- var/powertype
- var/rootpower = null
-
- if(power.root_power == power.type)
- powertype = "crown"
- else if(power.advanced)
- powertype = "diamond"
- rootpower = power.root_power.name
- else
- powertype = ""
- rootpower = power.root_power.name
-
- if(preferences.powers[power.name])
- state = "bad"
- word = "Forget"
- else
- state = "good"
- word = "Learn"
- if((power.cost + current_points) > max_power_points)
- state = "transparent"
- word = "N/A"
- color = "0.5"
- rootpower = null
- else
- color = "1"
-
- var/final_list = list(list(
- "description" = power.desc,
- "name" = power.name,
- "cost" = power.cost,
- "state" = state,
- "word" = word,
- "color" = color,
- "powertype" = powertype,
- "rootpower" = rootpower
- ))
-
- switch(power.power_type)
- if(TRAIT_PATH_SUBTYPE_THAUMATURGE)
- thaumaturge += final_list
- if(TRAIT_PATH_SUBTYPE_ENIGMATIST)
- enigmatist += final_list
- if(TRAIT_PATH_SUBTYPE_THEOLOGIST)
- theologist += final_list
- if(TRAIT_PATH_SUBTYPE_PSYKER)
- psyker += final_list
- if(TRAIT_PATH_SUBTYPE_CULTIVATOR)
- cultivator += final_list
- if(TRAIT_PATH_SUBTYPE_ABERRANT)
- aberrant += final_list
- if(TRAIT_PATH_SUBTYPE_WARFIGHTER)
- warfighter += final_list
- if(TRAIT_PATH_SUBTYPE_EXPERT)
- expert += final_list
- if(TRAIT_PATH_SUBTYPE_AUGMENTED)
- augmented += final_list
-
-
- data["total_power_points"] = max_power_points
- data["thaumaturge"] = thaumaturge
- data["enigmatist"] = enigmatist
- data["theologist"] = theologist
- data["psyker"] = psyker
- data["cultivator"] = cultivator
- data["aberrant"] = aberrant
- data["warfighter"] = warfighter
- data["expert"] = expert
- data["augmented"] = augmented
- data["power_points"] = point_check()
-
- return data
-
-/datum/preference_middleware/powers/proc/initialize_names_to_powers()
- name_to_powers = list()
- for(var/power_name in GLOB.all_powers)
- var/datum/power/power = GLOB.power_datum_instances[power_name]
- name_to_powers[power.name] = power_name
-
-/**
- * Gives a power to a character using the params list provided by tgui. Runs through multiple checks to ensure that the power can be learned, see respective procs for their description
- *
- * Always returns TRUE, ensuring the UI stays updated.
- */
-
-/datum/preference_middleware/powers/proc/give_power(list/params, mob/user)
- var/datum/power/power = name_to_powers[params["power_name"]]
- var/max_points = MAXIMUM_POWER_POINTS
-
- if(preferences.powers)
- if(power.advanced && advanced_check(power))
- to_chat(user, span_boldwarning("[power.name] is an advanced power! You cannot cross-path with it!"))
- return TRUE
-
- if(root_check(power))
- to_chat(user, span_boldwarning("[power.name] is missing it's root power!"))
- return TRUE
-
- if((point_check() + power.cost) > max_points)
- return TRUE
-
- var/datum/power/power_datum = new power()
-
- if(power_datum.blacklist.len && blacklist_check(power_datum, user))
- return TRUE
-
- if(power_datum.required_powers.len && required_check(power_datum))
- to_chat(user, span_boldwarning("[power.name] is missing one or more of it's required powers!"))
- return TRUE
-
- qdel(power_datum)
-
- preferences.powers[power.name] = power
-
- return TRUE
-
-/**
- * Remove Power
- *
- * Removes a power from a character using the params list provided by tgui. Recursively checks all learned powers for their root power, advanced power and required powers to make sure that they still pass all checks with said power removed.
- */
-
-/datum/preference_middleware/powers/proc/remove_power(list/params)
- var/datum/power/power = name_to_powers[params["power_name"]]
-
- preferences.powers -= power.name
-
- for(var/power_name in preferences.powers)
- var/datum/power/powor = preferences.powers[power_name]
-
- if(powor.advanced && advanced_check(powor))
- preferences.powers -= powor.name
- continue
-
- if(root_check(powor))
- preferences.powers -= powor.name
- continue
-
- var/datum/power/power_datum = new powor()
-
- if(power_datum.required_powers.len && required_check(power_datum))
- return TRUE
-
- qdel(power_datum)
-
-
- return TRUE
-
-/**
- * Advanced Power Check
- *
- * Gathers the advanced power's path, as well as the paths of all learned powers. If the list is longer than one, that means the user has cross-pathed, in which case the proc returns TRUE and the check fails. Otherwise, returns false and fails.
- */
-
-/datum/preference_middleware/powers/proc/advanced_check(datum/power/power_check)
- var/list/types = list()
- types += get_path_type(power_check.power_type)
-
- for(var/power_name in preferences.powers)
- var/datum/power/power = preferences.powers[power_name]
- var/type_to_check = get_path_type(power.power_type)
- if(!(type_to_check in types))
- types += type_to_check
-
- if(types.len > 1)
- return TRUE
-
- else return FALSE
-
-/**
- * Root Check
- *
- * Checks for a power's root power. If the power is a root power itself, the check immediately returns false, passing. If the power's root power is in the player's learned powers, it returns false, also passing. Otherwise, fails.
- */
-
-/datum/preference_middleware/powers/proc/root_check(datum/power/power_check)
-
- if(power_check.root_power == power_check)
- return FALSE
-
- for(var/power_name in preferences.powers)
- var/datum/power/powah = preferences.powers[power_name]
- if(power_check.root_power == powah)
- return FALSE
-
- return TRUE
-
-/**
- * Point Check
- *
- * Checks the total point value of a user's learned powers.
- */
-
-/datum/preference_middleware/powers/proc/point_check()
- var/total_points = 0
-
- for(var/power_name in preferences.powers)
- var/datum/power/expensive_ass_power = preferences.powers[power_name]
- total_points += expensive_ass_power.cost
-
- return total_points
-
-/**
- * Blacklist Check
- *
- * Checks if any of the user's learned powers are in a specific power's blacklist.
- */
-
-/datum/preference_middleware/powers/proc/blacklist_check(datum/power/power_check, mob/user)
- for(var/power_name in preferences.powers)
- if(preferences.powers[power_name] in power_check.blacklist)
- to_chat(user, span_boldwarning("[power_name] is in [power_check]'s blacklist!"))
- return TRUE
-
- return FALSE
-
-/**
- * Required Powers Check
- *
- * Cycles through a user's learned powers, and if that power is in the provided power's required_powers, increases the count by 1. If the count equals the length of the required_powers list, they have all the required powers, therefore the proc returns FALSE, meaning it passes. Otherwise, returns TRUE and fails.
- */
-
-/datum/preference_middleware/powers/proc/required_check(datum/power/power_check)
- var/count = 0
- for(var/power_name in preferences.powers)
- var/datum/power/required_power = preferences.powers[power_name]
- if(required_power in power_check.required_powers)
- count++
-
- if(count == power_check.required_powers.len)
- return FALSE
-
- return TRUE
-
-/datum/preferences/proc/sanitize_powers()
- var/powers_edited = FALSE
- for(var/power_name as anything in powers)
- if(!power_name)
- powers.Remove(power_name)
- powers_edited = TRUE
- continue
-
- var/datum/power/power = powers[power_name]
- power = new power()
- if(!(power.type in subtypesof(/datum/power)))
- powers.Remove(power_name)
- powers_edited = TRUE
- qdel(power)
-
- return powers_edited
-
-/datum/asset/simple/powers
- assets = list(
- "gear.png" = 'modular_doppler/modular_powers/icons/ui/powers/gear.png',
- "heart.png" = 'modular_doppler/modular_powers/icons/ui/powers/heart.png',
- "seal.png" = 'modular_doppler/modular_powers/icons/ui/powers/seal.png'
- )
-
-/datum/preference_middleware/powers/get_ui_assets()
- return list(
- get_asset_datum(/datum/asset/simple/powers),
- )
-
-/proc/get_path_type(string)
-
- switch(string)
-
- if(TRAIT_PATH_SUBTYPE_THAUMATURGE, TRAIT_PATH_SUBTYPE_ENIGMATIST, TRAIT_PATH_SUBTYPE_THEOLOGIST)
- return TRAIT_PATH_SORCEROUS
-
- if(TRAIT_PATH_SUBTYPE_PSYKER, TRAIT_PATH_SUBTYPE_CULTIVATOR, TRAIT_PATH_SUBTYPE_ABERRANT)
- return TRAIT_PATH_RESONANT
-
- if(TRAIT_PATH_SUBTYPE_WARFIGHTER, TRAIT_PATH_SUBTYPE_EXPERT, TRAIT_PATH_SUBTYPE_AUGMENTED)
- return TRAIT_PATH_MORTAL
diff --git a/modular_doppler/modular_quirks/permitted_cybernetic/code/preferences.dm b/modular_doppler/modular_quirks/permitted_cybernetic/code/preferences.dm
deleted file mode 100644
index 056b0caf0939c7..00000000000000
--- a/modular_doppler/modular_quirks/permitted_cybernetic/code/preferences.dm
+++ /dev/null
@@ -1,20 +0,0 @@
-/datum/preference/choiced/permitted_cybernetic
- category = PREFERENCE_CATEGORY_MANUALLY_RENDERED
- savefile_key = "permitted_cybernetic"
- savefile_identifier = PREFERENCE_CHARACTER
- can_randomize = FALSE
-
-/datum/preference/choiced/permitted_cybernetic/init_possible_values()
- return list("Random") + assoc_to_keys(GLOB.possible_quirk_implants)
-
-/datum/preference/choiced/permitted_cybernetic/create_default_value()
- return "Random"
-
-/datum/preference/choiced/permitted_cybernetic/is_accessible(datum/preferences/preferences)
- if (!..())
- return FALSE
-
- return "Permitted Cybernetic" in preferences.all_quirks
-
-/datum/preference/choiced/permitted_cybernetic/apply_to_human(mob/living/carbon/human/target, value)
- return
diff --git a/modular_doppler/modular_quirks/permitted_cybernetic/permitted_cybernetic.dm b/modular_doppler/modular_quirks/permitted_cybernetic/permitted_cybernetic.dm
deleted file mode 100644
index bdd6e55ac4f275..00000000000000
--- a/modular_doppler/modular_quirks/permitted_cybernetic/permitted_cybernetic.dm
+++ /dev/null
@@ -1,79 +0,0 @@
-GLOBAL_LIST_INIT(possible_quirk_implants, list(
- "Engineering Toolset" = /obj/item/organ/cyberimp/arm/toolkit/toolset,
- "Surgery Toolset" = /obj/item/organ/cyberimp/arm/toolkit/surgery,
- "Hydroponics Toolset" = /obj/item/organ/cyberimp/arm/toolkit/botany,
- "Sanitation Toolset" = /obj/item/organ/cyberimp/arm/toolkit/janitor,
- "Razorclaw Arm" = /obj/item/organ/cyberimp/arm/toolkit/razor_claws,
- "Excavator Arm" = /obj/item/organ/cyberimp/arm/toolkit/mining_drill,
- "Nutriment Pump Implant" = /obj/item/organ/cyberimp/chest/nutriment,
- "Flash Shielded Eyes" = /obj/item/organ/eyes/robotic/shield,
-))
-
-/datum/quirk/permitted_cybernetic
- name = "Permitted Cybernetic"
- desc = "You're allowed a cybernetic implant aboard the station, though this is information is available for security."
- value = 8
- mob_trait = TRAIT_PERMITTED_CYBERNETIC
- icon = FA_ICON_WRENCH
- /// Which implant to give the user
- var/obj/item/organ/desired_implant
-
-/datum/quirk_constant_data/implanted
- associated_typepath = /datum/quirk/permitted_cybernetic
- customization_options = list(/datum/preference/choiced/permitted_cybernetic)
-
-/datum/quirk/permitted_cybernetic/add_unique(client/client_source)
- desired_implant = GLOB.possible_quirk_implants[client_source?.prefs?.read_preference(/datum/preference/choiced/permitted_cybernetic)]
- if(isnull(desired_implant)) //Client gone or they chose a random implant
- desired_implant = GLOB.possible_quirk_implants[pick(GLOB.possible_quirk_implants)]
-
- var/mob/living/carbon/carbon_holder = quirk_holder
- if(carbon_holder.dna.species.type in GLOB.species_blacklist_no_humanoid)
- to_chat(carbon_holder, span_warning("Due to your species type, the [name] quirk has been disabled."))
- return
- if(carbon_holder.mind?.assigned_role.title == JOB_PRISONER)
- to_chat(carbon_holder, span_warning("Due to your job, the [name] quirk has been disabled."))
- return
- medical_record_text = "Patient has a company approved [desired_implant.name] installed within their body."
-
-/datum/quirk/permitted_cybernetic/post_add()
- var/obj/item/organ/implant = new desired_implant()
- var/mob/living/carbon/carbon_holder = quirk_holder
- if(implant.zone in GLOB.arm_zones)
- if(HAS_TRAIT(carbon_holder, TRAIT_LEFT_HANDED)) //Left handed person? Give them a leftie implant
- implant.zone = BODY_ZONE_L_ARM
- implant.slot = ORGAN_SLOT_LEFT_ARM_AUG
- implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED)
-
-/datum/quirk/permitted_cybernetic/add(client/client_source)
- . = ..()
- quirk_holder.update_implanted_hud()
-
-/datum/quirk/permitted_cybernetic/remove()
- var/mob/living/old_holder = quirk_holder
- . = ..()
- old_holder.update_implanted_hud()
-
-/mob/living/prepare_data_huds()
- . = ..()
- update_implanted_hud()
-
-/// Adds the HUD element if src has its trait. Removes it otherwise.
-/mob/living/proc/update_implanted_hud()
- var/image/quirk_holder = hud_list?[SEC_IMPLANT_HUD]
- if(isnull(quirk_holder))
- return
-
- var/datum/universal_icon/temporary_icon = uni_icon(icon, icon_state, dir)
- quirk_holder.pixel_y = temporary_icon.scale(32, -world.icon_size)
-
- if(iscarbon(src))
- var/mob/living/carbon/carbon_holder = src
- if(carbon_holder.dna.species.type in GLOB.species_blacklist_no_humanoid)
- return
- if(HAS_TRAIT(src, TRAIT_PERMITTED_CYBERNETIC))
- set_hud_image_active(SEC_IMPLANT_HUD)
- quirk_holder.icon = 'modular_doppler/overwrites/huds/hud.dmi'
- quirk_holder.icon_state = "hud_imp_quirk"
- else
- set_hud_image_inactive(SEC_IMPLANT_HUD)
diff --git a/modular_doppler/modular_species/species_types/slimes/code/roundstartslimes.dm b/modular_doppler/modular_species/species_types/slimes/code/roundstartslimes.dm
index 89ed6a0ddd8803..211e5dd653dc70 100644
--- a/modular_doppler/modular_species/species_types/slimes/code/roundstartslimes.dm
+++ b/modular_doppler/modular_species/species_types/slimes/code/roundstartslimes.dm
@@ -255,6 +255,7 @@
new_body.forceMove(get_turf(src))
new_body.blood_volume = BLOOD_VOLUME_SAFE+60
SSquirks.AssignQuirks(new_body, brainmob.client)
+ SSpowers.assign_powers(new_body, brainmob.client)
src.replace_into(new_body)
for(var/obj/item/bodypart/bodypart as anything in new_body.bodyparts)
if(!istype(bodypart, /obj/item/bodypart/chest))
diff --git a/modular_doppler/modular_vending/code/tg_vendors/sectech.dm b/modular_doppler/modular_vending/code/tg_vendors/sectech.dm
index fa3c8465a94de1..337adc2164c8ac 100644
--- a/modular_doppler/modular_vending/code/tg_vendors/sectech.dm
+++ b/modular_doppler/modular_vending/code/tg_vendors/sectech.dm
@@ -26,6 +26,7 @@
/obj/item/storage/box/alacran_dart/piercing = 3,
)
premium_doppler = list(
+ /obj/item/restraints/handcuffs/antiresonant = 6, // anti powers cuffs
/obj/item/gun/ballistic/automatic/schiebenmaschine = 30,
/obj/item/gun/ballistic/avispa_stingball_shooter = 5,
/obj/item/knife/combat/survival = 3,
diff --git a/modular_doppler/modular_vending/code/tg_vendors/wardrobes.dm b/modular_doppler/modular_vending/code/tg_vendors/wardrobes.dm
index 2834d2cb897ae2..154417d3873343 100644
--- a/modular_doppler/modular_vending/code/tg_vendors/wardrobes.dm
+++ b/modular_doppler/modular_vending/code/tg_vendors/wardrobes.dm
@@ -43,6 +43,8 @@
/obj/item/clothing/head/utility/hardhat/dblue = 2,
/obj/item/clothing/head/utility/hardhat/welding/dblue = 2,
/obj/item/clothing/head/utility/hardhat/red = 2,
+ /obj/item/clothing/suit/wizrobe/viszard = 3, // thaumaturge robes
+ /obj/item/clothing/head/wizard/viszard = 3 // thaumaturge robes
)
/obj/machinery/vending/wardrobe/atmos_wardrobe
@@ -133,7 +135,9 @@
/obj/item/clothing/head/cowboy/doppler/flat/cowl = 5,
/obj/item/clothing/head/cowboy/doppler/cattleman = 5,
/obj/item/clothing/head/cowboy/doppler/cattleman/wide = 5,
- /obj/item/riding_saddle/leather/blue = 3
+ /obj/item/riding_saddle/leather/blue = 3,
+ /obj/item/clothing/suit/wizrobe/secwiz = 3, // thaumaturge robes
+ /obj/item/clothing/head/wizard/secwiz = 3 // thaumaturge robes
)
diff --git a/modular_doppler/telepathy_qol/telepathy_quirk.dm b/modular_doppler/telepathy_qol/telepathy_quirk.dm
deleted file mode 100644
index 69d13f2d163970..00000000000000
--- a/modular_doppler/telepathy_qol/telepathy_quirk.dm
+++ /dev/null
@@ -1,32 +0,0 @@
-/datum/quirk/telepathic
- name = "Telepathic"
- desc = "You are able to transmit your thoughts to other living creatures."
- gain_text = span_purple("Your mind roils with psychic energy.")
- lose_text = span_notice("Mundanity encroaches upon your thoughts once again.")
- medical_record_text = "Patient has an unusually enlarged Broca's area visible in cerebral biology, and appears to be able to communicate via extrasensory means."
- value = 8
- icon = FA_ICON_HEAD_SIDE_COUGH
- /// Ref used to easily retrieve the action used when removing the quirk from silicons
- var/datum/weakref/tele_action_ref
-
-/datum/quirk/telepathic/add(client/client_source)
- if (iscarbon(quirk_holder))
- var/mob/living/carbon/human/human_holder = quirk_holder
- human_holder.dna.add_mutation(/datum/mutation/telepathy, MUTATION_SOURCE_QUIRK)
- else if (issilicon(quirk_holder))
- var/mob/living/silicon/robot_holder = quirk_holder
- var/datum/action/cooldown/spell/pointed/telepathy/tele_action = new
-
- tele_action.Grant(robot_holder)
- tele_action_ref = WEAKREF(tele_action)
-
-/datum/quirk/telepathic/remove()
- var/datum/action/cooldown/spell/pointed/telepathy/tele_action = tele_action_ref?.resolve()
- if (isnull(tele_action))
- tele_action_ref = null
- if (iscarbon(quirk_holder))
- var/mob/living/carbon/human/human_holder = quirk_holder
- human_holder.dna.remove_mutation(/datum/mutation/telepathy, MUTATION_SOURCE_QUIRK)
- else if (issilicon(quirk_holder) && !isnull(tele_action))
- QDEL_NULL(tele_action)
- tele_action_ref = null
diff --git a/tgstation.dme b/tgstation.dme
index 8ca1e32ee4c9d2..d283df9f23cada 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -660,6 +660,7 @@
#include "code\_globalvars\~doppler_globalvars\bitfields.dm"
#include "code\_globalvars\~doppler_globalvars\configuration.dm"
#include "code\_globalvars\~doppler_globalvars\objective.dm"
+#include "code\_globalvars\~doppler_globalvars\powers.dm"
#include "code\_globalvars\~doppler_globalvars\regexes.dm"
#include "code\_globalvars\~doppler_globalvars\religion.dm"
#include "code\_globalvars\~doppler_globalvars\text.dm"
@@ -6836,6 +6837,8 @@
#include "interface\fonts\vcr_osd_mono.dm"
#include "modular_doppler\_HELPERS\areas.dm"
#include "modular_doppler\_HELPERS\preferences.dm"
+#include "modular_doppler\_savefile_migration\code\_preferences_savefile.dm"
+#include "modular_doppler\_savefile_migration\code\powers_migration.dm"
#include "modular_doppler\accent_toggle\code\accent_toggle.dm"
#include "modular_doppler\accessable_storage\accessable_storage.dm"
#include "modular_doppler\accessable_storage\item.dm"
@@ -7497,18 +7500,183 @@
#include "modular_doppler\modular_mood\code\mood_events\dog_wag.dm"
#include "modular_doppler\modular_mood\code\mood_events\hotspring.dm"
#include "modular_doppler\modular_mood\code\mood_events\race_drink.dm"
-#include "modular_doppler\modular_powers\powers\_powers.dm"
-#include "modular_doppler\modular_powers\powers\core_powers.dm"
-#include "modular_doppler\modular_powers\powers\mortal_powers\augmented.dm"
-#include "modular_doppler\modular_powers\powers\mortal_powers\expert.dm"
-#include "modular_doppler\modular_powers\powers\mortal_powers\warfighter.dm"
-#include "modular_doppler\modular_powers\powers\resonant_powers\aberrant.dm"
-#include "modular_doppler\modular_powers\powers\resonant_powers\cultivator.dm"
-#include "modular_doppler\modular_powers\powers\resonant_powers\psyker.dm"
-#include "modular_doppler\modular_powers\powers\sorcerous_powers\enigmatist.dm"
-#include "modular_doppler\modular_powers\powers\sorcerous_powers\thaumaturge.dm"
-#include "modular_doppler\modular_powers\powers\sorcerous_powers\theologist.dm"
-#include "modular_doppler\modular_powers\preferences\powers_middleware.dm"
+#include "modular_doppler\modular_powers\code\_power.dm"
+#include "modular_doppler\modular_powers\code\_resonant_projectile.dm"
+#include "modular_doppler\modular_powers\code\powers_action.dm"
+#include "modular_doppler\modular_powers\code\powers_antimagic.dm"
+#include "modular_doppler\modular_powers\code\powers_helpers.dm"
+#include "modular_doppler\modular_powers\code\powers_living.dm"
+#include "modular_doppler\modular_powers\code\powers_prefs.dm"
+#include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm"
+#include "modular_doppler\modular_powers\code\powers_status_effect.dm"
+#include "modular_doppler\modular_powers\code\powers_subsystem.dm"
+#include "modular_doppler\modular_powers\code\powers_vv.dm"
+#include "modular_doppler\modular_powers\code\cargo\antiresonant_cuffs.dm"
+#include "modular_doppler\modular_powers\code\cargo\reality_anchor.dm"
+#include "modular_doppler\modular_powers\code\cargo\thaumaturgic_supplies.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_action.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_augment.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_augment_organ.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\auto_retriever.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\mental_shielding.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\pneumatic_arm.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\precognition_eyes.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\reagent_cannon.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\surgery\_premium_surgery.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\surgery\_premium_surgery_steps.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_action.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_power.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\creature_tamer.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\eye_for_ingredients.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\false_power.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\filthy_rich.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\heavy_lifter.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\hidden_powers.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\master_surgeon.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\obfuscate_voice.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\omnilingual.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\punt.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\rich.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\strider.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\expert\zoologist.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_command_action.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_action.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_power.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_assault.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_grit.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_recover.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\dual_wielder.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\explosives_specialist.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\focused_block.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\greater_tackler.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\heavy_slam.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\krav_maga.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\martial_artist.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\quick_draw.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\tackler.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\components\grenade_components.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_action.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_power.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root_anomalous.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root_beastial.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root_monstrous.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\armblade.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bioluminescence.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bloodhound.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\cocoon.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\darkvision.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\healing_factor.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\miasmic_conversion.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\radiosynthesis.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\resonant_immune.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\shapechange.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\shapechange_spider.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\shapechange_wolf.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\summonable.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\tail_sweep.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\vent_crawl.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\riftwalker\_riftwalker_datum.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\riftwalker\riftwalker.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\_web_craft_datum.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\_web_crafter_entries.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\binding_webs.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\snare_webs.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\tripwire_webs.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\web_crafter.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_action.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_alignment.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_energy.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_power.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_root.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\astraltouched_root.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\energy_dash.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\flamesoul_root.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\fly_like_a_shooting_star.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\from_friction_comes_flame.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\many_stars.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\set_fire_to_dry_hay.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\shadowwalker_root.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\travel_under_the_veil_of_night.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\vanish_unseen_into_shadow.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\levitate.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\manipulate.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\mirage.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\premonition.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\scrying.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\telepathy.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\telepathy_area.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\ward_mind.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\_psyker_event.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\brain_trauma.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\cardiac_arrest.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\magic_trauma.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\mirage_gangup.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\silence_trauma.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\telekinetic_backlash.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\tossed_around.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\dizziness.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\headache.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\nosebleed.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\twitching.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\exhaustion.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\eyes_bleed.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\hallucinate.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\vomit.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_organs\_psyker_organ.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_organs\chemotropic.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_organs\paracausal.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_root.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_spell.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\lodestone_legends.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_action.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_component.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_hemomancy.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_power.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_preperation.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\blend_for_me.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\brazen_bindings.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\conjure_rain.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\gale_blast.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\hemomancy.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\magic_barrage.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\phantasmal_tool.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\prestidigitation.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\sanguine_absorption.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\spell_preparation.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\vitalize_flora.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_affinity.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_robes.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_spell_focus.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_action.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_piety.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_power.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_revered.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_shared.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_twisted.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_unattended.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\culling.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\divine_protection.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\entropic_mending.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\flagellant.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\pious_prayer.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\purify.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike_upgrades.dm"
+#include "modular_doppler\modular_powers\code\security\reality_anchor.dm"
+#include "modular_doppler\modular_powers\code\security\resonant_cuffs.dm"
+#include "modular_doppler\modular_powers\code\signalers\martial_arts_signal.dm"
#include "modular_doppler\modular_preferences\chat_checks.dm"
#include "modular_doppler\modular_preferences\toggle_prefs.dm"
#include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm"
@@ -7545,8 +7713,6 @@
#include "modular_doppler\modular_quirks\paycheck_rations\code\rationpacks.dm"
#include "modular_doppler\modular_quirks\paycheck_rations\code\ticket_book.dm"
#include "modular_doppler\modular_quirks\paycheck_rations\code\tickets.dm"
-#include "modular_doppler\modular_quirks\permitted_cybernetic\permitted_cybernetic.dm"
-#include "modular_doppler\modular_quirks\permitted_cybernetic\code\preferences.dm"
#include "modular_doppler\modular_quirks\psychicholding\floating_items.dm"
#include "modular_doppler\modular_quirks\system_shock\system_shock.dm"
#include "modular_doppler\modular_quirks\tranquility\code\tranquility.dm"
@@ -7936,7 +8102,6 @@
#include "modular_doppler\taurs\code\taur_mechanics\taur_clothing_offset.dm"
#include "modular_doppler\taurs\code\taur_sprites\suits.dm"
#include "modular_doppler\telepathy_qol\telepathy_action.dm"
-#include "modular_doppler\telepathy_qol\telepathy_quirk.dm"
#include "modular_doppler\telepathy_qol\telepathy_reply_emote.dm"
#include "modular_doppler\temporary_flavor_text\temp_flavor_text.dm"
#include "modular_doppler\the-business\code\twitch.dm"
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx
index f7d9fd30d15f80..01853e138dbe88 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx
@@ -1,8 +1,8 @@
-import { Button, Section, Stack } from 'tgui-core/components';
+import { Box, Button, Collapsible, Section, Stack } from 'tgui-core/components';
import { useBackend } from '../../backend';
import { Powers } from './PowersMenu';
-import { PreferencesMenuData } from './types';
+import type { PreferencesMenuData } from './types';
type MortalPagePowers = {
handleCloseMortal: () => void;
@@ -10,6 +10,11 @@ type MortalPagePowers = {
export const MortalPage = (props: MortalPagePowers) => {
const { data } = useBackend();
+ const descriptionBlock = (text: string) => (
+
+ {text}
+
+ );
return (
@@ -41,6 +46,12 @@ export const MortalPage = (props: MortalPagePowers) => {
+ {descriptionBlock(
+ 'Warfighter, as the name implies, focuses almost exclusively on combat. It is split into three distinct categories, which are not mutually exclusive.\
+ \n\nCommander, which applies defensive buffs to targets through verbal or non-verbal command. The efficiency of these powers scales with whether the target is in your department and if you are a leadership role.\
+ \n\nEquipment Specialist, which specializes in using specific equipment in better ways. These usually require a specific type of item to get their mileage out of it, but some are more universally applicable than others, such as dual-wielding.\
+ \n\nMartial Artist, which powers up your unarmed prowess and grants you better strikes, access to martial arts and tackling.',
+ )}
{data.warfighter.map((val) => (
@@ -50,6 +61,10 @@ export const MortalPage = (props: MortalPagePowers) => {
+ {descriptionBlock(
+ 'Experts are broad in their capabilities, and often include the many phenomenal things anyone can do with perseverance, experience and a fair degree of luck. There are no broader mechanics in Expert.\
+ \n\nMost expert powers provide specialized bonuses that on their own may seem niche, but when presented with their use-case, can help you perform your actions come to fruition. An expert is only as good as their creativity.',
+ )}
{data.expert.map((val) => (
@@ -59,6 +74,14 @@ export const MortalPage = (props: MortalPagePowers) => {
+ {descriptionBlock(
+ 'The flesh is weak; Augmented lets you tweak and adjust your physical body with specialized augments, granting you capabilities on-par with resonance, in a technological manner.\
+ \n\nAugmented grants you augments at round-start, but is is beholden to a fair few restrictions and drawbacks; you can only have one augment per body part, and you are susceptible to EMPs, disabling your augments and possibly having adverse side-effects.\
+ \n\nA subcategory of powers exists within Augmented; Premium Augments. These are commercialized and specialized augments made out of propieretary parts, making them unable to be built on the station. \
+ These possess a quality meter, which dictates how much mileage you get out of your Premium Augments. The higher the percentage, the stronger their effects. \
+ Through robotic surgery, these can be maintained and refurbished, restoring their quality. Once quality reaches 0%, you are required to refurbish it for it to be functional.\
+ \nWhether you wish to burn through your augments and make repeat roboticist visits, or try to be more diligent with it, is up to you. Keep in mind as well; your powers can be physically stolen!',
+ )}
{data.augmented.map((val) => (
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx
index db43a7056965ee..83ab40fb32a6cf 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx
@@ -1,11 +1,49 @@
-import { Box, Button, Image, Section, Stack } from 'tgui-core/components';
+import { filter } from 'es-toolkit/compat';
+import { useState } from 'react';
+import {
+ Box,
+ Button,
+ Dropdown,
+ Floating,
+ Image,
+ Section,
+ Stack,
+} from 'tgui-core/components';
import { resolveAsset } from '../../assets';
import { useBackend } from '../../backend';
-import { PreferencesMenuData } from './types';
+import { PreferenceList } from './CharacterPreferences/MainPage';
+import type { PreferencesMenuData } from './types';
+
+function getCorrespondingPreferences(
+ customizationOptions: string[],
+ relevantPreferences: Record,
+) {
+ return Object.fromEntries(
+ filter(Object.entries(relevantPreferences), ([key]) =>
+ customizationOptions.includes(key),
+ ),
+ );
+}
export const Powers = (props) => {
- const { act } = useBackend();
+ const { act, data } = useBackend();
+ const [customizationExpanded, setCustomizationExpanded] = useState(false);
+
+ const customizationOptions = props.power.customization_options || [];
+ const hasCustomization =
+ props.power.customizable &&
+ props.power.has_power &&
+ customizationOptions.length > 0;
+ const customizationPreferences = hasCustomization
+ ? getCorrespondingPreferences(
+ customizationOptions,
+ data.character_preferences.manually_rendered_features,
+ )
+ : {};
+ const hasExpandableCustomization =
+ hasCustomization && Object.entries(customizationPreferences).length > 0;
+
return (
{
}}
>
- {props.power.description}
+ {/* Allows for newlines in power descs */}
+ {String(props.power.description)
+ .split('\n')
+ .map((line, i, lines) => (
+
+ {line}
+ {i < lines.length - 1 && }
+
+ ))}
+
+ ) : null}
+
+
);
};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/Resonant.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/Resonant.tsx
index 65527fadfd1489..f5cfb5f7d55cfa 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/Resonant.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/Resonant.tsx
@@ -1,14 +1,19 @@
-import { Button, Section, Stack } from 'tgui-core/components';
+import { Box, Button, Collapsible, Section, Stack } from 'tgui-core/components';
import { useBackend } from '../../backend';
import { Powers } from './PowersMenu';
-import { PreferencesMenuData } from './types';
+import type { PreferencesMenuData } from './types';
type ResonantPowerProps = {
handleCloseResonant: () => void;
};
export const ResonantPage = (props: ResonantPowerProps) => {
const { data } = useBackend();
+ const descriptionBlock = (text: string) => (
+
+ {text}
+
+ );
return (
@@ -40,6 +45,15 @@ export const ResonantPage = (props: ResonantPowerProps) => {
+ {descriptionBlock(
+ 'The mind grows stronger, and your body twisted to facilitate it, as much as it can handle. Psykers uses classically psychic abilities such as telekenisis and telepathy, mastering the domain over the mind.\
+ \n\nMechanically, this manifests in their special mechanic; Stress. You have an unique organ inside you called a Paracusal Gland. This is in-essence the liver of your brain; it is there to handle chemical and physical strain put on your body by your mental powers.\
+ Using your powers generates Stress proportional to the impact of your powers. Whilst you are under the Stress Threshold, it passively diminishes over-time, but should you go over it, you start experiencing negative events and your stress will not decay without using\
+ the special Meditate action you were given (or other abilities, depending on your root power). You are never truly certain of how much Stress you have, only the estimates given by your body violently reacting to the pressure.\
+ \n\nExceeding the threshold causes at first mild symptons, such as headaches, jittering and more. Continued overuse expands it to severe symptoms such as bleeding eyes, vomiting and more. Should you continue past this point, you will suffer a\
+ catastrophic breakdown, often inflicting permanent, long-lasting injuries on you, and reseting your Stress consequently.\
+ \n\nIn exchange for this Stress, almost none of your abilities have cooldowns or other limiting factors; Stress is your sole-limiting resource. Manage it well.',
+ )}
{data.psyker.map((val) => (
@@ -49,6 +63,15 @@ export const ResonantPage = (props: ResonantPowerProps) => {
+ {descriptionBlock(
+ "Your body is a temple; one that strengthens from aligning it with resonant energies. By associating with specific phenomena, you gain supernatural powers, allowing you resist blows and strike with your fists as if it were a blade.\
+ \n\nCultivator builds up a resource called Energy, which is the cost for a variety of their powers. Most prominently it is used to fuel a state called Alignment. Once you enter this heightened state of Alignment, you gain passive effects and heightened damage,\
+ turning you into a force to be reckoned with regardless of your current equipment. Many of your powers require Alignment to be active and cost Energy in turn, but have some incredibly powerful effects in turn.\
+ \n\nEnergy is build up through two methods; Meditation, and Aura. Meditation can be done at any point, engulfing you in light as you attune with the passive Resonance in the air. This slowly fills your energy, but prevents you from doing anything else.\
+ Meanwhile, Aura lets you harvest it passively from an environment with which you align. If your Alignment is Astral Touched, that means your Energy builds from seeing starlight and other space-based phenomena, whilst something such as Flame soul energizes from seeing exposed flames.\
+ You can combine these two methods; an Astral-Touched Cultivator energizes quickly while meditating before the stars. Your Energy caps out at 1000, and most Alignments require at least 200 to activate, with a hefty upkeep (you cannot gain Energy while in Alignment).\
+ \n\nYou won't be able to enter your heightened state often, but once you do, you will wield great powers. Wisdom is knowing when to wield it.",
+ )}
{data.cultivator.map((val) => (
@@ -58,6 +81,15 @@ export const ResonantPage = (props: ResonantPowerProps) => {
+ {descriptionBlock(
+ "Aberrant is a collection of the odd, the excentric and the extraordinary. It is home to many categories, of various capabilities that don't belong strongly in any particular path. These three categories are:\
+ \n\nBeastial; people who have the trait and qualities of animals. Whether being able to shift into one, or mimmicking their biological traits, they wield these along with their existing biology to enhance their capabilities.\
+ Beastial abilities often have a hunger cost and cannot be used while starving.\
+ \n\nAberrant; whose traits are not of animals, but of monsters. The ability to regenerate any wounds, to grow blades for arms. The qualities of monsters that are often the tail of rumor and folk-lore. They often resist any and all\
+ harm cast upon them; and often are the truly unstopable monsters people think about.\
+ \n\nAnomalous; whose very existence is unexplainable through sciences. The ability to end anomalies at a touch, the ability to walk through rifts in realities, or interacting in inexplicable ways with reality, such as healing from radiation poisoning.\
+ These oddities work in their own way, and wield their poorly understood powers in their day-to-day work.",
+ )}
{data.aberrant.map((val) => (
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/Sorcerous.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/Sorcerous.tsx
index 5306270a3e62d0..b8cc94dd6e9279 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/Sorcerous.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/Sorcerous.tsx
@@ -1,8 +1,8 @@
-import { Button, Section, Stack } from 'tgui-core/components';
+import { Box, Button, Collapsible, Section, Stack } from 'tgui-core/components';
import { useBackend } from '../../backend';
import { Powers } from './PowersMenu';
-import { PreferencesMenuData } from './types';
+import type { PreferencesMenuData } from './types';
type SorcerousPageProps = {
handleCloseSorcerous: () => void;
@@ -10,6 +10,11 @@ type SorcerousPageProps = {
export const SorcerousPage = (props: SorcerousPageProps) => {
const { data } = useBackend();
+ const descriptionBlock = (text: string) => (
+
+ {text}
+
+ );
return (
@@ -41,6 +46,18 @@ export const SorcerousPage = (props: SorcerousPageProps) => {
+ {descriptionBlock(
+ "Magic, wizards, sages. The most classical depiction of magic in folklore and history is based on perception, and people's believe that a person with a pointy-hat can cast a spell. To be a Thaumaturge, you have to act like a Thaumaturge.\
+ \nThaumaturgy has two core components; Spell Preperation, and Affinity.\
+ \n\nTo start off, your spells are limited not by cooldowns, but by charges. Every point you put in the Thaumaturge power grants you 2 points of Mana. This is used by your Spell Preperation power, which allows you to allocate\
+ your Mana to spells to charge them. The cost to gain the Power is the same as to prepare the Charges. Once you set your spells, that are the amount of charges you have. Once you run out of charges, you can't use \
+ that power again until you sleep for a certain duration. Not just any sleep will do; you need a catalyst on you to shape your dreams called an Arcane Focus. You start the round with it, and you'd best keep it safe, as without\
+ it you won't ever be able to restore your spells.\
+ \n\nFuthermore, you have Affinity to both scale and use your powers. Your Arcane Focus has a value called Affinity, which determines the potency of your spells. Some spells require a certain amount of affinity to wield;\
+ and you gain it by holding the affinity item. Exceeding the required affinity usually grants additional bonuses with spells, such as higher damage (elaborated per spell). Affinity also exists on other items and clothes; \
+ dressing like a Wizard with a wizard costume will grant you Affinity as well. Affinity does not stack; you take the highest source. You can examine items to see how much Affinity they have, if any. Usually anything \
+ you'd see on a druid, wizard, bard or other magically inclined person in folklore will grant you Affinity.",
+ )}
{data.thaumaturge.map((val) => (
@@ -50,6 +67,7 @@ export const SorcerousPage = (props: SorcerousPageProps) => {
+ {descriptionBlock('Enigmatist is still in development!')}
{data.enigmatist.map((val) => (
@@ -59,6 +77,13 @@ export const SorcerousPage = (props: SorcerousPageProps) => {
+ {descriptionBlock(
+ 'Whilst Thaumaturgy is rooted in the perception of others on you, Theology is rooted in your perception of self. To act holy and perform miracles is rooted in firm believe and willpower.\
+ \nTheologists are spread across several categories, each of which have a base power that heals the wounds of others. In what form and with what method differs per power, but it will always grant you a measure of Piety.\
+ \n\nPiety is a measure of your good deeds; it is gained by healing others with your powers, proportional to the healing (as long as it is sentient, healing animals is not pious, alas). These are in turn used to fuel other\
+ theologist powers, such as being able to bless weapons, randomly resist blows and other powers specific to your path. It has a maximum of 50.\
+ \n\nUniquely, the Chaplain gains additional powers and bonuses with certain powers, and has double the maximum amount of Piety. Theologist powers and not necessairly related to divinity; they are rooted in firm believe themselves, whether in said divinity or their deeds.',
+ )}
{data.theologist.map((val) => (
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/beastial_diet.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/beastial_diet.tsx
new file mode 100644
index 00000000000000..90016069b5484b
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/beastial_diet.tsx
@@ -0,0 +1,8 @@
+import type { FeatureChoiced } from '../../base';
+import { FeatureDropdownInput } from '../../dropdowns';
+
+export const beastial_diet: FeatureChoiced = {
+ name: 'Beastkindred Diet',
+ description: 'Diet preference.',
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_color.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_color.tsx
new file mode 100644
index 00000000000000..a3f254cf1a2e1f
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_color.tsx
@@ -0,0 +1,7 @@
+import { FeatureColorInput, type Feature } from '../../base';
+
+export const bioluminescence_color: Feature = {
+ name: 'Bioluminescence Color',
+ description: 'Chosen glow color.',
+ component: FeatureColorInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_size.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_size.tsx
new file mode 100644
index 00000000000000..db2675e99d6ee1
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_size.tsx
@@ -0,0 +1,8 @@
+import type { FeatureChoiced } from '../../base';
+import { FeatureDropdownInput } from '../../dropdowns';
+
+export const bioluminescence_size: FeatureChoiced = {
+ name: 'Bioluminescence Size',
+ description: 'Chosen glow size.',
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/false_power.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/false_power.tsx
new file mode 100644
index 00000000000000..279e9b2cdc11bc
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/false_power.tsx
@@ -0,0 +1,18 @@
+import {
+ type Feature,
+ type FeatureChoiced,
+ FeatureShortTextInput,
+} from '../../base';
+import { FeatureDropdownInput } from '../../dropdowns';
+
+export const false_power_entry: Feature = {
+ name: 'False Power Entry',
+ description: 'Custom security record text (max 100 chars).',
+ component: FeatureShortTextInput,
+};
+
+export const false_power_severity: FeatureChoiced = {
+ name: 'False Power Severity',
+ description: 'Threat severity shown in security records.',
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/premonition.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/premonition.tsx
new file mode 100644
index 00000000000000..f35255bc052db4
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/premonition.tsx
@@ -0,0 +1,14 @@
+import { Feature, FeatureShortTextInput, type FeatureChoiced } from '../../base';
+import { FeatureDropdownInput } from '../../dropdowns';
+
+export const premonition_keyword: Feature = {
+ name: 'Premonition Keyword',
+ description: 'Phrase that triggers your premonition.',
+ component: FeatureShortTextInput,
+};
+
+export const premonition_emote: FeatureChoiced = {
+ name: 'Premonition Emote',
+ description: 'Emote triggered by your premonition.',
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_form.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_form.tsx
new file mode 100644
index 00000000000000..52f2be51800649
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_form.tsx
@@ -0,0 +1,8 @@
+import type { FeatureChoiced } from '../../base';
+import { FeatureDropdownInput } from '../../dropdowns';
+
+export const shapechange_form: FeatureChoiced = {
+ name: 'Shapechange',
+ description: 'Chosen animal form.',
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_spider_form.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_spider_form.tsx
new file mode 100644
index 00000000000000..85158014827b5a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_spider_form.tsx
@@ -0,0 +1,8 @@
+import type { FeatureChoiced } from '../../base';
+import { FeatureDropdownInput } from '../../dropdowns';
+
+export const shapechange_spider_form: FeatureChoiced = {
+ name: 'Shapechange: Spider',
+ description: 'Chosen spider form.',
+ component: FeatureDropdownInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_keyword.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_keyword.tsx
new file mode 100644
index 00000000000000..6701936f8a7dff
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_keyword.tsx
@@ -0,0 +1,7 @@
+import { Feature, FeatureShortTextInput } from '../../base';
+
+export const summonable_keyword: Feature = {
+ name: 'Summonable Keyword',
+ description: 'Single word used to summon you.',
+ component: FeatureShortTextInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_rune_color.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_rune_color.tsx
new file mode 100644
index 00000000000000..0864302baa5c04
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_rune_color.tsx
@@ -0,0 +1,7 @@
+import { FeatureColorInput, type Feature } from '../../base';
+
+export const summonable_rune_color: Feature = {
+ name: 'Summonable Color',
+ description: 'Rune and spotlight color.',
+ component: FeatureColorInput,
+};
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts
index 80f38d9efcdb70..eeb9d0bf4a83ed 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts
@@ -107,11 +107,24 @@ export type Power = {
name: string;
icon: string;
cost: number;
+ has_power?: boolean;
state: string;
word: string;
color: string;
powertype: (string | null)[];
rootpower: (string | null)[];
+ required_powers?: string[];
+ required_allow_any?: boolean;
+ required_allow_subtypes?: boolean;
+ customizable?: boolean;
+ customization_options?: string[];
+ augment?: {
+ location?: string | null;
+ is_arm?: boolean;
+ assignment?: string | null;
+ left_blocked?: boolean;
+ right_blocked?: boolean;
+ } | null;
};
// DOPPLER EDIT END
@@ -230,6 +243,8 @@ export type PreferencesMenuData = {
augmented: Power[];
power_points: number;
+ augment_location?: string | null;
+
// DOPPLER EDIT END
keybindings: Record;
overflow_role: string;
diff --git a/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx b/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx
index 6508d1182b5977..1fe2209ff69690 100644
--- a/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx
+++ b/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx
@@ -13,8 +13,8 @@ import {
import { CharacterPreview } from '../common/CharacterPreview';
import { EditableText } from '../common/EditableText';
-import { CRIMESTATUS2COLOR, CRIMESTATUS2DESC } from './constants';
import { CrimeWatcher } from './CrimeWatcher';
+import { CRIMESTATUS2COLOR, CRIMESTATUS2DESC } from './constants';
import { getSecurityRecord } from './helpers';
import { RecordPrint } from './RecordPrint';
import type { SecurityRecordsData } from './types';
@@ -69,6 +69,9 @@ const RecordInfo = (props) => {
wanted_status,
voice,
// DOPPLER EDIT START - records & flavor text
+ power_notes,
+ power_notes_minor,
+ power_notes_major,
past_general_records,
past_security_records,
age_chronological,
@@ -77,6 +80,12 @@ const RecordInfo = (props) => {
const [isValid, setIsValid] = useState(true);
+ // DOPPLER ADDITION START - Power sec notes
+ const major_power_notes_array = power_notes_major?.split(' ') || [];
+ const minor_power_notes_array = power_notes_minor?.split(' ') || [];
+ const has_power_notes =
+ major_power_notes_array.length > 0 || minor_power_notes_array.length > 0;
+ // DOPPLER ADDITION END
const hasValidCrimes = !!crimes.find((crime) => !!crime.valid);
return (
@@ -222,6 +231,19 @@ const RecordInfo = (props) => {
/>
{/* DOPPLER EDIT START - records & flavor text */}
+
+ {major_power_notes_array.map((power, index) => (
+
+ • {power}
+
+ ))}
+ {minor_power_notes_array.map((power, index) => (
+ • {power}
+ ))}
+ {!has_power_notes && (
+ • {power_notes || 'No powers declared.'}
+ )}
+
{past_general_records || 'N/A'}
diff --git a/tgui/packages/tgui/interfaces/SecurityRecords/types.ts b/tgui/packages/tgui/interfaces/SecurityRecords/types.ts
index ff5df1fb560bb9..e96b5c2a850dcc 100644
--- a/tgui/packages/tgui/interfaces/SecurityRecords/types.ts
+++ b/tgui/packages/tgui/interfaces/SecurityRecords/types.ts
@@ -29,6 +29,9 @@ export type SecurityRecord = {
wanted_status: string;
voice: string;
// DOPPLER EDIT START - records & flavor text
+ power_notes: string;
+ power_notes_minor: string;
+ power_notes_major: string;
past_general_records: string;
past_security_records: string;
// DOPPLER EDIT END
diff --git a/tgui/packages/tgui/interfaces/ThaumaturgeSpellPrep.tsx b/tgui/packages/tgui/interfaces/ThaumaturgeSpellPrep.tsx
new file mode 100644
index 00000000000000..e5e5781d3548d8
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/ThaumaturgeSpellPrep.tsx
@@ -0,0 +1,129 @@
+import { Box, Button, DmIcon, Section, Stack } from 'tgui-core/components';
+
+import { useBackend } from '../backend';
+import { Window } from '../layouts';
+
+type SpellEntry = {
+ key: string;
+ name: string;
+ charges: number;
+ max_charges: number;
+ prep_cost: number;
+ icon?: string;
+ icon_state?: string;
+};
+
+type Data = {
+ tguitheme?: string;
+ mana_remaining: number;
+ mana_total: number;
+ mana_max: number;
+ first_time_preperation: boolean;
+ spell_count: number;
+ spells: SpellEntry[];
+};
+
+export const ThaumaturgeSpellPrep = (_props) => {
+ const { data, act } = useBackend();
+ const spells = data.spells || [];
+
+ return (
+
+
+
+
+ Mana remaining: {data.mana_remaining} /{' '}
+ {data.mana_total}
+
+
+
+
+
+ {spells.map((spell) => (
+
+
+
+
+
+
+
+
+
+
+ {spell.charges}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {spell.name}
+ Cost: {spell.prep_cost}
+
+
+
+
+ ))}
+
+
+
+
+
+
+ {data.first_time_preperation ? (
+
+ Preparing spells for the first time applies the charges
+ instantly!
+
+ ) : (
+
+ Your prepared charges will be applied the next time you sleep.
+
+ )}
+
+
+
+
+
+
+
+
+ );
+};