diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm
index c9036a54b7704b..ff2ab4aea1726b 100644
--- a/code/__DEFINES/~doppler_defines/powers.dm
+++ b/code/__DEFINES/~doppler_defines/powers.dm
@@ -10,29 +10,64 @@
#define POWER_PRIORITY_BASIC "Basic"
#define POWER_PRIORITY_ADVANCED "Advanced"
+/// Designations whne referring to arcehtypes.
#define POWER_ARCHETYPE_SORCEROUS "Sorcerous"
#define POWER_ARCHETYPE_RESONANT "Resonant"
#define POWER_ARCHETYPE_MORTAL "Mortal"
+/// UI/display sort order for power archetypes.
+#define POWER_ARCHETYPE_SORT_SORCEROUS 10
+#define POWER_ARCHETYPE_SORT_RESONANT 20
+#define POWER_ARCHETYPE_SORT_MORTAL 30
+
+/// Designations when referring to paths.
#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_IMBUED "Imbued"
#define POWER_PATH_WARFIGHTER "Warfighter"
#define POWER_PATH_EXPERT "Expert"
#define POWER_PATH_AUGMENTED "Augmented"
+#define POWER_PATH_IRREGULAR "Irregular"
+
+/// UI/display sort order for power paths within their archetype.
+#define POWER_PATH_SORT_PRIMARY 10
+#define POWER_PATH_SORT_SECONDARY 20
+#define POWER_PATH_SORT_TERTIARY 30
+#define POWER_PATH_SORT_QUATERNARY 40
+/// Color association per power. Generally speaking if you want to use colors, use these.
#define POWER_COLOR_THAUMATURGE "#7266dd"
-#define POWER_COLOR_ENIGMATIST "#26A300"
-#define POWER_COLOR_THEOLOGIST "#ddd166"
-#define POWER_COLOR_PSYKER "#FF3CC8"
+#define POWER_COLOR_ENIGMATIST "#439c27"
+#define POWER_COLOR_THEOLOGIST "#d1c029"
+#define POWER_COLOR_PSYKER "#b94398"
#define POWER_COLOR_CULTIVATOR "#66c5dd"
#define POWER_COLOR_ABERRANT "#4F3A57"
-#define POWER_COLOR_WARFIGHTER "#cc3d3d"
+#define POWER_COLOR_IMBUED "#e9874f"
+#define POWER_COLOR_WARFIGHTER "#ac2222"
#define POWER_COLOR_EXPERT "#38495C"
-#define POWER_COLOR_AUGMENTED "#7C8287"
+#define POWER_COLOR_AUGMENTED "#6b6652"
+#define POWER_COLOR_IRREGULAR "#cacaca"
+
+/// Bitflags to determine what anti-magic a power interacts with/what type of magic it is.
+#define POWER_MAGIC_STANDARD (1<<0)
+#define POWER_MAGIC_MENTAL (1<<1)
+#define POWER_MAGIC_UNHOLY (1<<2)
+#define POWER_MAGIC_SCRYING (1<<3)
+
+// Signals for power actions and power-holder power mutations.
+/// 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 when a power is added to a mob's power list: (datum/power/added_power)
+#define COMSIG_MOB_POWER_ADDED "mob_power_added"
+/// Sent when a power is removed from a mob's power list: (datum/power/removed_power)
+#define COMSIG_MOB_POWER_REMOVED "mob_power_removed"
+
/// Any traits granted by powers.
#define POWER_TRAIT "power_trait"
@@ -133,6 +168,8 @@
#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
+/// Sent by thaumaturge get_affinity for external affinity riders: (datum/action/cooldown/power/thaumaturge/action)
+#define COMSIG_THAUMATURGE_AFFINITY_QUERY "thaumaturge_affinity_query"
/**
* SORCEROUS: ENIGMATIST
@@ -297,8 +334,29 @@
* All defines related to the aberrant powers.
*/
+/// The value that the below numbers scale off of, because hunger is a little arbitrary. The reason why is as follows:
+/// Starving is 150, Hungry is 250, Fed is 350, Well Fed is 450 Full is 550 and Fat is 600. Fat is a negative soft-cap (you can go above 600) and starving is effectively 0 for our power.
+/// So our effective 'range' we want to consider is 150-550 (Starving-Full). We calculate this delta and that is effectively the 0%-100% range of our power's 'resource'
+#define ABERRANT_HUNGER_COST_BASE (NUTRITION_LEVEL_FULL - NUTRITION_LEVEL_STARVING)
+
+// Hunger costs
+#define ABERRANT_HUNGER_TRIVIAL (ABERRANT_HUNGER_COST_BASE / 100)
+#define ABERRANT_HUNGER_MINOR (ABERRANT_HUNGER_COST_BASE / 10)
+#define ABERRANT_HUNGER_MODERATE (ABERRANT_HUNGER_COST_BASE / 5)
+#define ABERRANT_HUNGER_MAJOR (ABERRANT_HUNGER_COST_BASE / 2)
+#define ABERRANT_HUNGER_EXTREME (ABERRANT_HUNGER_COST_BASE)
+
+/**
+ * RESONANT: IMBUED
+ * All defines related to the imbued powers.
+ */
+
+/// Fired by /datum/power/imbued_root/enchanted to collect additive percent bonuses to cooldown recovery.
+/// Args: (list/recovery_bonus_percents)
+#define COMSIG_IMBUED_ENCHANTED_RECOVERY_MODIFIERS "imbued_enchanted_recovery_modifiers"
+
// Trait that lets you use the riftwalker mechanic.
-#define TRAIT_ABERRANT_RIFTWALKER "riftwalker"
+#define TRAIT_IMBUED_RIFTWALKER "riftwalker"
/**MORTAL DEFINES
* I'm literally just using this to define Breacher Knuckle right now
diff --git a/code/__DEFINES/~doppler_defines/signals.dm b/code/__DEFINES/~doppler_defines/signals.dm
index f8e36d17a90bd9..cfd858002c933b 100644
--- a/code/__DEFINES/~doppler_defines/signals.dm
+++ b/code/__DEFINES/~doppler_defines/signals.dm
@@ -1,19 +1,12 @@
///Fired in combat_indicator.dm, used for syncing CI between mech and pilot
#define COMSIG_MOB_CI_TOGGLED "mob_ci_toggled"
-// Power signals
/// Sent when an obj/item calls item_use_power: (use_amount, user, check_only)
#define COMSIG_ITEM_POWER_USE "item_use_power"
#define NO_COMPONENT NONE
#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/modular_doppler/genemod_quirk/code/genemod_quirk.dm b/modular_doppler/genemod_quirk/code/genemod_quirk.dm
index 1c399a928e1a82..19f6ed7222ccb6 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/telekinesis, /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, /datum/mutation/inexorable)
var/list/genemods = list()
for (var/datum/mutation/mut as anything in subtypesof(/datum/mutation))
diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm
index 7d2a0553be47d0..b03275ee266c1d 100644
--- a/modular_doppler/modular_powers/code/_power.dm
+++ b/modular_doppler/modular_powers/code/_power.dm
@@ -62,6 +62,14 @@
/// 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
+ /// Icon used inside the powers menu to show this power. If this is left empty, it will default to action_path, and if both are active, this overrides.
+ /// You don't need to set this if your action_path is fine.
+ var/menu_icon
+ /// icon state that matches the icon
+ var/menu_icon_state
+ /// Bitflags which determine what type of magic the power is. This is largely used in the powers menu to convey what anti-magics they may trigger, but is also used by some powers to check if a certain power is a certain type of magic.
+ var/magic_flags = NONE
+
/datum/power/New()
. = ..()
for(var/trait in no_process_traits)
@@ -101,6 +109,7 @@
power_holder = new_holder
power_holder.powers += src
+ SEND_SIGNAL(power_holder, COMSIG_MOB_POWER_ADDED, src)
// If we weren't passed a client source try to use a present one
client_source ||= power_holder.client
@@ -140,6 +149,7 @@
UnregisterSignal(power_holder, process_update_signals)
power_holder.powers -= src
+ SEND_SIGNAL(power_holder, COMSIG_MOB_POWER_REMOVED, src)
if(mob_trait && !QDELETED(power_holder))
REMOVE_TRAIT(power_holder, mob_trait, POWER_TRAIT)
diff --git a/modular_doppler/modular_powers/code/misc/stomach_resource_conversion.dm b/modular_doppler/modular_powers/code/misc/stomach_resource_conversion.dm
new file mode 100644
index 00000000000000..6d4a3a001341ca
--- /dev/null
+++ b/modular_doppler/modular_powers/code/misc/stomach_resource_conversion.dm
@@ -0,0 +1,67 @@
+/*
+ Handles satiety costs and deductions across different unique types of stomachs, translating the resource-cost to the same amount for that stomach percentually.
+*/
+
+/// Convers the satiety cost from base satiety into an equivelant amount (percentually) in its own custom mechanics.
+/obj/item/organ/stomach/proc/get_satiety_cost(amount)
+ return amount
+
+/// Returns TRUE/FALSE depending on if a mob can afford the satiety cost.
+/obj/item/organ/stomach/proc/can_afford_satiety_cost(amount)
+ if(amount <= 0)
+ return TRUE
+ if(!iscarbon(owner))
+ return FALSE
+
+ var/mob/living/carbon/carbon_owner = owner
+ return carbon_owner.nutrition > (NUTRITION_LEVEL_STARVING + get_satiety_cost(amount))
+
+/// Adjusts satiety by the given amount.
+/obj/item/organ/stomach/proc/adjust_satiety(amount)
+ if(amount <= 0 || !iscarbon(owner))
+ return
+
+ var/mob/living/carbon/carbon_owner = owner
+ carbon_owner.adjust_nutrition(-get_satiety_cost(amount))
+
+/*
+ Special stomach versions below.
+*/
+
+/obj/item/organ/stomach/charging/get_satiety_cost(amount)
+ return scale_satiety_cost(amount, CHARGING_STOMACH_CHARGE_FULL - CHARGING_STOMACH_CHARGE_LOW)
+
+/obj/item/organ/stomach/charging/can_afford_satiety_cost(amount)
+ if(amount <= 0)
+ return TRUE
+
+ var/obj/item/stock_parts/power_store/charging_cell = get_cell()
+ return charging_cell?.charge() > (CHARGING_STOMACH_CHARGE_LOW + get_satiety_cost(amount))
+
+/obj/item/organ/stomach/charging/adjust_satiety(amount)
+ if(amount <= 0)
+ return
+
+ adjust_charge(-get_satiety_cost(amount))
+
+/obj/item/organ/stomach/ethereal/get_satiety_cost(amount)
+ return scale_satiety_cost(amount, ETHEREAL_CHARGE_FULL - ETHEREAL_CHARGE_LOWPOWER)
+
+/obj/item/organ/stomach/ethereal/can_afford_satiety_cost(amount)
+ if(amount <= 0)
+ return TRUE
+
+ return cell?.charge() > (ETHEREAL_CHARGE_LOWPOWER + get_satiety_cost(amount))
+
+/obj/item/organ/stomach/ethereal/adjust_satiety(amount)
+ if(amount <= 0)
+ return
+
+ adjust_charge(-get_satiety_cost(amount))
+
+/obj/item/organ/stomach/proc/scale_satiety_cost(amount, alternate_resource_capacity)
+ var/base_nutrition_capacity = NUTRITION_LEVEL_FULL - NUTRITION_LEVEL_STARVING
+ if(amount <= 0 || alternate_resource_capacity <= 0 || base_nutrition_capacity <= 0)
+ return amount
+
+ return amount / base_nutrition_capacity * alternate_resource_capacity
diff --git a/modular_doppler/modular_powers/code/misc/thaumaturgic_supplies.dm b/modular_doppler/modular_powers/code/misc/thaumaturgic_supplies.dm
index 8152e6034c8c8f..bd554367c4f264 100644
--- a/modular_doppler/modular_powers/code/misc/thaumaturgic_supplies.dm
+++ b/modular_doppler/modular_powers/code/misc/thaumaturgic_supplies.dm
@@ -31,7 +31,13 @@
/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
+ /obj/item/clothing/suit/wizrobe/viszard,
+ /obj/item/clothing/suit/wizrobe/durathread,
+ /obj/item/clothing/suit/wizrobe/durathread/earth,
+ /obj/item/clothing/suit/wizrobe/durathread/electric,
+ /obj/item/clothing/suit/wizrobe/durathread/fire,
+ /obj/item/clothing/suit/wizrobe/durathread/ice,
+ /obj/item/clothing/suit/wizrobe/durathread/necro,
)
/// There's a small chance that we manage to sneak in real wizard robes, in percentages.
diff --git a/modular_doppler/modular_powers/code/power_paths.dm b/modular_doppler/modular_powers/code/power_paths.dm
new file mode 100644
index 00000000000000..7d56e44e26d677
--- /dev/null
+++ b/modular_doppler/modular_powers/code/power_paths.dm
@@ -0,0 +1,221 @@
+/// Singleton metadata for a selectable power path and its preferences UI presentation.
+/datum/power_path
+ abstract_type = /datum/power_path
+
+ /// Stable UI key used throughout TGUI and middleware maps.
+ var/path_key
+ /// The POWER_PATH_* define value powers in this path point at.
+ var/power_path
+ /// The archetype metadata this path belongs to.
+ var/datum/power_archetype/archetype_type
+ /// Ordering within the archetype.
+ var/path_sort_order = 0
+ /// Human-readable name for the path.
+ var/display_name
+ /// Optional icon asset from tgui/packages/tgui/assets.
+ var/icon_asset_name
+ /// Whether this path should be clickable in preferences.
+ var/is_available = TRUE
+ /// Path-specific mechanics copy shown in the preferences page.
+ var/mechanics_text = ""
+ /// Path-specific overview copy shown in the preferences page.
+ var/overview_text = ""
+ /// Accent color used by the preferences page.
+ var/theme_color = "#ffffff"
+ /// Whether this path bypasses the 2-path selection limit.
+ var/path_limit_exempt = FALSE
+
+/datum/power_path/New()
+ . = ..()
+
+ ASSERT(abstract_type != type, "Attempted to instantiate abstract power path datum [type]")
+ ASSERT(!isnull(path_key), "Power path datum [type] is missing path_key")
+ ASSERT(!isnull(power_path), "Power path datum [type] is missing power_path")
+ ASSERT(!isnull(archetype_type), "Power path datum [type] is missing archetype_type")
+ ASSERT(!isnull(GLOB.all_power_archetypes[initial(archetype_type.archetype_id)]), "Power path datum [type] references unregistered archetype [archetype_type]")
+ ASSERT(!isnull(display_name), "Power path datum [type] is missing display_name")
+
+/datum/power_path/proc/get_archetype()
+ return GLOB.all_power_archetypes[initial(archetype_type.archetype_id)]
+
+/datum/power_path/proc/to_ui_data()
+ var/datum/power_archetype/archetype_data = get_archetype()
+ return list(
+ "displayName" = display_name,
+ "archetypeId" = archetype_data.archetype_id,
+ "iconAssetName" = icon_asset_name,
+ "isAvailable" = is_available,
+ "mechanicsText" = mechanics_text,
+ "overviewText" = overview_text,
+ "pathLimitExempt" = path_limit_exempt,
+ "themeColor" = theme_color,
+ )
+
+/// Metadata for archetypes, in which the paths are contained.
+/datum/power_archetype
+ abstract_type = /datum/power_archetype
+
+ /// Stable UI key used for landing page grouping.
+ var/archetype_id
+ /// Display title for the archetype group.
+ var/title
+ /// Ordering on the landing page.
+ var/sort_order = 0
+
+/datum/power_archetype/New()
+ . = ..()
+
+ ASSERT(abstract_type != type, "Attempted to instantiate abstract power archetype datum [type]")
+ ASSERT(!isnull(archetype_id), "Power archetype datum [type] is missing archetype_id")
+ ASSERT(!isnull(title), "Power archetype datum [type] is missing title")
+
+/datum/power_archetype/sorcerous
+ archetype_id = "sorcerous"
+ title = "Sorcerous"
+ sort_order = POWER_ARCHETYPE_SORT_SORCEROUS
+
+/datum/power_archetype/resonant
+ archetype_id = "resonant"
+ title = "Resonant"
+ sort_order = POWER_ARCHETYPE_SORT_RESONANT
+
+/datum/power_archetype/mortal
+ archetype_id = "mortal"
+ title = "Mortal"
+ sort_order = POWER_ARCHETYPE_SORT_MORTAL
+
+/// Constructs the global archetype registry by instantiating every concrete /datum/power_archetype subtype.
+/proc/generate_power_archetypes()
+ RETURN_TYPE(/list/datum/power_archetype)
+
+ var/list/datum/power_archetype/all_power_archetypes = list()
+ var/list/archetype_types = sort_list(subtypesof(/datum/power_archetype), GLOBAL_PROC_REF(cmp_power_archetypes_asc))
+
+ for(var/datum/power_archetype/archetype_path as anything in archetype_types)
+ if(initial(archetype_path.abstract_type) == archetype_path)
+ continue
+
+ var/datum/power_archetype/archetype_data = new archetype_path
+ if(!isnull(all_power_archetypes[archetype_data.archetype_id]))
+ stack_trace("Duplicate power archetype id [archetype_data.archetype_id] detected while registering [archetype_path]. Existing archetype datum: [all_power_archetypes[archetype_data.archetype_id]]")
+ qdel(archetype_data)
+ continue
+
+ all_power_archetypes[archetype_data.archetype_id] = archetype_data
+
+ return all_power_archetypes
+
+/// Constructs the global power path registry by instantiating every concrete /datum/power_path subtype.
+/proc/generate_power_paths()
+ RETURN_TYPE(/list/datum/power_path)
+
+ var/list/datum/power_path/all_power_paths = list()
+ var/list/path_types = sort_list(subtypesof(/datum/power_path), GLOBAL_PROC_REF(cmp_power_paths_asc))
+
+ for(var/datum/power_path/path_type as anything in path_types)
+ if(initial(path_type.abstract_type) == path_type)
+ continue
+
+ var/datum/power_path/path_data = new path_type
+ if(!isnull(all_power_paths[path_data.path_key]))
+ stack_trace("Duplicate power path key [path_data.path_key] detected while registering [path_type]. Existing path datum: [all_power_paths[path_data.path_key]]")
+ qdel(path_data)
+ continue
+
+ all_power_paths[path_data.path_key] = path_data
+
+ return all_power_paths
+
+/// Constructs a lookup keyed by the POWER_PATH_* define value.
+/proc/generate_power_paths_by_define()
+ RETURN_TYPE(/list/datum/power_path)
+
+ var/list/datum/power_path/power_paths_by_define = list()
+
+ for(var/path_key in GLOB.all_power_paths)
+ var/datum/power_path/path_data = GLOB.all_power_paths[path_key]
+ if(!isnull(power_paths_by_define[path_data.power_path]))
+ stack_trace("Duplicate power path define [path_data.power_path] detected while indexing [path_data.type]. Existing path datum: [power_paths_by_define[path_data.power_path]]")
+ continue
+ power_paths_by_define[path_data.power_path] = path_data
+
+ return power_paths_by_define
+
+/// Builds the server-owned TGUI config map for every registered power path.
+/proc/generate_power_path_ui_data()
+ RETURN_TYPE(/list)
+
+ var/list/power_path_ui_data = list()
+
+ for(var/path_key in GLOB.all_power_paths)
+ var/datum/power_path/path_data = GLOB.all_power_paths[path_key]
+ power_path_ui_data[path_key] = path_data.to_ui_data()
+
+ return power_path_ui_data
+
+/// Builds the ordered archetype list TGUI uses on the landing page.
+/proc/generate_power_path_archetypes()
+ RETURN_TYPE(/list)
+
+ var/list/power_path_archetypes = list()
+ var/list/archetype_entries_by_id = list()
+
+ for(var/archetype_id in GLOB.all_power_archetypes)
+ var/datum/power_archetype/archetype_data = GLOB.all_power_archetypes[archetype_id]
+ var/list/archetype_entry = list(
+ "id" = archetype_data.archetype_id,
+ "title" = archetype_data.title,
+ "pathIds" = list(),
+ )
+ archetype_entries_by_id[archetype_data.archetype_id] = archetype_entry
+ power_path_archetypes += list(archetype_entry)
+
+ for(var/path_key in GLOB.all_power_paths)
+ var/datum/power_path/path_data = GLOB.all_power_paths[path_key]
+ var/datum/power_archetype/archetype_data = path_data.get_archetype()
+ var/list/archetype_entry = archetype_entries_by_id[archetype_data.archetype_id]
+ var/list/path_ids = archetype_entry["pathIds"]
+ path_ids += path_data.path_key
+
+ return power_path_archetypes
+
+/// Returns the first registered power path key for UI fallback selection.
+/proc/generate_fallback_power_path_id()
+ for(var/list/archetype_entry as anything in GLOB.power_path_archetypes)
+ var/list/path_ids = archetype_entry["pathIds"]
+ if(length(path_ids))
+ return path_ids[1]
+ return null
+
+GLOBAL_LIST_INIT_TYPED(all_power_archetypes, /datum/power_archetype, generate_power_archetypes())
+GLOBAL_LIST_INIT_TYPED(all_power_paths, /datum/power_path, generate_power_paths())
+GLOBAL_LIST_INIT_TYPED(power_paths_by_define, /datum/power_path, generate_power_paths_by_define())
+GLOBAL_LIST_INIT(power_path_ui_data, generate_power_path_ui_data())
+GLOBAL_LIST_INIT(power_path_archetypes, generate_power_path_archetypes())
+GLOBAL_VAR_INIT(fallback_power_path_id, generate_fallback_power_path_id())
+
+/// Sorts power archetypes by archetype order.
+/proc/cmp_power_archetypes_asc(datum/power_archetype/first_archetype, datum/power_archetype/second_archetype)
+ var/first_sort_order = initial(first_archetype.sort_order)
+ var/second_sort_order = initial(second_archetype.sort_order)
+
+ if(first_sort_order != second_sort_order)
+ return first_sort_order - second_sort_order
+
+ return sorttext(initial(second_archetype.archetype_id), initial(first_archetype.archetype_id))
+
+/// Sorts power paths by archetype and then path order.
+/proc/cmp_power_paths_asc(datum/power_path/first_path, datum/power_path/second_path)
+ var/datum/power_archetype/first_archetype = GLOB.all_power_archetypes[initial(first_path.archetype_type.archetype_id)]
+ var/datum/power_archetype/second_archetype = GLOB.all_power_archetypes[initial(second_path.archetype_type.archetype_id)]
+
+ if(first_archetype.sort_order != second_archetype.sort_order)
+ return first_archetype.sort_order - second_archetype.sort_order
+
+ var/first_path_order = initial(first_path.path_sort_order)
+ var/second_path_order = initial(second_path.path_sort_order)
+
+ if(first_path_order != second_path_order)
+ return first_path_order - second_path_order
+
+ return sorttext(initial(second_path.path_key), initial(first_path.path_key))
diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_datum.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_datum.dm
new file mode 100644
index 00000000000000..c7360aba78f1ae
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_datum.dm
@@ -0,0 +1,10 @@
+/datum/power_path/augmented
+ path_key = "augmented"
+ power_path = POWER_PATH_AUGMENTED
+ archetype_type = /datum/power_archetype/mortal
+ path_sort_order = POWER_PATH_SORT_TERTIARY
+ display_name = "Augmented"
+ icon_asset_name = "augmentedicon.png"
+ mechanics_text = "Augmented 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!"
+ overview_text = "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."
+ theme_color = POWER_COLOR_AUGMENTED
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
index c78b6f12914fad..3c31d71d3e8583 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_action.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_action.dm
@@ -5,8 +5,10 @@
/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"
+ background_icon_state = "bg_augmented"
+ overlay_icon_state = "bg_augmented_border"
+ background_icon = 'modular_doppler/modular_powers/icons/powers/backgrounds.dmi'
+ overlay_icon = 'modular_doppler/modular_powers/icons/powers/backgrounds.dmi'
/// The border overlay. This is declared seperately so active_overlay can swap with it.
var/base_overlay_icon_state
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
index 5d490bb27fbcff..207d768c19cb2e 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm
@@ -73,7 +73,7 @@
if(!(casted_magic_flags & MAGIC_RESISTANCE_MIND))
return NONE
antimagic_sources += src
- var/adjusted_cost = process_quality_cost(max(1, charge_cost))
+ var/adjusted_cost = process_quality_cost(charge_cost)
premium_component.adjust_quality(-adjusted_cost)
return COMPONENT_MAGIC_BLOCKED
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
index b2fe36bea8430e..682cb6b5fcb762 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_action.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_action.dm
@@ -1,3 +1,4 @@
/datum/action/cooldown/power/expert
name = "abstract expert power action - ahelp this"
- resonant = FALSE
+ background_icon_state = "bg_expert"
+ overlay_icon_state = "bg_expert_border"
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_datum.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_datum.dm
new file mode 100644
index 00000000000000..bcbc4f2317fe67
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_datum.dm
@@ -0,0 +1,10 @@
+/datum/power_path/expert
+ path_key = "expert"
+ power_path = POWER_PATH_EXPERT
+ archetype_type = /datum/power_archetype/mortal
+ path_sort_order = POWER_PATH_SORT_SECONDARY
+ display_name = "Expert"
+ icon_asset_name = "experticon.png"
+ mechanics_text = "There are no broader mechanics in Expert. Most 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."
+ overview_text = "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"
+ theme_color = POWER_COLOR_EXPERT
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
index 8628f4db7a368c..9c662ca895f0b7 100644
--- 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
@@ -8,3 +8,6 @@
security_record_text = "Subject has a keen eye for spotting substances inside food, drinks and chemicals."
mob_trait = TRAIT_REAGENT_SCANNER
value = 3
+
+ menu_icon = 'icons/obj/medical/chemical.dmi'
+ menu_icon_state = "beakergold"
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
index 546a4a89b6a705..37eadb3b15ec06 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/expert/filthy_rich.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/filthy_rich.dm
@@ -10,6 +10,9 @@
value = 8
required_powers = list(/datum/power/expert/rich)
+ menu_icon = 'icons/obj/economy.dmi'
+ menu_icon_state = "spacecash1000_4"
+
// we just make it the same as rich but reduced because we are lazy.
var/riches = 7500
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
index d6e378865d3332..4d6f6e73a708bd 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/expert/heavy_lifter.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/heavy_lifter.dm
@@ -8,6 +8,10 @@
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
+
+ menu_icon = 'icons/obj/storage/crates.dmi'
+ menu_icon_state = "crate" //32x48 sprite may get scrungled
+
/// how much xp we start with on average.
var/starting_xp_base = SKILL_EXP_JOURNEYMAN
/// tracks how much was given for removal later.
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
index 5ce01d10b9e216..19060619dc798e 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/expert/master_surgeon.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/master_surgeon.dm
@@ -8,6 +8,10 @@
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 = 3
+
+ menu_icon = 'icons/obj/medical/surgery_tools.dmi'
+ menu_icon_state = "scalpel"
+
/// 1.5x faster => multiply time by 1/1.5
var/surgery_speed_mult = 1 / 1.5
/// Flat reduction to failure chance (percentage points)
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm
index e1bcd55321d410..1e2b2135e7df2d 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm
@@ -3,6 +3,10 @@
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
+
+ menu_icon = 'icons/obj/service/library.dmi'
+ menu_icon_state = "book1"
+
/// Saved list of languages that were given by this power to remove when the power is removed.
var/list/given_languages_list = list()
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm
index f2ee5a55ee3160..7d4041c3757b45 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm
@@ -7,6 +7,10 @@
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."
+
+ menu_icon = 'icons/obj/economy.dmi'
+ menu_icon_state = "spacecash50_4"
+
// how rich are we?
var/riches = 2500
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm
index 03a948a5ab9d18..91c4925145e09b 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm
@@ -10,6 +10,9 @@
value = 6
required_powers = list(/datum/power/expert/heavy_lifter)
+ menu_icon = 'icons/obj/clothing/suits/armor.dmi'
+ menu_icon_state = "riot"
+
/// 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
diff --git a/modular_doppler/modular_powers/code/powers/mortal/irregular/_irregular_action.dm b/modular_doppler/modular_powers/code/powers/mortal/irregular/_irregular_action.dm
new file mode 100644
index 00000000000000..e14ba0352ffad1
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/irregular/_irregular_action.dm
@@ -0,0 +1,4 @@
+/datum/action/cooldown/power/irregular
+ name = "abstract irregular power action - ahelp this"
+ background_icon_state = "bg_irregular"
+ overlay_icon_state = "bg_irregular_border"
diff --git a/modular_doppler/modular_powers/code/powers/mortal/irregular/_irregular_datum.dm b/modular_doppler/modular_powers/code/powers/mortal/irregular/_irregular_datum.dm
new file mode 100644
index 00000000000000..1270b06b3183e3
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/irregular/_irregular_datum.dm
@@ -0,0 +1,11 @@
+/datum/power_path/irregular
+ path_key = "irregular"
+ power_path = POWER_PATH_IRREGULAR
+ archetype_type = /datum/power_archetype/mortal
+ path_sort_order = POWER_PATH_SORT_QUATERNARY
+ display_name = "Irregular"
+ icon_asset_name = "irregularicon.png"
+ mechanics_text = "Irregular does not count against the 2-paths only limit. In turn, all powers in Irregular provide niche benefits only, and often only at payoffs (for example, records-manipulating powers could be seen as fraud). There are no other overarching mechanics."
+ overview_text = "A catch-all for niche qualities that make you stand out, but are not great feats worth boasting about."
+ theme_color = POWER_COLOR_IRREGULAR
+ path_limit_exempt = TRUE
diff --git a/modular_doppler/modular_powers/code/powers/mortal/irregular/_irregular_power.dm b/modular_doppler/modular_powers/code/powers/mortal/irregular/_irregular_power.dm
new file mode 100644
index 00000000000000..a3e1c598d1ccc8
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/irregular/_irregular_power.dm
@@ -0,0 +1,8 @@
+/datum/power/irregular
+ name = "Irregular Power"
+ desc = "Living up to the name, you have done something irregular: found an abstract type!"
+
+ archetype = POWER_ARCHETYPE_MORTAL
+ path = POWER_PATH_IRREGULAR
+ priority = POWER_PRIORITY_BASIC
+ abstract_parent_type = /datum/power/irregular
diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/false_power.dm b/modular_doppler/modular_powers/code/powers/mortal/irregular/false_power.dm
similarity index 87%
rename from modular_doppler/modular_powers/code/powers/mortal/expert/false_power.dm
rename to modular_doppler/modular_powers/code/powers/mortal/irregular/false_power.dm
index 95365a4c6921f1..8ed9cf6a9063db 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/expert/false_power.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/irregular/false_power.dm
@@ -2,19 +2,22 @@
Fill the sec records with a fake power. Or really anything else you want to write down.
*/
-/datum/power/expert/false_power
+/datum/power/irregular/false_power
name = "False Power"
desc = "A bit of misinformation about your capabilities and it's 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)
+ menu_icon = 'icons/obj/service/bureaucracy.dmi'
+ menu_icon_state = "docs_red"
+
+/datum/power/irregular/false_power/add(client/client_source)
apply_false_power_prefs(client_source)
-/datum/power/expert/false_power/post_add()
+/datum/power/irregular/false_power/post_add()
apply_false_power_prefs(power_holder?.client)
. = ..()
-/datum/power/expert/false_power/get_security_record_text()
+/datum/power/irregular/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]
@@ -30,7 +33,7 @@
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)
+/datum/power/irregular/false_power/proc/apply_false_power_prefs(client/client_source)
if(!client_source)
security_threat = POWER_THREAT_MINOR
return
@@ -93,7 +96,7 @@
return
/datum/power_constant_data/false_power
- associated_typepath = /datum/power/expert/false_power
+ associated_typepath = /datum/power/irregular/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/hidden_powers.dm b/modular_doppler/modular_powers/code/powers/mortal/irregular/hidden_powers.dm
similarity index 73%
rename from modular_doppler/modular_powers/code/powers/mortal/expert/hidden_powers.dm
rename to modular_doppler/modular_powers/code/powers/mortal/irregular/hidden_powers.dm
index 2734b77124f900..3d0ceae9c00d7f 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/expert/hidden_powers.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/irregular/hidden_powers.dm
@@ -1,19 +1,23 @@
-/datum/power/expert/hidden_powers
+/datum/power/irregular/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
+
+ menu_icon = 'icons/obj/service/bureaucracy.dmi'
+ menu_icon_state = "folder_sred"
+
/// 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)
+/datum/power/irregular/hidden_powers/add(client/client_source)
apply_hidden_visibility()
-/datum/power/expert/hidden_powers/remove()
+/datum/power/irregular/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()
+/datum/power/irregular/hidden_powers/proc/apply_hidden_visibility()
if(!power_holder)
return
@@ -21,7 +25,7 @@
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
+ if(istype(power_instance, /datum/power/irregular/false_power)) // false power explicitly stays visible
power_instance.include_in_security_records = TRUE
else
power_instance.include_in_security_records = FALSE
@@ -29,7 +33,7 @@
power_holder.refresh_security_power_records()
/// Undoes the visibility changes from hidden powers
-/datum/power/expert/hidden_powers/proc/restore_hidden_visibility()
+/datum/power/irregular/hidden_powers/proc/restore_hidden_visibility()
if(!power_holder)
original_visibility.Cut()
return
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
index c90015bdda64b4..fd42d45b254b4d 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_action.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_action.dm
@@ -1,4 +1,4 @@
/datum/action/cooldown/power/warfighter
name = "abstract expert warfighter action - ahelp this"
- resonant = FALSE
- background_icon_state = "bg_cult"
+ background_icon_state = "bg_warfighter"
+ overlay_icon_state = "bg_warfighter_border"
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_datum.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_datum.dm
new file mode 100644
index 00000000000000..6b42fedccadae7
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_datum.dm
@@ -0,0 +1,10 @@
+/datum/power_path/warfighter
+ path_key = "warfighter"
+ power_path = POWER_PATH_WARFIGHTER
+ archetype_type = /datum/power_archetype/mortal
+ path_sort_order = POWER_PATH_SORT_PRIMARY
+ display_name = "Warfighter"
+ icon_asset_name = "warfightericon.png"
+ mechanics_text = "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."
+ overview_text = "Practical combat skills, tried, tested and trained for milennia before magic ever reared its head. They may throw fireballs: but what good are their feats of magic if they're already tackled and restrained to the ground?"
+ theme_color = POWER_COLOR_WARFIGHTER
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
index a30b47071389ed..24bd51e2265dca 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/explosives_specialist.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/explosives_specialist.dm
@@ -6,5 +6,7 @@
value = 4
required_powers = list(/datum/power/warfighter/quick_draw)
mob_trait = TRAIT_POWER_EXPLOSIVES_SPECIALIST
+ menu_icon = 'icons/obj/weapons/grenade.dmi'
+ menu_icon_state = "pipebomb-timer"
// 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/greater_tackler.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/greater_tackler.dm
index 666d641f973793..a069aa3ed8ee65 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/greater_tackler.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/greater_tackler.dm
@@ -7,9 +7,11 @@
security_record_text = "Subject is exceedingly good at landing tackles."
security_threat = POWER_THREAT_MAJOR
value = 5
-
required_powers = list(/datum/power/warfighter/tackler)
+ menu_icon = 'icons/obj/clothing/gloves.dmi'
+ menu_icon_state = "gorilla"
+
/// bonuses to success chance
var/skill_mod_bonus = 3
/// bonuses to range
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
index 5c66ea5c034fca..43b966ab93c1c6 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/martial_artist.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/martial_artist.dm
@@ -3,10 +3,12 @@
*/
/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."
+ desc = "Trained in specialized combat maneuvers, you know where to best strike your opponents. Your punches deal 5 extra damage on hit!"
security_record_text = "Subject is trained in hand-to-hand combat and throws stronger punches."
security_threat = POWER_THREAT_MAJOR
value = 2
+ menu_icon = 'icons/obj/fluff/gym_equipment.dmi'
+ menu_icon_state = "punchingbag"
power_flags = POWER_HUMAN_ONLY
/// how much EEEEXTRA DEEEAAMEEEEG we do with our punches.
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm
index 153b71df0e74d5..a6274ea010124e 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm
@@ -8,9 +8,11 @@
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)
+ menu_icon = 'icons/obj/clothing/gloves.dmi'
+ menu_icon_state = "black"
+
/// the datum that the tackle system is in
var/datum/component/tackler
diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/tchotchke_style.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tchotchke_style.dm
index 93a57cc64ce7d2..2b26e7f6f35ff3 100644
--- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/tchotchke_style.dm
+++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tchotchke_style.dm
@@ -14,6 +14,10 @@
security_threat = POWER_THREAT_MAJOR
value = 8
required_powers = list(/datum/power/warfighter/martial_artist)
+
+ menu_icon = 'icons/obj/clothing/gloves.dmi'
+ menu_icon_state = "wizard"
+
/// 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/martial_art_component
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
index 96374511dc4767..eef815e13f0d7a 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_action.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_action.dm
@@ -1,4 +1,49 @@
/datum/action/cooldown/power/aberrant
name = "abstract aberrant power action - ahelp this"
- background_icon_state = "bg_alien"
- overlay_icon_state = "bg_alien_border"
+ background_icon_state = "bg_aberrant"
+ overlay_icon_state = "bg_aberrant_border"
+
+ /// How much hunger cost the ability use has
+ var/cost
+
+// Block use while starving.
+/datum/action/cooldown/power/aberrant/can_use(mob/living/user, atom/target)
+ . = ..()
+ if(!.)
+ return FALSE
+ if(!can_pay_hunger_cost(user, cost))
+ owner.balloon_alert(user, "too hungry!")
+ return FALSE
+ return TRUE
+
+// Action cost
+/datum/action/cooldown/power/aberrant/on_action_success(mob/living/user, atom/target)
+ . = ..()
+ modify_hunger(cost, user)
+
+/// Quick handler that modifies the mob's hunger
+/datum/action/cooldown/power/aberrant/proc/modify_hunger(amount, mob/living/user = owner)
+ if(amount <= 0 || !iscarbon(user))
+ return
+
+ var/mob/living/carbon/carbon_user = user
+ var/obj/item/organ/stomach/stomach = carbon_user.get_organ_slot(ORGAN_SLOT_STOMACH)
+ if(stomach)
+ stomach.adjust_satiety(amount)
+ return
+
+ carbon_user.adjust_nutrition(-amount)
+
+/// Returns TRUE when the user can afford the cost with their current type of stomach
+/datum/action/cooldown/power/aberrant/proc/can_pay_hunger_cost(mob/living/user, amount)
+ if(amount <= 0)
+ return TRUE
+ if(!iscarbon(user))
+ return FALSE
+
+ var/mob/living/carbon/carbon_user = user
+ var/obj/item/organ/stomach/stomach = carbon_user.get_organ_slot(ORGAN_SLOT_STOMACH)
+ if(stomach)
+ return stomach.can_afford_satiety_cost(amount)
+
+ return carbon_user.nutrition > (NUTRITION_LEVEL_STARVING + amount)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_datum.dm
new file mode 100644
index 00000000000000..54ef6ac36d2746
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_datum.dm
@@ -0,0 +1,10 @@
+/datum/power_path/aberrant
+ path_key = "aberrant"
+ power_path = POWER_PATH_ABERRANT
+ archetype_type = /datum/power_archetype/resonant
+ path_sort_order = POWER_PATH_SORT_TERTIARY
+ display_name = "Aberrant"
+ icon_asset_name = "aberranticon.png"
+ mechanics_text = "Aberrant specializes in adjusting their physical qualities to mimmick various other creatures, granting them unique physical advantages, both big and small.\n\nNearly all your abilities impact your hunger, increasing it with their use. If you have an alternative stomach, such as by being an Ethereal, it will deduct that stomach's satiety equivelant instead. Make sure to match your strength with a voracious apetite, as nearly all your powers cannot be activated while starving (and passive powers will stop functioning then). Your powers are sometimes non-magical.\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. Most powers belonging to Beastial are oriented towards utility, rather than raw offensive force.\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. Aberrant focuses on strong combat prowress, both in high defense and offense."
+ overview_text = "The body altered and twisted. Exposure to resonance has exposed them to atavism, morphing their bodies to take on beastial traits, or sometimes even monstrous ones."
+ theme_color = POWER_COLOR_ABERRANT
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
index af7a0f008faa14..567bd1f4906c68 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_power.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_power.dm
@@ -6,3 +6,4 @@
path = POWER_PATH_ABERRANT
priority = POWER_PRIORITY_BASIC
abstract_parent_type = /datum/power/aberrant
+ magic_flags = POWER_MAGIC_STANDARD
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
index a015cca0992011..8d1b008d73c177 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root.dm
@@ -6,3 +6,4 @@
archetype = POWER_ARCHETYPE_RESONANT
path = POWER_PATH_ABERRANT
priority = POWER_PRIORITY_BASIC // removing roots after the fact
+ magic_flags = NONE // the roots aren't magical
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
index f719d3284c38d7..9ed5d36f461346 100644
--- 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
@@ -6,6 +6,8 @@
value = 2
/// Saved preference value used for security records snapshotting.
var/chosen_diet = "None"
+ menu_icon = 'icons/obj/food/meat.dmi'
+ menu_icon_state = "meat"
/datum/power/aberrant_root/beastial/get_security_record_text()
switch(chosen_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
index 31aa7ca7d9d481..1e48dc8fbde99b 100644
--- 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
@@ -2,18 +2,25 @@
/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 resistant to bloodloss."
+ \nThe thresholds for being low on blood are unchanged, meaning you are extra resistant to bloodloss.\
+ \nIf your species lacks natural blood regen (e.g Hemophages), you also bleed out 35% slower from wounds."
security_record_text = "Subject's body contains and regenerates more blood."
value = 3
+ menu_icon = 'icons/obj/medical/bloodpack.dmi'
+ menu_icon_state = "generic_bloodpack"
/// 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
+ /// Tracks if we applied our bleed multiplier so we can undo safely.
+ var/bleed_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
+ /// How much we multiply the bleed rate with for mobs with TRAIT_NOHUNGER
+ var/nohunger_bleed_mult = 0.65
/datum/power/aberrant_root/monstrous/add()
var/mob/living/carbon/human/human_holder = power_holder
@@ -21,13 +28,24 @@
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
+ if(HAS_TRAIT(human_holder, TRAIT_NOHUNGER)) // yes, blood regen is arbitrated by this trait.
+ human_holder.physiology.bleed_mod *= nohunger_bleed_mult
+ bleed_multiplier_applied = TRUE
+
RegisterSignal(human_holder, COMSIG_HUMAN_ON_HANDLE_BLOOD, PROC_REF(handle_extra_blood_regen))
+/// Adds your blood. In add_unique so it doesn't reset your blood if you acquire this power through power transfers.
+/datum/power/aberrant_root/monstrous/add_unique(client/client_source)
+ var/mob/living/carbon/human/human_holder = power_holder
+ if(!istype(human_holder) || HAS_TRAIT(human_holder, TRAIT_NOBLOOD))
+ return
+
+ human_holder.blood_volume = min(target_blood_volume, BLOOD_VOLUME_MAXIMUM)
+
/datum/power/aberrant_root/monstrous/remove()
var/mob/living/carbon/human/human_holder = power_holder
@@ -40,8 +58,10 @@
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
+ if(bleed_multiplier_applied)
+ human_holder.physiology.bleed_mod /= nohunger_bleed_mult
+ bleed_multiplier_applied = FALSE
+
target_blood_volume = 0
@@ -59,5 +79,13 @@
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
+ // Blood regen scales off of nutrtion and drains nutrition which we need to emulate that.
+ if(HAS_TRAIT(human_holder, TRAIT_NOHUNGER))
+ return
+ var/nutrition_ratio = round(human_holder.nutrition / NUTRITION_LEVEL_WELL_FED, 0.2)
+ if(human_holder.satiety > 80) // Base blood regen has this bonus too
+ nutrition_ratio *= 1.25
+ human_holder.adjust_nutrition(-nutrition_ratio * HUNGER_FACTOR * seconds_per_tick)
+
+ var/blood_regen_amount = BLOOD_REGEN_FACTOR * human_holder.physiology.blood_regen_mod * nutrition_ratio * 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
index 2a6aa0aa8dc5a1..36f8b19b86806d 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm
@@ -6,6 +6,7 @@
security_record_text = "Subject can manifest a sharp-edged blade from their arm."
security_threat = POWER_THREAT_MAJOR
value = 4
+ magic_flags = POWER_MAGIC_STANDARD
required_powers = list(/datum/power/aberrant_root/monstrous)
action_path = /datum/action/cooldown/power/aberrant/armblade
@@ -17,6 +18,9 @@
active = FALSE
cooldown_time = 50
+ cost = ABERRANT_HUNGER_TRIVIAL * 5
+ /// Whether the current activation extended the blade and should spend hunger.
+ var/extended_blade = FALSE
/datum/action/cooldown/power/aberrant/armblade/Grant(mob/granted_to)
. = ..()
@@ -27,6 +31,7 @@
UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
/datum/action/cooldown/power/aberrant/armblade/use_action(mob/living/user, atom/target)
+ extended_blade = FALSE
if(active)
for(var/obj/item/melee/arm_blade/aberrant/blade in user.held_items)
user.temporarilyRemoveItemFromInventory(blade, TRUE)
@@ -49,8 +54,14 @@
playsound(user, 'sound/effects/blob/blobattack.ogg', 30, TRUE)
active = TRUE
+ extended_blade = TRUE
return TRUE
+/datum/action/cooldown/power/aberrant/armblade/on_action_success(mob/living/user, atom/target)
+ cost = extended_blade ? ABERRANT_HUNGER_MINOR : 0
+ . = ..()
+ extended_blade = FALSE
+
/// When dispelled, arm pops back in.
/datum/action/cooldown/power/aberrant/armblade/proc/on_dispel(mob/owner, atom/dispeller)
SIGNAL_HANDLER
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm
index ade3ce9f683afd..f41299dbe0facb 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm
@@ -1,12 +1,15 @@
// 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."
+ desc = "You can glow! You passively emit the chosen light color; which can be toggled on or off at will.\
+ \nToggling the light causes a trivial amount of hunger."
value = 1
+ magic_flags = POWER_MAGIC_STANDARD
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
+ required_powers = list(/datum/power/aberrant_root)
+ required_allow_subtypes = TRUE
+
action_path = /datum/action/cooldown/power/aberrant/bioluminescence
/datum/action/cooldown/power/aberrant/bioluminescence
@@ -16,6 +19,7 @@
button_icon_state = "lantern-blue-on"
cooldown_time = 5
+ cost = ABERRANT_HUNGER_TRIVIAL
// start with da pretty lights on
active = TRUE
@@ -50,7 +54,6 @@
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
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm
index 7876cbba008d84..73055456ca3479 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm
@@ -2,9 +2,10 @@
/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."
+ \n Whilst you have someone's blood, you have an indicator of your quarry's direction. Does not work on scrying and magic immune creatures. Causes a minor amount of hunger on use."
security_record_text = "Subject can track down a creature's direction using blood samples."
value = 10
+ magic_flags = POWER_MAGIC_STANDARD | POWER_MAGIC_SCRYING
required_powers = list(/datum/power/aberrant_root/beastial)
action_path = /datum/action/cooldown/power/aberrant/bloodhound
@@ -17,8 +18,7 @@
click_to_activate = TRUE
target_range = 1
- /// How much hunger does tracking someone take?
- var/hunger_cost = 20
+ cost = ABERRANT_HUNGER_TRIVIAL * 5
/// How long you can keep a mob's scent.
var/scent_duration = 2 MINUTES
@@ -45,7 +45,6 @@
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.
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm
index 46f809f66ba294..e696913e3cbd51 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm
@@ -5,10 +5,11 @@
\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 compared to normal.\
- \n Costs hunger to use, and cannot be used while starving."
+ \n Gain a moderate amount of hunger on 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
+ magic_flags = NONE // non-magical
required_powers = list(/datum/power/aberrant/web_crafter)
action_path = /datum/action/cooldown/power/aberrant/cocoon
@@ -24,6 +25,7 @@
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
@@ -32,16 +34,6 @@
// 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))
@@ -59,10 +51,8 @@
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
+ cost = last_cocoon_was_mob ? (ABERRANT_HUNGER_MINOR) : (ABERRANT_HUNGER_TRIVIAL * 5)
+ 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)
@@ -149,8 +139,6 @@
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
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm
index cbbd5489629eff..0804a8a9b60a2e 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm
@@ -4,10 +4,12 @@
desc = "Your eyes see perfectly in the dark; but your vision gains a colored tint. This color is customizable."
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
+ required_powers = list(/datum/power/aberrant_root)
+ required_allow_subtypes = TRUE
+
+ menu_icon = 'icons/mob/actions/actions_changeling.dmi'
+ menu_icon_state = "darkness_adaptation"
/// Saves if we apply the cutoffs for darkvision.
var/eye_color_cutoffs_applied = FALSE
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
index 704885372cde34..020557e4ab7f47 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/healing_factor.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/healing_factor.dm
@@ -4,17 +4,20 @@
/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.\
- \nThe more this power heals, the hungrier you become."
+ \nThe more this power heals, the hungrier you become: every 4 health healed amounts to a trivial amount of hunger."
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)
+ magic_flags = NONE // non-magical
+
+ menu_icon = 'icons/mob/actions/actions_changeling.dmi'
+ menu_icon_state = "fleshmend"
/// how much we heal per second
var/healing = 0.2
- /// How much healing is required to consume 1 satiety.
- var/hunger_per_healing = 2
+ /// How much hunger we generate for every 1 point of healing.
+ var/hunger_per_healing = ABERRANT_HUNGER_TRIVIAL * 0.5
/datum/power/aberrant/healing_factor/process(seconds_per_tick)
// Does not work if you're in crit
@@ -29,9 +32,10 @@
var/mob/living/carbon/mob = power_holder
for(var/obj/item/bodypart/bodypart in mob.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC))
var/damage_before = bodypart.get_damage()
- if(bodypart.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC)) // make people hungry based on how much we have healed
- var/damage_healed = max(0, damage_before - bodypart.get_damage())
- if(damage_healed > 0)
- mob.adjust_nutrition(-(damage_healed / hunger_per_healing))
+ var/updated_bodypart_state = bodypart.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC)
+ var/damage_healed = max(0, damage_before - bodypart.get_damage())
+ if(damage_healed > 0)
+ mob.adjust_nutrition(-(damage_healed * hunger_per_healing))
+ if(updated_bodypart_state)
mob.update_damage_overlays()
break
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/inexorable.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/inexorable.dm
new file mode 100644
index 00000000000000..ee8b7bc3561e5d
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/inexorable.dm
@@ -0,0 +1,29 @@
+/*
+ Grants /datum/mutation/inexorable. I don't personally enjoy handing out mutations as a power (it feels like it infringes on geneticist) but since its relatively popular as a quirk option from genemodded I figured giving it a home is still nice.
+*/
+/datum/power/aberrant/inexorable
+ name = "Inexorable"
+ desc = "You can keep fighting for several moments in critical condition: but your body will start rapidly damaging itself as vital parts of it begin failing. Grants the Inexorable mutation."
+ security_record_text = "Subject can keep fighting even as they enter critical condition."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+ power_flags = POWER_HUMAN_ONLY
+ required_powers = list(/datum/power/aberrant_root/monstrous)
+ magic_flags = NONE // non-magical
+
+ menu_icon = 'icons/mob/actions/actions_changeling.dmi'
+ menu_icon_state = "fake_death"
+
+/datum/power/aberrant/inexorable/add(client/client_source)
+ . = ..()
+ if(!ishuman(power_holder))
+ return
+ var/mob/living/carbon/human/human_holder = power_holder
+ human_holder.dna.add_mutation(/datum/mutation/inexorable, src)
+
+/datum/power/aberrant/inexorable/remove()
+ . = ..()
+ if(!ishuman(power_holder))
+ return
+ var/mob/living/carbon/human_holder = power_holder
+ human_holder.dna.remove_mutation(/datum/mutation/inexorable, src)
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
index 7362f5fa3239bb..fc65faf827b2d8 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/miasmic_conversion.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/miasmic_conversion.dm
@@ -3,12 +3,16 @@
*/
/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."
+ 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.\
+ \nYou also passively heal 0.05 toxins damage damage per second. This healing causes a trivial amount of hunger every 2 health healed."
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)
+ magic_flags = NONE // non-magical
+
+ menu_icon = 'icons/mob/actions/actions_changeling.dmi'
+ menu_icon_state = "biodegrade"
/// how much we passively heal tox
var/passive_tox_healing = 0.05
@@ -16,6 +20,8 @@
var/healing = 1
/// the ratio at which we convert.
var/conversion_rate = 0.90
+ /// How much hunger we generate for every 1 point of healing.
+ var/hunger_per_healing = ABERRANT_HUNGER_TRIVIAL * 0.5
/datum/power/aberrant/miasmic_conversion/process(seconds_per_tick)
var/heal_amt = healing * seconds_per_tick
@@ -37,10 +43,12 @@
// Applies healing, then reapplies as damage.
var/damage_before = bodypart.get_damage()
- if(bodypart.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC))
+ var/updated_bodypart_state = bodypart.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC)
+ if(updated_bodypart_state)
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)
+ power_holder.adjust_nutrition(-(healed * hunger_per_healing))
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm
index 3d3fe3c755bd41..0d9ef56cad768d 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm
@@ -6,14 +6,15 @@
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 transferred to your original form (halved if reverting back manually)."
+ \n Using the ability causes a moderate amount of hunger, and cannot be used while you're starving.\
+ \n If the creature dies or the effect ends (including by being dispelled), you are reverted to your normal form (prone on the ground), and all damage taken is transferred to your original form (halved if reverting back manually)."
security_threat = POWER_THREAT_MAJOR
value = 5
+ magic_flags = POWER_MAGIC_STANDARD
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
+ required_powers = list(/datum/power/aberrant_root)
+ required_allow_subtypes = TRUE
action_path = /datum/action/cooldown/power/aberrant/shapechange
/datum/power/aberrant/shapechange/get_security_record_text()
@@ -59,8 +60,7 @@
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
+ cost = ABERRANT_HUNGER_MODERATE
/// Tracks if the current activation performed a shift (not a revert).
var/just_shifted = FALSE
/// Persistent identifier used for the shapeshifted form.
@@ -109,10 +109,6 @@
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)
@@ -180,9 +176,8 @@
// Subtract hunger on succesful use
/datum/action/cooldown/power/aberrant/shapechange/on_action_success(mob/living/user, atom/target)
+ cost = just_shifted ? (ABERRANT_HUNGER_MODERATE) : 0
. = ..()
- if(just_shifted)
- user.adjust_nutrition(-hunger_cost)
just_shifted = FALSE
/// Creates the relevant mob for shapeshift.
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
index 330c6494a60811..67a5413a5f4fc7 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_spider.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_spider.dm
@@ -3,6 +3,7 @@
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 invisibility in webs."
value = 3
+ magic_flags = POWER_MAGIC_STANDARD
required_powers = list(/datum/power/aberrant/shapechange)
/// Saved form so we can restore on removal.
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
index d4224d52d83e21..23023672715680 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm
@@ -3,6 +3,7 @@
name = "Shapechange: Wolf"
desc = "Overrides your chosen Shapechange form with a Wolf; a fast creature with a strong bite attack."
value = 2
+ magic_flags = POWER_MAGIC_STANDARD
required_powers = list(/datum/power/aberrant/shapechange)
/// Saved form so we can restore on removal.
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
index 76740bb2bbf89a..8b845d2ce0230c 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/tail_sweep.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/tail_sweep.dm
@@ -8,6 +8,7 @@
security_record_text = "Subject can use their tail to damage and knock back foes in active combat."
security_threat = POWER_THREAT_MAJOR
value = 4
+ magic_flags = NONE // non-magical
required_powers = list(/datum/power/aberrant_root/beastial)
action_path = /datum/action/cooldown/power/aberrant/tailsweep
@@ -23,8 +24,7 @@
var/range = 1
/// Throw distance
var/throw_dist = 2
- /// Hunger cost of the power
- var/hunger_cost = 10
+ cost = ABERRANT_HUNGER_TRIVIAL * 2.5
/// How much brute damage it deals
var/damage = 20
/// How much stam damage it deals
@@ -33,6 +33,9 @@
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(!.)
+ return FALSE
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)
@@ -40,10 +43,7 @@
if(!tail && !taur_body)
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
- . = ..()
+ return TRUE
/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)
@@ -72,7 +72,3 @@
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
index d924f770c39a47..69bcbdf54520a8 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/vent_crawl.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/vent_crawl.dm
@@ -2,13 +2,19 @@
/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."
+ \nIf you are undersized, you can crawl in vents while wearing your normal equipment.\
+ \nYou are vulnerable to anti-magic while vent-crawling and may become stuck if you are silenced during it! You also gain a trivial amount of hunger every second you spend vent crawling; though you can still vent-crawl regardless of hunger level. Neither the anti-magic nor hunger cost apply to undersized mobs.\
+ \nOversized mobs cannot use this ability."
security_record_text = "Subject can crawl through ventilation shafts."
security_threat = POWER_THREAT_MAJOR
value = 5
+ magic_flags = POWER_MAGIC_STANDARD
power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES
- required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous)
- required_allow_any = TRUE
+ required_powers = list(/datum/power/aberrant_root)
+ required_allow_subtypes = TRUE
+
+ menu_icon = 'icons/obj/machines/atmospherics/unary_devices.dmi'
+ menu_icon_state = "vent_out" //sus
/datum/power/aberrant/vent_crawl/add(client/client_source)
. = ..()
@@ -35,15 +41,18 @@
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
+ // Disqualifies for gear check & hunger 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
+ // Disqualifies for gear check & hunger if undersized
if(HAS_TRAIT(power_holder, TRAIT_UNDERSIZED))
REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src)
return
+ // Hunger cost!
+ power_holder.adjust_nutrition(-((ABERRANT_HUNGER_TRIVIAL * 0.5) * seconds_per_tick))
+
// 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)
@@ -70,12 +79,14 @@
/// 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!")
- return FALSE
if(HAS_TRAIT(source, TRAIT_OVERSIZED))
source.balloon_alert(source, "You're too big to fit!")
return FALSE
+ if(HAS_TRAIT(source, TRAIT_UNDERSIZED)) // undersized bypasses silence
+ return TRUE
+ if(HAS_TRAIT(source, TRAIT_RESONANCE_SILENCED))
+ source.balloon_alert(source, "Silenced!")
+ return FALSE
return TRUE
/// Checks for back slot, head slot and suit slot
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
index 5a9551fdd75efa..19f774c62ec255 100644
--- 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
@@ -2,8 +2,8 @@
/datum/web_craft_entry
/// Type spawned by this entry
var/obj/spawn_type
- /// Hunger cost to craft
- var/hunger_cost = 0
+ /// Aberrant hunger cost to craft
+ var/cost = 0
/// Time to craft (do_after). 0 for instant.
var/craft_time = 0
/// Display name for the radial
@@ -40,7 +40,7 @@
var/list/info_bits = list()
if(desc)
info_bits += desc
- info_bits += "Cost: [hunger_cost] hunger"
+ info_bits += "Cost: [cost] hunger"
if(craft_time > 0)
info_bits += "Time: [craft_time/10]s"
choice.info = jointext(info_bits, " ")
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
index d77c7ccab12073..86e634a2a6354b 100644
--- 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
@@ -2,13 +2,13 @@
/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
+ cost = ABERRANT_HUNGER_TRIVIAL * 3.5
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
+ cost = ABERRANT_HUNGER_TRIVIAL * 2.5
craft_time = 1 SECONDS
icon = 'icons/effects/web.dmi'
icon_state = "webpassage"
@@ -17,25 +17,25 @@
/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
+ cost = ABERRANT_HUNGER_MINOR / 2
/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
+ cost = ABERRANT_HUNGER_MINOR / 2
// 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
+ cost = ABERRANT_HUNGER_MINOR / 2
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
+ cost = ABERRANT_HUNGER_TRIVIAL * 2.5
craft_time = 1 SECONDS
/datum/web_craft_entry/tripwire_web/spawn_structure(mob/living/user, turf/target_turf)
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
index d505301a744c10..4932f8b40bd1b6 100644
--- 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
@@ -5,6 +5,7 @@
security_record_text = "Subject can craft bolas and restraints from their spider silk."
security_threat = POWER_THREAT_MAJOR
value = 3
+ magic_flags = NONE // non-magical
required_powers = list(/datum/power/aberrant/web_crafter)
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
index 4598d475e89b2b..a3947df7ea32e2 100644
--- 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
@@ -7,6 +7,7 @@
security_record_text = "Subject can craft leg snaring traps from their spider silk."
security_threat = POWER_THREAT_MAJOR
value = 3
+ magic_flags = NONE // non-magical
required_powers = list(/datum/power/aberrant/web_crafter)
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
index 6ab07e731b602f..a5f21d267f0820 100644
--- 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
@@ -2,9 +2,10 @@
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."
+ \n Creatures immune to magic or 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
+ magic_flags = POWER_MAGIC_STANDARD | POWER_MAGIC_SCRYING
required_powers = list(/datum/power/aberrant/web_crafter)
@@ -48,6 +49,17 @@
if(maker)
maker_ref = WEAKREF(maker)
pick_icon_state()
+ RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
+
+/obj/structure/spider/tripwire_web/Destroy(force)
+ . = ..()
+ UnregisterSignal(src, COMSIG_ATOM_DISPEL)
+
+/// Removes the tripwire web silently. You may never have known it was there~
+/obj/structure/spider/tripwire_web/proc/on_dispel(datum/source, atom/dispeller)
+ SIGNAL_HANDLER
+ qdel(src)
+ return DISPEL_RESULT_DISPELLED
/** 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
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
index 8ee594f384c80b..43f90f527e600b 100644
--- 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
@@ -2,11 +2,12 @@
/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 Creating anything using Web Crafter causes varying amount of hunger, 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
+ magic_flags = NONE // non-magical
required_powers = list(/datum/power/aberrant_root/beastial)
action_path = /datum/action/cooldown/power/aberrant/web_crafter
@@ -49,6 +50,7 @@
// Craft the item.
if(!create_obj(user, last_crafted_entry))
return FALSE
+ cost = last_crafted_entry.cost
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
@@ -89,22 +91,14 @@
if(!create_obj(user, entry))
return FALSE
last_crafted_entry = entry
+ cost = entry.cost
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
+ cost = 0
+ if(last_crafted_entry)
+ cost = last_crafted_entry.cost
+ return ..()
/// Populates the list of web entries
/datum/action/cooldown/power/aberrant/web_crafter/proc/get_web_craft_entries()
@@ -142,10 +136,6 @@
/// 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!")
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
index ab1d5d383c2591..27e431fa204392 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_action.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_action.dm
@@ -1,7 +1,7 @@
/datum/action/cooldown/power/cultivator
name = "abstract cultivator power action - ahelp this"
- background_icon_state = "bg_revenant"
- overlay_icon_state = "bg_spell_border"
+ background_icon_state = "bg_cultivator"
+ overlay_icon_state = "bg_cultivator_border"
button_icon = 'icons/mob/actions/backgrounds.dmi'
/// The component that talks with cultivator energy. Mostly all functions here communicate with this.
diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_datum.dm
new file mode 100644
index 00000000000000..19a9c4a2421894
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_datum.dm
@@ -0,0 +1,10 @@
+/datum/power_path/cultivator
+ path_key = "cultivator"
+ power_path = POWER_PATH_CULTIVATOR
+ archetype_type = /datum/power_archetype/resonant
+ path_sort_order = POWER_PATH_SORT_SECONDARY
+ display_name = "Cultivator"
+ icon_asset_name = "cultivatoricon.png"
+ mechanics_text = "Cultivator revolves around a resource they build up 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."
+ overview_text = "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 like a mountain, and strike with your fists as if it were a blade."
+ theme_color = POWER_COLOR_CULTIVATOR
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
index 46defc78609ecd..45d1a1ffe8a082 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_energy.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_energy.dm
@@ -1,5 +1,5 @@
/// Helper to format the text that gets thrown onto the energy hud element.
-#define FORMAT_ENERGY_TEXT(charges) MAPTEXT("
[floor(charges)]
")
+#define FORMAT_ENERGY_TEXT(charges) MAPTEXT("
[floor(charges)]
")
/datum/component/cultivator_energy
dupe_mode = COMPONENT_DUPE_UNIQUE
@@ -43,6 +43,10 @@
UnregisterFromParent()
STOP_PROCESSING(SSfastprocess, src)
+ // Scoots the theologist UI if it exists
+ var/datum/component/theologist_piety/theologist_piety = attached_mob?.GetComponent(/datum/component/theologist_piety)
+ theologist_piety?.update_screen_loc()
+
if(!attached_mob)
return
@@ -107,6 +111,10 @@
cultivator_ui = new /atom/movable/screen/cultivator_energy(null, hud_used)
hud_used.infodisplay += cultivator_ui
+ // Scoots the theologist UI if it exists
+ var/datum/component/theologist_piety/theologist_piety = living_holder.GetComponent(/datum/component/theologist_piety)
+ theologist_piety?.update_screen_loc()
+
// Set initial text so it isn't blank until first adjust.
cultivator_ui.maptext = FORMAT_ENERGY_TEXT(energy)
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
index 1b6b67d3de7011..c5b0428a74a76b 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_power.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_power.dm
@@ -5,4 +5,5 @@
archetype = POWER_ARCHETYPE_RESONANT
path = POWER_PATH_CULTIVATOR
priority = POWER_PRIORITY_BASIC
+ magic_flags = POWER_MAGIC_STANDARD
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
index 31d96cc17000d5..5f1cb03761923a 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm
@@ -4,6 +4,7 @@
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
+ magic_flags = POWER_MAGIC_STANDARD
archetype = POWER_ARCHETYPE_RESONANT
path = POWER_PATH_CULTIVATOR
priority = POWER_PRIORITY_ROOT
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
index b496e0ff0df226..df155683990d36 100644
--- 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
@@ -6,6 +6,9 @@
value = 3
required_powers = list(/datum/power/cultivator_root/astral_touched)
+ menu_icon = 'icons/effects/effects.dmi'
+ menu_icon_state = "ion_trails"
+
/// the trailing particles
var/datum/effect_system/trail_follow/ion/grav_allowed/flight_trail
/// ref to the root power's action
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
index c6009772800da1..92b1551a17b1e0 100644
--- 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
@@ -9,6 +9,9 @@
value = 3
required_powers = list(/datum/power/cultivator_root/flame_soul)
+ menu_icon = 'icons/effects/effects.dmi'
+ menu_icon_state = "explosion_particle"
+
/// how much we BRING THE HEAT on our punches
var/bonus_heat = 20
/// the flame stacks we apply per punch
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
index 04185cc577eab5..dcb4175e10179a 100644
--- 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
@@ -11,6 +11,9 @@
required_powers = list(/datum/power/cultivator_root/shadow_walker)
power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES
+ menu_icon = 'icons/effects/effects.dmi'
+ menu_icon_state = "void_conduit"
+
/// Cached alignment action for gating effects.
var/datum/action/cooldown/power/cultivator/alignment/shadow_walker/shadow_walker_alignment
/// Current instance of the status effect
diff --git a/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_action.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_action.dm
new file mode 100644
index 00000000000000..873356d44406b0
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_action.dm
@@ -0,0 +1,4 @@
+/datum/action/cooldown/power/imbued
+ name = "abstract imbued power action - ahelp this"
+ background_icon_state = "bg_imbued"
+ overlay_icon_state = "bg_imbued_border"
diff --git a/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_datum.dm
new file mode 100644
index 00000000000000..f305bdcb040e63
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_datum.dm
@@ -0,0 +1,10 @@
+/datum/power_path/imbued
+ path_key = "imbued"
+ power_path = POWER_PATH_IMBUED
+ archetype_type = /datum/power_archetype/resonant
+ path_sort_order = POWER_PATH_SORT_QUATERNARY
+ display_name = "Imbued"
+ icon_asset_name = "imbuedicon.png"
+ mechanics_text = "Imbued applies largely passive effects to the owner, often adding strange and unusual interactions which require smarts and circumstance to wield effectively.\n\nAnomalous are supernatural effects unexpleinable by sciences but not magical. 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. There is no larger overarching mechanics; use each tool wisely.\nPowers from this category are not affected by antimagic!\n\nEnchanted creatures are more in-touch with magic. Their body interacts with magic phenomena, often having gimmicky, unusual qualities to them, or having enhanced effect when combined with other magical archetypes. Whilst it has no larger overarching mechanics, it often has synergies with other powers."
+ overview_text = "Whether resonant or anomalous, the Imbued have had how they interact with the world altered forever. They disobey the laws of science, even as their body remains the same."
+ theme_color = POWER_COLOR_IMBUED
diff --git a/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_power.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_power.dm
new file mode 100644
index 00000000000000..ecc107a4bcd603
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_power.dm
@@ -0,0 +1,8 @@
+/datum/power/imbued
+ name = "Abstract Imbued Power"
+ desc = "Denying all reasonable explenations for its existence, the abstract type is the boogeyman that haunts the scientific community. Quick, inform a man of computer science!"
+
+ archetype = POWER_ARCHETYPE_RESONANT
+ path = POWER_PATH_IMBUED
+ priority = POWER_PRIORITY_BASIC
+ abstract_parent_type = /datum/power/imbued
diff --git a/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_root.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_root.dm
new file mode 100644
index 00000000000000..d1b125e8651188
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_root.dm
@@ -0,0 +1,8 @@
+/datum/power/imbued_root
+ name = "Abstract Imbued Power"
+ desc = "Denying all reasonable explenations for its existence, the abstract type is the boogeyman that haunts the scientific community. Quick, inform a man of computer science!"
+ abstract_parent_type = /datum/power/imbued_root
+
+ archetype = POWER_ARCHETYPE_RESONANT
+ path = POWER_PATH_IMBUED
+ 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/imbued/_imbued_root_anomalous.dm
similarity index 75%
rename from modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm
rename to modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_root_anomalous.dm
index af2bba9bcdea6b..e687b6d68405b6 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_root_anomalous.dm
@@ -2,20 +2,22 @@
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"
+/datum/power/imbued_root/anomalous
+ name = "Anomalous"
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
+ menu_icon = 'icons/effects/effects.dmi'
+ menu_icon_state = "shield2"
-/datum/power/aberrant_root/anomalous/add(client/client_source)
+/datum/power/imbued_root/anomalous/add(client/client_source)
RegisterSignal(power_holder, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_unarmed_attack))
-/datum/power/aberrant_root/anomalous/remove()
+/datum/power/imbued_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)
+/datum/power/imbued_root/anomalous/proc/on_unarmed_attack(mob/living/source, atom/target, proximity, modifiers)
SIGNAL_HANDLER
if(!proximity || !istype(target, /obj/effect/anomaly))
diff --git a/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_root_enchanted.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_root_enchanted.dm
new file mode 100644
index 00000000000000..5313dff5c5696e
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/_imbued_root_enchanted.dm
@@ -0,0 +1,109 @@
+/*
+ Enchanted root. You are magical! Accelerates qualifying cooldown recovery based on your magical power investment.
+*/
+
+/datum/power/imbued_root/enchanted
+ name = "Enchanted"
+ desc = "You ambiently seem to interact more positively with magic. So long as you are not silenced, you regenerate action cooldowns faster based on your magical prowress.\
+ \nEvery point invested in a magical power makes your cooldowns go 0.5% faster; this becomes 2% if the power is in the Imbued path.\
+ \nThis only affects magical power actions, plus spell-type actions."
+ security_record_text = "Subject seems to be able to wield existing magical powers more often."
+ security_threat = POWER_THREAT_MAJOR
+ value = 2
+ magic_flags = POWER_MAGIC_STANDARD
+ menu_icon = 'icons/effects/effects.dmi'
+ menu_icon_state = "quantum_sparks"
+ power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES
+ no_process_traits = list(TRAIT_RESONANCE_SILENCED)
+
+ ///CDR = Cooldown Reduction. Is a gamer term.
+
+ /// Flat deciseconds removed from qualifying cooldowns per second before power-based scaling.
+ var/CDR_base = 0
+ /// Additional deciseconds removed per point of non-Imbued magical power value.
+ var/CDR_per_magic_value = 0.05
+ /// Additional deciseconds removed per point of Imbued magical power value.
+ var/CDR_per_imbued_magic_value = 0.2
+ /// Cached base cooldown recovery per second from the holder's current powers.
+ var/CDR_cache = 0
+ /// Whether the cached cooldown recovery value needs to be rebuilt.
+ var/CDR_cache_dirty = TRUE
+
+/datum/power/imbued_root/enchanted/add(client/client_source)
+ . = ..()
+ RegisterSignals(power_holder, list(COMSIG_MOB_POWER_ADDED, COMSIG_MOB_POWER_REMOVED), PROC_REF(on_power_list_changed))
+
+/datum/power/imbued_root/enchanted/remove()
+ . = ..()
+ UnregisterSignal(power_holder, list(COMSIG_MOB_POWER_ADDED, COMSIG_MOB_POWER_REMOVED))
+
+/datum/power/imbued_root/enchanted/process(seconds_per_tick)
+ if(!length(power_holder?.actions))
+ return
+
+ // This calculates the BASE value
+ var/cooldown_reduction = get_CDR_per_second() * seconds_per_tick
+ // This claculates the MULTIPLIERS value
+ cooldown_reduction *= get_recovery_multiplier()
+ if(cooldown_reduction <= 0)
+ return
+
+ for(var/datum/action/cooldown/cooldown_action as anything in power_holder.actions)
+ if(!should_accelerate_action(cooldown_action))
+ continue
+ if(cooldown_action.next_use_time <= world.time)
+ continue
+
+ cooldown_action.next_use_time = max(world.time, cooldown_action.next_use_time - cooldown_reduction)
+ cooldown_action.build_all_button_icons(UPDATE_BUTTON_STATUS)
+
+/// Calculates the base cooldown recovery rate from the holder's magical powers.
+/// This defaults to using the cached value for optimization; and if its dirty/non-existant we cache a new vlaue.
+/datum/power/imbued_root/enchanted/proc/get_CDR_per_second()
+ if(CDR_cache_dirty)
+ cache_CDR_per_second()
+
+ return CDR_cache
+
+/// Calculates our effective base cooldown recovery bonus and caches it for later procs.
+/datum/power/imbued_root/enchanted/proc/cache_CDR_per_second()
+ CDR_cache = CDR_base
+
+ for(var/datum/power/power_instance as anything in power_holder?.powers)
+ if(!power_instance)
+ continue
+ if(!power_instance.magic_flags)
+ continue
+ // Imbued gives extra
+ if(power_instance.path == POWER_PATH_IMBUED)
+ CDR_cache += power_instance.value * CDR_per_imbued_magic_value
+ continue
+ // Its magical, so it gives a reduction.
+ CDR_cache += power_instance.value * CDR_per_magic_value
+
+ CDR_cache_dirty = FALSE
+
+/// Listens to changes in powers and marks the cache as dirty if the mob's powers are adjusted.
+/datum/power/imbued_root/enchanted/proc/on_power_list_changed(mob/living/source, datum/power/changed_power)
+ SIGNAL_HANDLER
+
+ CDR_cache_dirty = TRUE
+
+/// How much we multiply our recovery rate by after all enchanted rider powers contribute their additive percentages.
+/datum/power/imbued_root/enchanted/proc/get_recovery_multiplier()
+ var/list/recovery_bonus_percents = list()
+ SEND_SIGNAL(power_holder, COMSIG_IMBUED_ENCHANTED_RECOVERY_MODIFIERS, recovery_bonus_percents)
+
+ var/total_bonus_percent = 0
+ for(var/recovery_bonus_percent in recovery_bonus_percents)
+ total_bonus_percent += recovery_bonus_percent
+
+ return 1 + (total_bonus_percent / 100)
+
+/// Proc that returns TRUE/FALSE if a power is qualified to get a CDR reduction from enchanted.
+/datum/power/imbued_root/enchanted/proc/should_accelerate_action(datum/action/cooldown/cooldown_action)
+ if(istype(cooldown_action, /datum/action/cooldown/power))
+ var/datum/action/cooldown/power/power_action = cooldown_action
+ return power_action.is_magical()
+
+ return istype(cooldown_action, /datum/action/cooldown/spell)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/imbued/alcohol_is_mana.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/alcohol_is_mana.dm
new file mode 100644
index 00000000000000..a32df24b48f667
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/alcohol_is_mana.dm
@@ -0,0 +1,43 @@
+/*
+ Alcohol is mana! Basically lets you upscale your cooldowns greatly by consuming alcohol.
+ Effects are transfered using a signaler.
+*/
+/datum/power/imbued/alcohol_is_mana
+ name = "Alcohol is Mana"
+ desc = "Whilst some are still trying to understand if the concept of 'Mana' is real and whether it is a substance or just an ephemeral energy, you certainly have found your 'Mana'.\
+ \nYour Enchanted root power has increased effectiveness depending on your degree of intoxication, up to a maximum of 500% increased effectiveness."
+ security_record_text = "Subject seems to be able to wield existing magical powers much more often when inebriated."
+ security_threat = POWER_THREAT_MAJOR
+ value = 2
+ magic_flags = POWER_MAGIC_STANDARD
+
+ required_powers = list(/datum/power/imbued_root/enchanted)
+ menu_icon = 'icons/obj/drinks/mixed_drinks.dmi'
+ menu_icon_state = "wizz_fizz"
+
+ /// How much drunkenness contributes to Enchanted's bonus recovery percentage.
+ var/alcohol_to_recovery_percent = 5
+ /// The cap on how much bonus recovery percentage Alcohol is Mana! can contribute.
+ var/maximum_recovery_percent = 500
+
+/datum/power/imbued/alcohol_is_mana/add(client/client_source)
+ . = ..()
+ RegisterSignal(power_holder, COMSIG_IMBUED_ENCHANTED_RECOVERY_MODIFIERS, PROC_REF(add_enchanted_recovery_modifier))
+
+/datum/power/imbued/alcohol_is_mana/remove()
+ . = ..()
+ UnregisterSignal(power_holder, COMSIG_IMBUED_ENCHANTED_RECOVERY_MODIFIERS)
+
+/// Sends a signal to Enchanted to modify the given amount based on your durnkenness.
+/datum/power/imbued/alcohol_is_mana/proc/add_enchanted_recovery_modifier(mob/living/source, list/recovery_bonus_percents)
+ SIGNAL_HANDLER
+
+ if(!istype(source))
+ return NONE
+
+ var/drunk_amount = source.get_drunk_amount()
+ if(drunk_amount <= 0)
+ return NONE
+
+ recovery_bonus_percents += min(drunk_amount * alcohol_to_recovery_percent, maximum_recovery_percent)
+ return NONE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/imbued/mage_sight.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/mage_sight.dm
new file mode 100644
index 00000000000000..3dd615bf4a6cf3
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/mage_sight.dm
@@ -0,0 +1,168 @@
+/datum/power/imbued/mage_sight
+ name = "Mage Sight"
+ desc = "You can see the magic in the air around people with a bit of intense focus. When activated, you see a cyan aura around any creature currently near you that has magical powers, or is capable of casting spell-type actions. This persists for 6 seconds.\
+ \nIf you both share a path and the target has at least one magical power in that path, you will see orange-colored aura on them instead, verifying that you both share at least one path. If the target is immune to resonant scrying or magic, you won't detect anything.\
+ \nHas a 30 second cooldown."
+ security_record_text = "Subject can identify other magic-using individuals."
+ value = 2
+ magic_flags = POWER_MAGIC_STANDARD | POWER_MAGIC_SCRYING
+
+ required_powers = list(/datum/power/imbued_root/enchanted)
+ action_path = /datum/action/cooldown/power/imbued/mage_sight
+
+/datum/action/cooldown/power/imbued/mage_sight
+ name = "Mage Sight"
+ desc = "When activated, you see a cyan aura around any creature currently near you that has magical powers, or is capable of casting spell-type actions. This persists for 6 seconds.\
+ \nIf you both share a path and the target has at least one magical power in that path, you will see orange-colored aura on them instead, verifying that you both share at least one path. If the target is immune to resonant scrying or magic, you won't detect anything."
+ button_icon = 'icons/effects/effects.dmi'
+ button_icon_state = "blip"
+ cooldown_time = 30 SECONDS
+
+ /// Client-only images currently being shown by this activation.
+ var/list/mob/living/tracked_targets = list()
+ var/list/image/target_images = list()
+ /// Cached left eye color for restoring the caster after the effect ends.
+ var/cached_left_eye_color
+ /// Cached right eye color for restoring the caster after the effect ends.
+ var/cached_right_eye_color
+
+/datum/action/cooldown/power/imbued/mage_sight/Remove(mob/removed_from)
+ clear_mage_sight_overlays()
+ removed_from?.remove_client_colour(REF(src))
+ restore_caster_visuals(removed_from)
+ return ..()
+
+/datum/action/cooldown/power/imbued/mage_sight/use_action(mob/living/carbon/human/user, atom/target)
+ if(!user?.client)
+ return FALSE
+
+ apply_caster_visuals(user)
+ show_mage_sight_overlays(user, 6 SECONDS)
+ user.playsound_local(get_turf(user), 'sound/effects/magic/swap.ogg', 50, TRUE)
+ user.add_client_colour(/datum/client_colour/mage_sight_flash, REF(src))
+ addtimer(CALLBACK(src, PROC_REF(remove_caster_flash), user), 0.5 SECONDS)
+ addtimer(CALLBACK(src, PROC_REF(restore_caster_visuals), user), 6 SECONDS)
+ return TRUE
+
+/// Shows the per-viewer sight overlay on every valid mob in view.
+/datum/action/cooldown/power/imbued/mage_sight/proc/show_mage_sight_overlays(mob/living/user, duration)
+ if(!user?.client)
+ return
+
+ clear_mage_sight_overlays()
+
+ for(var/mob/living/seen_mob in view(user))
+ if(seen_mob == user)
+ continue
+ var/overlay_color = get_mage_sight_color(user, seen_mob)
+ if(!overlay_color)
+ continue
+
+ var/image/mage_sight_image = image(icon = 'icons/effects/effects.dmi', loc = seen_mob, icon_state = "blip", layer = BELOW_MOB_LAYER)
+ mage_sight_image.color = overlay_color
+
+ user.client.images += mage_sight_image
+ tracked_targets += seen_mob
+ target_images[seen_mob] = mage_sight_image
+
+ if(duration > 0)
+ addtimer(CALLBACK(src, PROC_REF(clear_mage_sight_overlays)), duration)
+
+/// Applies the temporary eye-color change to the caster.
+/datum/action/cooldown/power/imbued/mage_sight/proc/apply_caster_visuals(mob/living/carbon/human/user)
+ cached_left_eye_color = user.eye_color_left
+ cached_right_eye_color = user.eye_color_right
+ user.set_eye_color(POWER_COLOR_IMBUED, COLOR_CYAN)
+ user.update_body()
+
+/// Restores the caster's original eye colors after the effect ends.
+/datum/action/cooldown/power/imbued/mage_sight/proc/restore_caster_visuals(mob/living/carbon/human/user)
+ if(!user)
+ return
+ if(isnull(cached_left_eye_color) || isnull(cached_right_eye_color))
+ return
+
+ user.set_eye_color(cached_left_eye_color, cached_right_eye_color)
+ user.update_body()
+ cached_left_eye_color = null
+ cached_right_eye_color = null
+
+/// Removes the short client flash applied when mage sight activates.
+/datum/action/cooldown/power/imbued/mage_sight/proc/remove_caster_flash(mob/living/carbon/human/user)
+ user?.remove_client_colour(REF(src))
+
+/// Removes all client-only overlays created by mage sight.
+/datum/action/cooldown/power/imbued/mage_sight/proc/clear_mage_sight_overlays()
+ if(owner?.client)
+ for(var/mob/living/seen_mob as anything in tracked_targets)
+ var/image/mage_sight_image = target_images[seen_mob]
+ if(mage_sight_image)
+ owner.client.images -= mage_sight_image
+
+ tracked_targets.Cut()
+ target_images.Cut()
+
+/// Returns the overlay color for a valid target, or null if the target should not be highlighted.
+/datum/action/cooldown/power/imbued/mage_sight/proc/get_mage_sight_color(mob/living/user, mob/living/target_mob)
+ if(!user || !target_mob)
+ return null
+ if(HAS_TRAIT(target_mob, TRAIT_ANTIRESONANCE_SCRYING) || target_mob.can_block_resonance(0)) // scrying immunity and magic immunity block info
+ return null
+
+ // If the mob is detected to have a power tagged as interacting with magic systems.
+ var/has_magical_power = FALSE
+ // If the mob is detected having a tagged power in a path you share.
+ var/shares_magical_path = FALSE
+
+ // Iterates all powers and checks if they are magic-tagged and share a path.
+ for(var/datum/power/target_power as anything in target_mob.powers)
+ if(!power_has_magic_flags(target_power))
+ continue
+
+ has_magical_power = TRUE
+ if(!shares_magical_path && owner_has_magical_power_in_path(user, target_power.path))
+ shares_magical_path = TRUE
+
+ if(has_magical_power)
+ return shares_magical_path ? POWER_COLOR_IMBUED : COLOR_CYAN
+
+ if(has_spell_type_action(target_mob))
+ return COLOR_CYAN
+
+ return null
+
+/// Whether the owner has any magical power in the specified path.
+/datum/action/cooldown/power/imbued/mage_sight/proc/owner_has_magical_power_in_path(mob/living/user, power_path)
+ if(!user || !power_path)
+ return FALSE
+
+ for(var/datum/power/owner_power as anything in user.powers)
+ if(!power_has_magic_flags(owner_power))
+ continue
+ if(owner_power.path == power_path)
+ return TRUE
+
+ return FALSE
+
+/// Whether a power participates in any of the magic-interaction domains.
+/datum/action/cooldown/power/imbued/mage_sight/proc/power_has_magic_flags(datum/power/power_type)
+ if(!power_type)
+ return FALSE
+ return !!power_type.magic_flags
+
+/// Whether the target can cast standard spell actions.
+/datum/action/cooldown/power/imbued/mage_sight/proc/has_spell_type_action(mob/living/target_mob)
+ if(!length(target_mob?.actions))
+ return FALSE
+
+ for(var/datum/action/cooldown/action_datum as anything in target_mob.actions)
+ if(istype(action_datum, /datum/action/cooldown/spell))
+ return TRUE
+
+ return FALSE
+
+/datum/client_colour/mage_sight_flash
+ priority = CLIENT_COLOR_IMPORTANT_PRIORITY
+ color = POWER_COLOR_IMBUED
+ fade_in = 0.125 SECONDS
+ fade_out = 0.125 SECONDS
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/radiosynthesis.dm
similarity index 89%
rename from modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm
rename to modular_doppler/modular_powers/code/powers/resonant/imbued/radiosynthesis.dm
index 5e6c663f17b498..df7a5b9fcdcf30 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/radiosynthesis.dm
@@ -1,20 +1,22 @@
/*
Sunbathe under the Supermatter for healing. Doctors hate this trick! Heals every damage type except oxyloss.
*/
-/datum/power/aberrant/radiosynthesis
+/datum/power/imbued/radiosynthesis
name = "Radiosynthesis"
desc = "Rather than the molecular degradation 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 body parts."
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/imbued_root/anomalous)
- required_powers = list(/datum/power/aberrant_root/anomalous)
+ menu_icon = 'icons/hud/screen_alert.dmi'
+ menu_icon_state = "irradiated"
/// how much we heal per second
var/healing = 1
-/datum/power/aberrant/radiosynthesis/process(seconds_per_tick)
+/datum/power/imbued/radiosynthesis/process(seconds_per_tick)
// Only heal if we're irradiated
if(!HAS_TRAIT(power_holder, TRAIT_IRRADIATED))
return
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/resonant_immune.dm
similarity index 75%
rename from modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm
rename to modular_doppler/modular_powers/code/powers/resonant/imbued/resonant_immune.dm
index eb0054349b7c54..77782f59a6f268 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/resonant_immune.dm
@@ -1,20 +1,22 @@
/*
You're immune to resonant antics! But also you're permanently silenced.
*/
-/datum/power/aberrant/counter_resonance
+/datum/power/imbued/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 Radiosynthesis, 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/imbued_root/anomalous)
- required_powers = list(/datum/power/aberrant_root/anomalous)
+ menu_icon = 'icons/effects/effects.dmi'
+ menu_icon_state = "shield-old"
-/datum/power/aberrant/counter_resonance/add()
+/datum/power/imbued/counter_resonance/add()
ADD_TRAIT(power_holder, TRAIT_ANTIRESONANCE, src)
ADD_TRAIT(power_holder, TRAIT_RESONANCE_SILENCED, src)
-/datum/power/aberrant/counter_resonance/remove()
+/datum/power/imbued/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/imbued/returning_throws.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/returning_throws.dm
new file mode 100644
index 00000000000000..c239feba498fb6
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/returning_throws.dm
@@ -0,0 +1,474 @@
+/*
+ Thrown items return to you and are caught cleanly. This uses a component that whilst similar to boomerang, makes it behave much more generously.
+*/
+/datum/power/imbued/returning_throws
+ name = "Returning Throws"
+ desc = "Anything you throw forth with intent seems to come flying back to you! Whilst active, throwing objects will cause them to fly back towards you after traveling their maximum distance or hitting a target, so long as there is a direct path.\
+ \nYou will always succesfully catch the object, regardless if you have your throw intent set or not. Returning Throws an be toggled on or off."
+ security_record_text = "Subject displays a tendency for thrown handheld objects to return to their hands through seemingly impossible trajectories."
+ security_threat = POWER_THREAT_MAJOR
+ value = 5
+ magic_flags = POWER_MAGIC_STANDARD
+ action_path = /datum/action/cooldown/power/imbued/returning_throws
+
+ required_powers = list(/datum/power/imbued_root/enchanted)
+
+/// Action button for enabling or disabling Returning Throws.
+/datum/action/cooldown/power/imbued/returning_throws
+ name = "Returning Throws"
+ desc = "Toggles Returning Throws on or off. Whilst active, throwing objects will cause them to fly back towards you after traveling their maximum distance or hitting a target, so long as there is a direct path.\
+ \nYou will always succesfully catch the object, regardless if you have your throw intent set or not."
+ button_icon = 'icons/obj/weapons/spear.dmi'
+ button_icon_state = "dragoonpole1"
+ cooldown_time = 0
+ active_overlay_icon_state = "bg_spell_border_active_blue" // changing the color to indicate its active is cooler
+
+ /// Every item this action has marked so it can clean them up on removal.
+ var/list/managed_item_refs = list()
+ /// How many return hops attuned items should attempt before giving up.
+ var/return_hop_attempts = 3
+
+/// Registers the held-item signaler and tracks any held item
+/datum/action/cooldown/power/imbued/returning_throws/Grant(mob/granted_to)
+ . = ..()
+ active = TRUE
+ RegisterSignal(granted_to, COMSIG_MOB_UPDATE_HELD_ITEMS, PROC_REF(on_held_items_updated))
+ if(active)
+ refresh_held_items(granted_to)
+ build_all_button_icons(UPDATE_BUTTON_OVERLAY)
+
+/// Unregisters signals and untracks held items.
+/datum/action/cooldown/power/imbued/returning_throws/Remove(mob/removed_from)
+ UnregisterSignal(removed_from, COMSIG_MOB_UPDATE_HELD_ITEMS)
+ remove_all_managed_items(removed_from)
+ . = ..()
+
+/// Toggles between on/off and tracks/untracks held items consequently.
+/datum/action/cooldown/power/imbued/returning_throws/use_action(mob/living/user, atom/target)
+ active = !active
+ if(active)
+ refresh_held_items(user)
+ else
+ remove_all_managed_items(user)
+ owner.balloon_alert(owner, active ? "returning throws on" : "returning throws off")
+ build_all_button_icons(UPDATE_BUTTON_OVERLAY)
+ return TRUE
+
+/// Override to enable the active_overlay_icon_state to be visible when active (which is normally hoarded by targeted action's default behavior)
+/datum/action/cooldown/power/imbued/returning_throws/is_action_active(atom/movable/screen/movable/action_button/current_button)
+ return active
+
+/// Refreshes item tracking whenever our held items are changed
+/datum/action/cooldown/power/imbued/returning_throws/proc/on_held_items_updated(mob/living/power_owner)
+ SIGNAL_HANDLER
+ refresh_held_items(power_owner)
+
+/// Iterates the owner's current hand items and ensures each one is tracked
+/datum/action/cooldown/power/imbued/returning_throws/proc/refresh_held_items(mob/living/power_owner)
+ if(!active)
+ return
+
+ if(!istype(power_owner))
+ return
+
+ clear_invalid_managed_refs()
+
+ for(var/obj/item/held_item as anything in power_owner.held_items)
+ if(!istype(held_item))
+ continue
+ attune_item(power_owner, held_item)
+
+/// Adds or refreshes the returning-throw component on a specific held item.
+/datum/action/cooldown/power/imbued/returning_throws/proc/attune_item(mob/living/power_owner, obj/item/held_item)
+ if(QDELETED(held_item))
+ return
+
+ var/datum/component/returning_throw_attunement/attunement = held_item.GetComponent(/datum/component/returning_throw_attunement)
+ if(attunement)
+ if(attunement.belongs_to(power_owner))
+ attunement.max_return_hops = return_hop_attempts
+ track_managed_item(held_item)
+ return
+
+ held_item.AddComponent(/datum/component/returning_throw_attunement, power_owner, return_hop_attempts, TRUE)
+ track_managed_item(held_item)
+
+/// Stores a weak reference so the action can later clean up only items it touched.
+/datum/action/cooldown/power/imbued/returning_throws/proc/track_managed_item(obj/item/managed_item)
+ if(QDELETED(managed_item))
+ return
+
+ var/datum/weakref/item_ref = WEAKREF(managed_item)
+ if(item_ref in managed_item_refs)
+ return
+ managed_item_refs += item_ref
+
+/// Removes dead weak references from the managed item list.
+/datum/action/cooldown/power/imbued/returning_throws/proc/clear_invalid_managed_refs()
+ for(var/datum/weakref/item_ref as anything in managed_item_refs.Copy())
+ if(!item_ref?.resolve())
+ managed_item_refs -= item_ref
+
+/// Deletes every component that still belongs to the provided owner, or the action owner if omitted.
+/datum/action/cooldown/power/imbued/returning_throws/proc/remove_all_managed_items(mob/living/power_owner = owner)
+ for(var/datum/weakref/item_ref as anything in managed_item_refs.Copy())
+ var/obj/item/managed_item = item_ref?.resolve()
+ if(QDELETED(managed_item))
+ continue
+
+ var/datum/component/returning_throw_attunement/attunement = managed_item.GetComponent(/datum/component/returning_throw_attunement)
+ if(attunement?.belongs_to(power_owner))
+ qdel(attunement)
+
+ managed_item_refs.Cut()
+
+/*
+ Item-side component for Returning Throws.
+ While the owner has an item in hand, it becomes a forced-catch returning throw. Whilst this is similar to boomerang, it is more magical in that it tries to hop and curve back.
+ The attunement stays on the item until another mob claims it.
+*/
+/datum/component/returning_throw_attunement
+ dupe_mode = COMPONENT_DUPE_HIGHLANDER
+
+ /// Filter id used for the temporary returning-throw outline.
+ var/static/throw_outline_filter_id = "returning_throw_outline"
+
+ /// The mob who originally enchanted this item.
+ var/datum/weakref/owner_ref
+ /// Whether this component should delete itself when it loses its assigned owner or changes hands.
+ var/self_terminate = FALSE
+ /// Maximum number of return hops this attunement will attempt for a throw.
+ var/max_return_hops = 3
+ /// Whether the current throw is one of our guided return hops.
+ var/is_returning = FALSE
+ /// How many guided return hops remain before we leave the item where it landed.
+ var/remaining_return_hops = 0
+ /// Whether this return sequence has already spent its single allowed mob hit.
+ var/return_hit_mob = FALSE
+ /// Whether the next relaunch should be free because the item hit a mob on the return trip.
+ var/pending_free_return_hop = FALSE
+ /// Whether this attunement currently owns the temporary throw visuals.
+ var/created_throw_effect = FALSE
+ /// Whether this component temporarily granted the item PASSMOB for the current return sequence.
+ var/added_passmob = FALSE
+
+/// Validates the parent item, records which mob owns this component, and stores its allowed return-hop count.
+/datum/component/returning_throw_attunement/Initialize(mob/living/owner, max_return_hops, self_terminate = FALSE)
+ . = ..()
+ if(!isitem(parent))
+ return COMPONENT_INCOMPATIBLE
+
+ if(istype(owner))
+ owner_ref = WEAKREF(owner)
+ else if(self_terminate)
+ return COMPONENT_INCOMPATIBLE
+
+ src.self_terminate = self_terminate
+ src.max_return_hops = max(0, max_return_hops)
+
+/// Hooks the item signals needed for ownership, catch interception, and throw visuals.
+/datum/component/returning_throw_attunement/RegisterWithParent()
+ RegisterSignal(parent, COMSIG_ITEM_PICKUP, PROC_REF(on_item_picked_up))
+ RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_item_equipped))
+ RegisterSignal(parent, COMSIG_MOVABLE_POST_THROW, PROC_REF(on_post_throw))
+ RegisterSignal(parent, COMSIG_MOVABLE_PRE_IMPACT, PROC_REF(on_pre_impact))
+ RegisterSignal(parent, COMSIG_MOVABLE_IMPACT, PROC_REF(on_throw_impact))
+ RegisterSignal(parent, COMSIG_MOVABLE_THROW_LANDED, PROC_REF(on_throw_landed))
+
+/// Unhooks every signal installed by this attunement.
+/datum/component/returning_throw_attunement/UnregisterFromParent()
+ UnregisterSignal(parent, list(COMSIG_ITEM_PICKUP, COMSIG_ITEM_EQUIPPED, COMSIG_MOVABLE_POST_THROW, COMSIG_MOVABLE_PRE_IMPACT, COMSIG_MOVABLE_IMPACT, COMSIG_MOVABLE_THROW_LANDED))
+
+/// Removes temporary visuals and clears throw state before deletion.
+/datum/component/returning_throw_attunement/Destroy(force)
+ clear_throw_effect()
+ is_returning = FALSE
+ remaining_return_hops = 0
+ reset_return_trip_state()
+ owner_ref = null
+ return ..()
+
+/// Returns TRUE when this attunement belongs to the queried mob.
+/datum/component/returning_throw_attunement/proc/belongs_to(mob/living/possible_owner)
+ return owner_ref?.resolve() == possible_owner
+
+/// Adds the temporary outline for the duration of the throw if one is not already present.
+/datum/component/returning_throw_attunement/proc/ensure_throw_effect()
+ var/obj/item/parent_item = parent
+ if(QDELETED(parent_item))
+ return
+
+ if(created_throw_effect)
+ return
+
+ parent_item.add_filter(throw_outline_filter_id, 2, outline_filter(2, POWER_COLOR_IMBUED))
+ created_throw_effect = TRUE
+
+/// Removes the temporary throw visuals, but only if this attunement created them.
+/datum/component/returning_throw_attunement/proc/clear_throw_effect()
+ if(!created_throw_effect)
+ return
+
+ var/obj/item/parent_item = parent
+ if(QDELETED(parent_item))
+ created_throw_effect = FALSE
+ return
+
+ parent_item.remove_filter(throw_outline_filter_id)
+ created_throw_effect = FALSE
+
+/// Clears per-return state so later throws start clean and temporary mob phasing is removed.
+/datum/component/returning_throw_attunement/proc/reset_return_trip_state()
+ pending_free_return_hop = FALSE
+ return_hit_mob = FALSE
+
+ if(!added_passmob)
+ return
+
+ var/obj/item/parent_item = parent
+ if(!QDELETED(parent_item))
+ parent_item.pass_flags &= ~PASSMOB
+ added_passmob = FALSE
+
+/// Lets the returning item pass through non-target mobs after its single allowed return hit is spent.
+/datum/component/returning_throw_attunement/proc/enable_return_mob_phasing(obj/item/parent_item)
+ if(parent_item.pass_flags & PASSMOB)
+ return
+
+ parent_item.pass_flags |= PASSMOB
+ added_passmob = TRUE
+
+/// Returns TRUE when the owner is close enough to a landed item that we should snap it back into hand.
+/datum/component/returning_throw_attunement/proc/is_within_close_catch_range(mob/living/owner, obj/item/parent_item)
+ if(owner.z != parent_item.z)
+ return FALSE
+
+ return get_dist(owner, parent_item) <= 1
+
+/// Starts another return hop aimed at the owner's current position instead of the stale original target turf.
+/datum/component/returning_throw_attunement/proc/launch_return_hop(mob/living/owner, obj/item/parent_item, return_speed)
+ if(QDELETED(owner) || QDELETED(parent_item))
+ return FALSE
+
+ if(parent_item.throwing)
+ return FALSE
+
+ var/final_return_speed = max(1, return_speed || parent_item.throw_speed)
+ var/return_range = max(1, get_dist(parent_item, owner) + parent_item.throw_range + 1)
+ is_returning = TRUE
+ parent_item.throw_at(owner, return_range, final_return_speed, owner, TRUE)
+ return TRUE
+
+/// Continues the return path after a landing by catching nearby items, re-aiming distant ones, or stopping after several failed hops.
+/datum/component/returning_throw_attunement/proc/continue_return_to_owner(mob/living/owner, obj/item/parent_item, return_speed)
+ if(QDELETED(owner) || QDELETED(parent_item))
+ qdel(src)
+ return
+
+ // We caught it :D
+ if(parent_item.loc == owner)
+ clear_throw_effect()
+ is_returning = FALSE
+ remaining_return_hops = 0
+ reset_return_trip_state()
+ return
+
+ // If it lands adjacent to the owner, we attempt to force it into their hands.
+ if(is_within_close_catch_range(owner, parent_item) && force_catch_in_hand(owner, parent_item))
+ clear_throw_effect()
+ is_returning = FALSE
+ remaining_return_hops = 0
+ reset_return_trip_state()
+ owner.throw_mode_off(THROW_MODE_TOGGLE)
+ owner.visible_message(
+ span_notice("[owner] catches [parent_item] as it whips back around."),
+ span_notice("You catch [parent_item] as it whips back into your hand."),
+ )
+ return
+
+ // We ran out of hops D:
+ if(remaining_return_hops <= 0)
+ clear_throw_effect()
+ is_returning = FALSE
+ reset_return_trip_state()
+ return
+
+ // Deducts a return-hop unless the free return hop var was set to TRUE
+ if(pending_free_return_hop)
+ pending_free_return_hop = FALSE
+ else
+ remaining_return_hops--
+
+ // Launches ourselves again
+ if(!launch_return_hop(owner, parent_item, return_speed))
+ clear_throw_effect()
+ is_returning = FALSE
+ reset_return_trip_state()
+
+/// Defers the next return decision until after the current throw datum has fully cleaned itself up.
+/datum/component/returning_throw_attunement/proc/queue_return_resolution(mob/living/owner, obj/item/parent_item, return_speed)
+ addtimer(CALLBACK(src, PROC_REF(continue_return_to_owner), owner, parent_item, return_speed), 0.1 SECONDS)
+
+/// Places the returning item directly into a hand without going through stack merge logic.
+/datum/component/returning_throw_attunement/proc/force_catch_in_hand(mob/living/owner, obj/item/parent_item)
+ var/empty_active_hand_index = owner.can_put_in_hand(parent_item, owner.active_hand_index) ? owner.active_hand_index : null
+ if(!isnull(empty_active_hand_index))
+ return owner.put_in_hand(parent_item, empty_active_hand_index, forced = TRUE, ignore_anim = TRUE)
+
+ var/inactive_hand_index = owner.get_inactive_hand_index()
+ var/empty_inactive_hand_index = owner.can_put_in_hand(parent_item, inactive_hand_index) ? inactive_hand_index : null
+ if(!isnull(empty_inactive_hand_index))
+ return owner.put_in_hand(parent_item, empty_inactive_hand_index, forced = TRUE, ignore_anim = TRUE)
+
+ return owner.put_in_hand(parent_item, owner.active_hand_index, forced = TRUE, ignore_anim = TRUE)
+
+/// Removes the component if the item is picked up by anyone other than its owner.
+/datum/component/returning_throw_attunement/proc/on_item_picked_up(obj/item/source, mob/taker)
+ SIGNAL_HANDLER
+ var/mob/living/owner = owner_ref?.resolve()
+ if(!istype(owner))
+ if(self_terminate)
+ qdel(src)
+ return
+
+ clear_throw_effect()
+ is_returning = FALSE
+ remaining_return_hops = 0
+ reset_return_trip_state()
+ if(self_terminate && taker != owner)
+ qdel(src)
+
+/// Removes the component if the item is equipped by anyone other than its owner.
+/datum/component/returning_throw_attunement/proc/on_item_equipped(obj/item/source, mob/equipper, slot)
+ SIGNAL_HANDLER
+ var/mob/living/owner = owner_ref?.resolve()
+ if(!istype(owner))
+ if(self_terminate)
+ qdel(src)
+ return
+
+ clear_throw_effect()
+ is_returning = FALSE
+ remaining_return_hops = 0
+ reset_return_trip_state()
+ if(self_terminate && equipper != owner)
+ qdel(src)
+
+/// Starts a fresh return sequence and applies the temporary outline when the attuned owner throws the item.
+/datum/component/returning_throw_attunement/proc/on_post_throw(datum/source, datum/thrownthing/throwingdatum, spin)
+ SIGNAL_HANDLER
+ var/mob/living/owner = owner_ref?.resolve()
+ var/mob/living/thrower = throwingdatum?.get_thrower()
+ // If there is no owner, we either self-terminate or we usurpt it with the htrower
+ if(!istype(owner))
+ if(self_terminate)
+ qdel(src)
+ return
+ if(!istype(thrower))
+ return
+ owner = thrower
+ owner_ref = WEAKREF(thrower)
+
+ if(!istype(thrower))
+ return
+ // If we are not self-terminating and someone stole the item and threw it, they are now the onwer.
+ if(!self_terminate && thrower != owner)
+ owner = thrower
+ owner_ref = WEAKREF(thrower)
+
+ // If thrower/owner are not the same (cause they at this poitn should be the same), something again went wrong and we need to top.
+ if(thrower != owner)
+ return
+
+ // Sets the remaining return hops to the configured max (inherited from the action or the component)
+ if(!is_returning)
+ reset_return_trip_state()
+ if(!is_returning)
+ remaining_return_hops = max_return_hops
+ ensure_throw_effect()
+
+/// Intercepts the return impact on the owner and force-places the item back into their hands.
+/datum/component/returning_throw_attunement/proc/on_pre_impact(datum/source, atom/hit_atom, datum/thrownthing/throwingdatum)
+ SIGNAL_HANDLER
+ var/mob/living/owner = owner_ref?.resolve()
+ var/obj/item/parent_item = parent
+ if(QDELETED(parent_item))
+ qdel(src)
+ return COMPONENT_MOVABLE_IMPACT_NEVERMIND
+
+ if(!istype(owner))
+ if(self_terminate)
+ qdel(src)
+ return COMPONENT_MOVABLE_IMPACT_NEVERMIND
+
+ if(hit_atom != owner)
+ return
+
+ if(throwingdatum?.get_thrower() != owner)
+ return
+
+ if(force_catch_in_hand(owner, parent_item))
+ clear_throw_effect()
+ is_returning = FALSE
+ remaining_return_hops = 0
+ reset_return_trip_state()
+ owner.throw_mode_off(THROW_MODE_TOGGLE)
+ owner.visible_message(
+ span_notice("[owner] catches [parent_item] as it whips back around."),
+ span_notice("You catch [parent_item] as it whips back into your hand."),
+ )
+ return COMPONENT_MOVABLE_IMPACT_NEVERMIND
+
+/// After the first return-trip mob hit, grant a free relaunch and phase through further mobs until the item is caught or gives up.
+/datum/component/returning_throw_attunement/proc/on_throw_impact(datum/source, atom/hit_atom, datum/thrownthing/throwingdatum, caught)
+ SIGNAL_HANDLER
+ if(caught || !isliving(hit_atom) || !is_returning)
+ return
+
+ var/mob/living/owner = owner_ref?.resolve()
+ var/obj/item/parent_item = parent
+ if(QDELETED(parent_item))
+ qdel(src)
+ return
+
+ if(!istype(owner))
+ if(self_terminate)
+ qdel(src)
+ return
+
+ var/mob/living/struck_mob = hit_atom
+ if(struck_mob == owner)
+ return
+
+ if(return_hit_mob)
+ return
+
+ return_hit_mob = TRUE
+ pending_free_return_hop = TRUE
+ enable_return_mob_phasing(parent_item)
+
+/// Continues guiding the item back toward its owner after each landing until it is caught or runs out of return hops.
+/datum/component/returning_throw_attunement/proc/on_throw_landed(datum/source, datum/thrownthing/throwingdatum)
+ SIGNAL_HANDLER
+ var/mob/living/owner = owner_ref?.resolve()
+ var/obj/item/parent_item = parent
+ if(QDELETED(parent_item))
+ qdel(src)
+ return
+
+ // Is it grabbed by someone who is not our owner and is the component set to self termiante?
+ if(!istype(owner))
+ if(self_terminate)
+ qdel(src)
+ return
+
+ // Is it not in our owner's hands yet?
+ if(throwingdatum?.get_thrower() != owner)
+ clear_throw_effect()
+ is_returning = FALSE
+ remaining_return_hops = 0
+ reset_return_trip_state()
+ return
+
+ // Tries repeated hop behavior. This is what largely handles the return hopping behavior
+ queue_return_resolution(owner, parent_item, throwingdatum?.speed || parent_item.throw_speed)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/riftwalker/_riftwalker_datum.dm
similarity index 98%
rename from modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm
rename to modular_doppler/modular_powers/code/powers/resonant/imbued/riftwalker/_riftwalker_datum.dm
index e07f83416f735d..48fa22b8f9e137 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/riftwalker/_riftwalker_datum.dm
@@ -162,7 +162,7 @@ GLOBAL_DATUM_INIT(riftwalker_network, /datum/riftwalker_network_tracker, new)
/// 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)
+ return HAS_TRAIT(user, TRAIT_IMBUED_RIFTWALKER)
// Teleport logic.
/obj/effect/riftwalker_rift/attack_hand(mob/living/user, list/modifiers)
@@ -242,7 +242,7 @@ GLOBAL_DATUM_INIT(riftwalker_network, /datum/riftwalker_network_tracker, new)
/datum/atom_hud/alternate_appearance/basic/riftwalker/mobShouldSee(mob/viewer)
if(!isliving(viewer))
return FALSE
- return HAS_TRAIT(viewer, TRAIT_ABERRANT_RIFTWALKER)
+ return HAS_TRAIT(viewer, TRAIT_IMBUED_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/imbued/riftwalker/riftwalker.dm
similarity index 55%
rename from modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/riftwalker.dm
rename to modular_doppler/modular_powers/code/powers/resonant/imbued/riftwalker/riftwalker.dm
index d994b2e499d66a..7760b40b817cd0 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/riftwalker.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/riftwalker/riftwalker.dm
@@ -1,17 +1,26 @@
/*
You can walk through persistent rifts.
*/
-/datum/power/aberrant/riftwalker
+/datum/power/imbued/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 with, 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
+ mob_trait = TRAIT_IMBUED_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)
+ required_powers = list(/datum/power/imbued_root/anomalous)
+
+ menu_icon = 'icons/effects/effects.dmi'
+ menu_icon_state = "bluestream"
// need the mob to be instantiated to generate rifts safely.
-/datum/power/aberrant/riftwalker/post_add(client/client_source)
+/datum/power/imbued/riftwalker/post_add(client/client_source)
..()
GLOB.riftwalker_network.generate_rifts()
+
+ // Refresh existing Riftwalker alternate appearances for this holder. Fixes a bug where sometimes players were spawning in with rifts not visible.
+ for(var/datum/atom_hud/alternate_appearance/rift_hud as anything in GLOB.active_alternate_appearances)
+ if(!istype(rift_hud, /datum/atom_hud/alternate_appearance/basic/riftwalker))
+ continue
+ rift_hud.check_hud(power_holder)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm b/modular_doppler/modular_powers/code/powers/resonant/imbued/summonable.dm
similarity index 97%
rename from modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm
rename to modular_doppler/modular_powers/code/powers/resonant/imbued/summonable.dm
index c96dbb12b39213..46baf07182630b 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/imbued/summonable.dm
@@ -1,21 +1,24 @@
/*
You can be summoned by speaking a specific keywords.
*/
-/datum/power/aberrant/summonable
+/datum/power/imbued/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 successfully 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
+ magic_flags = POWER_MAGIC_STANDARD
+ required_powers = list(/datum/power/imbued_root/enchanted)
- required_powers = list(/datum/power/aberrant_root/anomalous)
+ menu_icon = 'icons/effects/eldritch.dmi'
+ menu_icon_state = "realitycrack"
/// 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()
+/datum/power/imbued/summonable/get_security_record_text()
var/keyword = summon_component?.keyword
if(!keyword)
keyword = power_holder?.client?.prefs?.read_preference(/datum/preference/text/summonable_keyword)
@@ -25,7 +28,7 @@
return "Subject is summonable via keyword \"[keyword]\"."
// Adds the custom beetlejuice component and sets the beetlejuiec word.
-/datum/power/aberrant/summonable/post_add()
+/datum/power/imbued/summonable/post_add()
if(!power_holder)
return
@@ -47,7 +50,7 @@
. = ..()
-/datum/power/aberrant/summonable/remove()
+/datum/power/imbued/summonable/remove()
. = ..()
if(summon_component)
QDEL_NULL(summon_component)
@@ -319,7 +322,7 @@
return
/datum/power_constant_data/summonable
- associated_typepath = /datum/power/aberrant/summonable
+ associated_typepath = /datum/power/imbued/summonable
customization_options = list(/datum/preference/text/summonable_keyword, /datum/preference/color/summonable_rune_color)
// Orbiting rune for Summonable arrival.
diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm
index dfa06a543b4a86..7992f9d91b5cb7 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm
@@ -7,6 +7,8 @@ Reduces stress for psykers and restores Energy for cultivators
desc = "Restores the full potential of your resonant powers."
button_icon = 'icons/mob/actions/actions_spells.dmi'
button_icon_state = "chuuni"
+ background_icon_state = "bg_irregular"
+ overlay_icon_state = "bg_irregular_border"
/// Both Cultivator and Psyker can benefit from meditate.
var/psyker_spotlight_color = POWER_COLOR_PSYKER
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
index 73dbcf6a407d4a..6c76a97fca18d5 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_action.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_action.dm
@@ -1,7 +1,7 @@
/datum/action/cooldown/power/psyker
name = "abstract psyker power action - ahelp this"
- background_icon_state = "bg_hive"
- overlay_icon_state = "bg_hive_border"
+ background_icon_state = "bg_psyker"
+ overlay_icon_state = "bg_psyker_border"
button_icon = 'icons/mob/actions/backgrounds.dmi'
// We're a psychic we don't need hands.
@@ -10,9 +10,6 @@
/// 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
@@ -34,6 +31,10 @@
/datum/action/cooldown/power/psyker/proc/modify_stress(amount, override_cap)
psyker_organ.modify_stress(amount, override_cap)
+/// Whether this psyker effect should be treated as mind-affecting for target validation.
+/datum/action/cooldown/power/psyker/proc/is_mental_effect()
+ return !!(magic_resistance_types & MAGIC_RESISTANCE_MIND)
+
// 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/can_use(mob/living/user, mob/living/target)
if(!ValidateOrgan())
@@ -42,25 +43,24 @@
else
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))
+ // Dumb targets are a psyker-specific exception for mind-affecting effects.
+ if(isliving(target) && is_mental_effect() && HAS_TRAIT(target, TRAIT_DUMB))
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.
+/// Checks if the target can be affected by mental based psyker stuff, since it has its own litle list of unique immunities including TRAIT_DUMB.
+/// 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))
+ if(is_magical() && target.can_block_resonance(charge_cost))
return FALSE
- if(target.can_block_resonance(charge_cost))
+ if(magic_resistance_types && target.can_block_magic(magic_resistance_types, charge_cost = charge_cost))
return FALSE
- if(HAS_TRAIT(target, TRAIT_DUMB)) // this is a feature
+ if((magic_resistance_types & MAGIC_RESISTANCE_MIND) && HAS_TRAIT(target, TRAIT_DUMB)) // this is a feature
return FALSE
return TRUE
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_datum.dm
new file mode 100644
index 00000000000000..f0915f3d3849f9
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_datum.dm
@@ -0,0 +1,10 @@
+/datum/power_path/psyker
+ path_key = "psyker"
+ power_path = POWER_PATH_PSYKER
+ archetype_type = /datum/power_archetype/resonant
+ path_sort_order = POWER_PATH_SORT_PRIMARY
+ display_name = "Psyker"
+ icon_asset_name = "psykericon.png"
+ mechanics_text = "Your special mechanic is called 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.\nUsing 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."
+ overview_text = "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."
+ theme_color = POWER_COLOR_PSYKER
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
index 8ab03775baa747..8720aa667a876f 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_power.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_power.dm
@@ -8,5 +8,6 @@
archetype = POWER_ARCHETYPE_RESONANT
path = POWER_PATH_PSYKER
priority = POWER_PRIORITY_BASIC
+ magic_flags = POWER_MAGIC_STANDARD | POWER_MAGIC_MENTAL
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
index 629dcfbe45a184..aa08e87576b989 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_root.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_root.dm
@@ -8,6 +8,7 @@
value = 0 // all roots should be free unless they are stronger than the defaults
power_flags = POWER_HUMAN_ONLY
+ magic_flags = POWER_MAGIC_STANDARD
archetype = POWER_ARCHETYPE_RESONANT
path = POWER_PATH_PSYKER
priority = POWER_PRIORITY_ROOT
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/deflect.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/deflect.dm
index 6a3f93eab32105..b685b3807f5bd0 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/deflect.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/deflect.dm
@@ -1,8 +1,8 @@
/datum/power/psyker_power/deflect
name = "Deflect"
- desc = "Deflects projectiles that strike you, flinging them away from you and preventing harm. These projectiles are then flung towards your current cursor position. Has an incredibly high upkeep, every projectile deflected \
+ desc = "Deflects projectiles that strike you, flinging them away from you and preventing harm. These projectiles are then flung towards your current cursor position. Has a high upkeep, every projectile deflected \
causes stress equal to the projectile's damage + 10 , and ends prematurely if you suffer a catastrophic stress event.\
- \nCauses stamina damage equal to half the stress generated!"
+ \nCauses stamina damage equal to a third of of the stress generated!"
security_record_text = "Subject can deflect projectiles away from themselves and towards new targets."
security_threat = POWER_THREAT_MAJOR
value = 8
@@ -11,21 +11,23 @@
/datum/action/cooldown/power/psyker/deflect
name = "Deflect"
- desc = "Deflects projectiles that strike you, flinging them away from you and preventing harm. These projectiles are then flung towards your current cursor position. Has an incredibly high upkeep, every projectile deflected \
+ desc = "Deflects projectiles that strike you, flinging them away from you and preventing harm. These projectiles are then flung towards your current cursor position. Has a high upkeep, every projectile deflected \
causes stress equal to the projectile's damage + 10, and ends prematurely if you suffer a catastrophic stress event.\
- \nCauses stamina damage equal to half the stress generated!"
+ \nCauses stamina damage equal to a third of the stress generated!"
button_icon = 'icons/mob/actions/actions_elites.dmi'
button_icon_state = "singular_shot"
- cooldown_time = 4 SECONDS
+ cooldown_time = 50
/// Forced cooldown when the effect is dispelled.
var/dispel_cooldown_time = 15 SECONDS
/// Per-second upkeep while active.
- var/stress_per_second = 10
+ var/stress_per_second = 5
/// Flat stress added on top of projectile damage when we successfully try to deflect it.
var/projectile_stress_bonus = 10
- /// How much stress is also dealt as stamina damage? Multaplicative number.
- var/stress_as_stam_damage = 0.5
+ /// How much stress is also dealt as stamina damage? Multiplicative number.
+ var/stress_as_stam_damage = 0.33
+ /// If our power is able to deflect magic
+ var/can_deflect_magic = FALSE
/// The status effect on the caster.
var/datum/status_effect/power/deflect/active_effect
@@ -51,6 +53,10 @@
build_all_button_icons(UPDATE_BUTTON_STATUS)
return TRUE
+/datum/action/cooldown/power/psyker/deflect/on_action_success(mob/living/user, atom/target)
+ . = ..()
+ no_cooldown_on_use = active_effect ? TRUE : FALSE // quick and dirty way to prevent it from going on cooldown when enabling, given it has an upkeep
+
/datum/action/cooldown/power/psyker/deflect/proc/force_dispel_cooldown()
StartCooldownSelf(dispel_cooldown_time)
build_all_button_icons(UPDATE_BUTTON_STATUS)
@@ -68,6 +74,10 @@
var/projectile_stress_bonus
/// How much stress is also dealt as stamina damage? Multaplicative number
var/stress_as_stam_damage
+ /// If we can deflect magic
+ var/can_deflect_magic
+ /// If our power stops working when we're incapacitated.
+ var/disabled_by_incapacitate
/// Reference to the deflect action.
var/datum/action/cooldown/power/psyker/deflect/source_action
/// Tracks whether removal was caused by a dispel so we can force cooldown exactly once.
@@ -91,6 +101,8 @@
stress_per_second = source_action.stress_per_second
projectile_stress_bonus = source_action.projectile_stress_bonus
stress_as_stam_damage = source_action.stress_as_stam_damage
+ can_deflect_magic = source_action.can_deflect_magic
+ disabled_by_incapacitate = source_action.disabled_by_incapacitate
/// Applies cursor tracking and the overlay bubble.
/datum/status_effect/power/deflect/on_apply()
@@ -144,7 +156,7 @@
if(!source_action?.ValidateOrgan())
qdel(src)
return NONE
- if(source.stat != CONSCIOUS || HAS_TRAIT(source, TRAIT_INCAPACITATED))
+ if(disabled_by_incapacitate && (source.stat != CONSCIOUS || HAS_TRAIT(source, TRAIT_INCAPACITATED)))
return NONE
if(!isturf(source.loc))
return NONE
@@ -176,8 +188,8 @@
qdel(src)
return NONE
- // You do not get to redirect the projectile but you do prevent it from hitting you
- if(istype(hitting_projectile, /obj/projectile/magic) || istype(hitting_projectile, /obj/projectile/beam/instakill))
+ // If you can't deflect magic, you do not get to redirect the projectile but you do prevent it from hitting you
+ if(!can_deflect_magic && (istype(hitting_projectile, /obj/projectile/magic) || istype(hitting_projectile, /obj/projectile/beam/instakill)))
to_chat(source, span_userdanger("Your psychic strength is not strong enough to steer this magic!"))
was_dispelled = TRUE
qdel(src)
@@ -249,7 +261,7 @@
qdel(src)
return
// ends prematurely if incapacitated
- if(owner.stat != CONSCIOUS || HAS_TRAIT(owner, TRAIT_INCAPACITATED))
+ if(disabled_by_incapacitate && (owner.stat != CONSCIOUS || HAS_TRAIT(owner, TRAIT_INCAPACITATED)))
qdel(src)
return
source_action.modify_stress(stress_per_second * seconds_between_ticks)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm
index 1d0719da317878..a64a42ba395fad 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm
@@ -3,6 +3,7 @@
desc = "Grants the ability to levitate yourself above surfaces and lets you propel yourself in zero-gravity. Passively causes stress while in use."
security_record_text = "Subject can levitate their body regardless of the current gravity."
value = 4
+ magic_flags = POWER_MAGIC_STANDARD
required_powers = list(/datum/power/psyker_root)
action_path = /datum/action/cooldown/power/psyker/levitate
@@ -12,8 +13,6 @@
button_icon = 'icons/mob/actions/actions_minor_antag.dmi'
button_icon_state = "beam_up"
- mental = FALSE
-
/datum/action/cooldown/power/psyker/levitate/use_action()
. = ..()
if(!active)
@@ -50,12 +49,12 @@
// Dispel function; basically off-switch and possibly comedic faceplant
/datum/action/cooldown/power/psyker/levitate/Grant(mob/granted_to)
. = ..()
- if(resonant)
+ if(is_magical())
RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
/datum/action/cooldown/power/psyker/levitate/Remove(mob/removed_from)
. = ..()
- if(resonant)
+ if(is_magical())
UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
/// Ends the effect; makes them splat if they can't catch themselves.
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm
index 368b56ede271c2..6791511e614e51 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm
@@ -13,9 +13,11 @@
security_record_text = "Subject can psychically interact with objects from a distance."
security_threat = POWER_THREAT_MAJOR
value = 2
+ magic_flags = POWER_MAGIC_STANDARD
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.
+ required_allow_subtypes = FALSE
// 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)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm
index e191a90f850b29..fd63684424ec0d 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm
@@ -7,6 +7,8 @@
\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 = 1
+ menu_icon = 'icons/hud/actions.dmi'
+ menu_icon_state = "language_menu" // all the speechbubles are weird sizes aaaaa, I need a better icon
/// Trakcs the component
var/datum/component/beetlejuice/premonition/premonition_component
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
index 20ebfe1b60e84b..d2d609bec394df 100644
--- 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
@@ -9,6 +9,8 @@
\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
+ menu_icon = 'modular_doppler/modular_powers/icons/items/organs.dmi'
+ menu_icon_state = "chemotropic"
/obj/item/organ/resonant/psyker/chemotropic
name = "chemotropic gland"
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
index 73c5dd420f6014..4330efbd3e80ae 100644
--- 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
@@ -7,6 +7,10 @@
\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
+ magic_flags = POWER_MAGIC_STANDARD | POWER_MAGIC_MENTAL
+
+ menu_icon = 'modular_doppler/modular_powers/icons/items/organs.dmi'
+ menu_icon_state = "paracausal"
/obj/item/organ/resonant/psyker/paracausal
name = "paracausal gland"
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm
index 3481b44e4e1f66..4f53857f0aeb98 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm
@@ -9,6 +9,7 @@
Passively builds up stress, with extended use causing escalating amounts of stress. The target sometimes gets premonitions to indicate they are being watched."
security_record_text = "Subject can psychically observe people's locations based on blood samples from extreme distances."
value = 10
+ magic_flags = POWER_MAGIC_STANDARD | POWER_MAGIC_MENTAL | POWER_MAGIC_SCRYING
action_path = /datum/action/cooldown/power/psyker/scrying
/datum/action/cooldown/power/psyker/scrying
@@ -85,12 +86,12 @@
// Dispel signalers
/datum/action/cooldown/power/psyker/scrying/Grant(mob/granted_to)
. = ..()
- if(resonant)
+ if(is_magical())
RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
/datum/action/cooldown/power/psyker/scrying/Remove(mob/removed_from)
. = ..()
- if(resonant)
+ if(is_magical())
UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
end_scrying()
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm
index 7fc7ac25cb3f88..00b7af1ed1816b 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm
@@ -13,6 +13,7 @@
security_record_text = "Subject can wield telekinesis to maneuver and fling objects."
security_threat = POWER_THREAT_MAJOR
value = 5
+ magic_flags = POWER_MAGIC_STANDARD
required_powers = list(/datum/power/psyker_root)
action_path = /datum/action/cooldown/power/psyker/telekinesis
@@ -26,11 +27,14 @@
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
+ /// Specific target types we never want telekinesis to manipulate.
+ var/static/list/grab_blacklist = typecacheof(list(
+ /obj/vehicle/sealed/mecha,
+ ))
+
/// Stat required for us to grab a mob.
var/stat_required = DEAD
@@ -113,12 +117,12 @@
/datum/action/cooldown/power/psyker/telekinesis/Grant(mob/granted_to)
. = ..()
- if(resonant)
+ if(is_magical())
RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
/datum/action/cooldown/power/psyker/telekinesis/Remove(mob/removed_from)
. = ..()
- if(resonant)
+ if(is_magical())
UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
/// Calculates the stres cost of vairous interactions.
@@ -231,6 +235,8 @@
return FALSE
if(iseffect(target))
return FALSE
+ if(is_type_in_typecache(target, grab_blacklist))
+ return FALSE
var/atom/movable/movable_target = target
if(movable_target.anchored)
diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm
index 6a56f9aa0a66c3..3a45fccab78036 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm
@@ -105,7 +105,7 @@
for(var/mob/living/target in view(user))
if(target == user)
continue
- if(mental && !can_affect_mental(target))
+ if(is_mental_effect() && !can_affect_mental(target))
continue
targets += target
@@ -143,7 +143,7 @@
for(var/mob/living/target in view(user))
if(target == user)
continue
- if(mental && !can_affect_mental(target))
+ if(is_mental_effect() && !can_affect_mental(target))
continue
targets += target
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
index 69db8a9bf1f655..838bcaa85c0daa 100644
--- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm
+++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm
@@ -4,6 +4,7 @@
security_record_text = "Subject can initiate one-way communication with all visible targets."
value = 1
required_powers = list(/datum/power/psyker_power/telepathy)
+ required_allow_subtypes = FALSE
/datum/power/psyker_power/telepathy_area/post_add()
. = ..()
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_datum.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_datum.dm
new file mode 100644
index 00000000000000..dc1ff23fc2bf2c
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_datum.dm
@@ -0,0 +1,11 @@
+/datum/power_path/enigmatist
+ path_key = "enigmatist"
+ power_path = POWER_PATH_ENIGMATIST
+ archetype_type = /datum/power_archetype/sorcerous
+ path_sort_order = POWER_PATH_SORT_SECONDARY
+ display_name = "Enigmatist"
+ icon_asset_name = "enigmatisticon.png"
+ is_available = FALSE
+ mechanics_text = "POWERS UNREACHABLE BY YOUR WEAK HANDS, NUANCE UNCOMPREHENDABLE BY YOUR FEEBLE MIND, HORRORS THAT SOON WILL BE UNDERSTOOD BY YOUR FRAIL BODY"
+ overview_text = "INTERLOPER, THIS IS NOT YOUR REALM TO ENTER. NOT YET!"
+ theme_color = POWER_COLOR_ENIGMATIST
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
index c0cb3a9fca7541..a8063c9aac8b38 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm
@@ -1,7 +1,7 @@
/datum/action/cooldown/power/thaumaturge
name = "abstract thaumaturge power action - ahelp this"
- background_icon_state = "bg_star"
- overlay_icon_state = "bg_default_border"
+ background_icon_state = "bg_thaumaturge"
+ overlay_icon_state = "bg_thaumaturge_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.
@@ -56,8 +56,15 @@
return FALSE
return TRUE
+/datum/action/cooldown/power/thaumaturge/sync_magic_resistance_types_from_power()
+ . = ..()
+ ValidateThaumaturgeComponent()
+ magic_resistance_types |= thaumaturge_component?.additional_magic_resistance_flags || NONE
+ return magic_resistance_types
+
/datum/action/cooldown/power/thaumaturge/try_use(mob/living/user, atom/target)
ValidateThaumaturgeComponent()
+ sync_magic_resistance_types_from_power()
if(!check_if_valid(user))
return FALSE
if(ishuman(user)) // We're not checking for clothes on cats
@@ -65,8 +72,6 @@
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.
@@ -214,4 +219,3 @@
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
index 75cc56428e334e..1051ff6d25195f 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_component.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_component.dm
@@ -16,27 +16,79 @@
var/resource_display_multiplier = 1
/// Extra flags applied to thaumaturge action anti-magic checks while this component exists.
var/additional_magic_resistance_flags = NONE
+ /// Optional action button background override applied to owned thaumaturge powers.
+ var/action_background_icon_state_override
/datum/component/thaumaturge/Initialize(mob/living/new_attached_mob)
. = ..()
if(!isliving(parent))
return COMPONENT_INCOMPATIBLE
attached_mob = new_attached_mob || parent
+ RegisterSignal(attached_mob, COMSIG_MOB_GRANTED_ACTION, PROC_REF(on_owner_action_granted))
apply_action_magic_flags(TRUE)
+ apply_action_background_override(TRUE)
/datum/component/thaumaturge/Destroy(force)
+ if(attached_mob)
+ UnregisterSignal(attached_mob, COMSIG_MOB_GRANTED_ACTION)
apply_action_magic_flags(FALSE)
+ apply_action_background_override(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
+ var/additional_power_magic_flags = convert_magic_resistance_flags_to_power_flags(additional_magic_resistance_flags)
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
+ var/datum/power/origin_power = thaumaturge_action.origin_power
+ if(!origin_power || !additional_power_magic_flags)
+ continue
+ var/base_power_magic_flags = initial(origin_power.magic_flags)
+ origin_power.magic_flags = add_flags ? (base_power_magic_flags | additional_power_magic_flags) : base_power_magic_flags
+ thaumaturge_action.sync_magic_resistance_types_from_power()
+
+/// Applies an optional action button background override to owned thaumaturge powers.
+/datum/component/thaumaturge/proc/apply_action_background_override(apply_override = TRUE)
+ if(!attached_mob || !action_background_icon_state_override)
+ 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
+ thaumaturge_action.background_icon_state = apply_override ? action_background_icon_state_override : initial(thaumaturge_action.background_icon_state)
+ thaumaturge_action.build_all_button_icons(UPDATE_BUTTON_BACKGROUND)
+
+/// Applies current component-based action overrides to newly granted thaumaturge actions so that they inherit component behavior as well.
+/datum/component/thaumaturge/proc/on_owner_action_granted(mob/living/source, datum/action/granted_action)
+ SIGNAL_HANDLER
+
+ var/datum/action/cooldown/power/thaumaturge/thaumaturge_action = granted_action
+ if(!istype(thaumaturge_action))
+ return
+
+ if(additional_magic_resistance_flags)
+ var/additional_power_magic_flags = convert_magic_resistance_flags_to_power_flags(additional_magic_resistance_flags)
+ var/datum/power/origin_power = thaumaturge_action.origin_power
+ if(origin_power && additional_power_magic_flags)
+ var/base_power_magic_flags = initial(origin_power.magic_flags)
+ origin_power.magic_flags = base_power_magic_flags | additional_power_magic_flags
+ thaumaturge_action.sync_magic_resistance_types_from_power()
+ if(action_background_icon_state_override)
+ thaumaturge_action.background_icon_state = action_background_icon_state_override
+ thaumaturge_action.build_all_button_icons(UPDATE_BUTTON_BACKGROUND)
+/// Converts action anti-magic resistance flags into preference-menu power magic tags.
+/datum/component/thaumaturge/proc/convert_magic_resistance_flags_to_power_flags(magic_resistance_flags)
+ var/power_magic_flags = NONE
+ if(magic_resistance_flags & MAGIC_RESISTANCE_HOLY)
+ power_magic_flags |= POWER_MAGIC_UNHOLY
+ if(magic_resistance_flags & MAGIC_RESISTANCE_MIND)
+ power_magic_flags |= POWER_MAGIC_MENTAL
+ if(magic_resistance_flags & MAGIC_RESISTANCE)
+ power_magic_flags |= POWER_MAGIC_STANDARD
+ return power_magic_flags
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_datum.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_datum.dm
new file mode 100644
index 00000000000000..f6b8ede28beb92
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_datum.dm
@@ -0,0 +1,10 @@
+/datum/power_path/thaumaturge
+ path_key = "thaumaturge"
+ power_path = POWER_PATH_THAUMATURGE
+ archetype_type = /datum/power_archetype/sorcerous
+ path_sort_order = POWER_PATH_SORT_PRIMARY
+ display_name = "Thaumaturge"
+ icon_asset_name = "thaumaturgeicon.png"
+ mechanics_text = "Thaumaturgy has two core components; Spell Preparation, 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 Preparation 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\nFurthermore, 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."
+ overview_text = "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."
+ theme_color = POWER_COLOR_THAUMATURGE
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
index e3e8b7afd87e19..f903d9afcbf4f9 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_hemomancy.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_hemomancy.dm
@@ -7,6 +7,7 @@
resource_display_mode = THAUMATURGE_RESOURCE_DISPLAY_PREP_COST
additional_magic_resistance_flags = MAGIC_RESISTANCE_HOLY
resource_display_multiplier = THAUMATURGE_HEMOMANCY_BLOOD_COST_MULTIPLIER
+ action_background_icon_state_override = "bg_thaumaturge_hemomancy"
/// HUD element that shows current blood amount.
var/atom/movable/screen/hemophage/blood/thaumaturge/blood_tracker
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
index 103e2383c35a3e..22c5c705629232 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_power.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_power.dm
@@ -6,4 +6,5 @@
archetype = POWER_ARCHETYPE_SORCEROUS
path = POWER_PATH_THAUMATURGE
priority = POWER_PRIORITY_BASIC
+ magic_flags = POWER_MAGIC_STANDARD
abstract_parent_type = /datum/power/thaumaturge
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
index 2221a7cd965c77..09e8ecbc1a037f 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm
@@ -8,3 +8,4 @@
archetype = POWER_ARCHETYPE_SORCEROUS
path = POWER_PATH_THAUMATURGE
priority = POWER_PRIORITY_ROOT
+ magic_flags = POWER_MAGIC_STANDARD
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
index c7b4f8eec19fb2..e7dfa5f6b0310b 100644
--- 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
@@ -203,6 +203,8 @@ A lot of Affinity asignments are vibe-based depending on looks, visibility and r
affinity = 4
/obj/item/clothing/suit/wizrobe/tape/fake
affinity = 4
+/obj/item/clothing/suit/wizrobe/durathread
+ affinity = 4
// Wizrobe hats (Fakes)
/obj/item/clothing/head/wizard/fake
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
index ccabd7c4f0f578..a946ef5958fc62 100644
--- 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
@@ -11,7 +11,7 @@
cold_protection = CHEST|GROIN|ARMS|HANDS|LEGS
heat_protection = CHEST|GROIN|ARMS|HANDS|LEGS
fishing_modifier = -6 // high vishing
- affinity = 3
+ affinity = 4
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')
@@ -25,7 +25,7 @@
icon_state = "hivizhat"
armor_type = /datum/armor/none
fishing_modifier = -5 // high vishing
- affinity = 3
+ affinity = 4
// 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
@@ -56,4 +56,4 @@
armor_type = /datum/armor/none
fishing_modifier = -2
resistance_flags = FLAMMABLE
- affinity = 3
+ affinity = 4
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
index ed25675701ab52..310f2b1c0e7f3e 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm
@@ -45,6 +45,10 @@
// Tweak as needed.
var/knockback_range = 3
+ /// Hidden or special movables that should never be dragged by wind effects.
+ var/static/list/wind_drag_blacklist = typecacheof(list(
+ /atom/movable/mirage_holder, // movable object that creates the illusion. surprised it is an atom/movable.
+ ))
// Code for dragging along objects.
/obj/projectile/resonant/gale_blast/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
@@ -95,7 +99,11 @@
if(!movable_instance)
return FALSE
- // Core rule: anchored objects do not move
+ // Skips anything in the blacklist.
+ if(is_type_in_typecache(movable_instance, wind_drag_blacklist))
+ return FALSE
+
+ // Anchored objects do not move
if(movable_instance.anchored)
return FALSE
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/hemomancy.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/hemomancy.dm
index 2ffa4671ffd41e..c20670ef587862 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/hemomancy.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/hemomancy.dm
@@ -14,6 +14,7 @@
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
+ magic_flags = POWER_MAGIC_STANDARD | POWER_MAGIC_UNHOLY
/datum/power/thaumaturge_root/hemomancy/post_add()
if(!power_holder) // So it doesn't runtime at init
@@ -36,9 +37,10 @@
button_icon_state = "manip"
max_charges = null
cooldown_time = 15
-
prep_cost = 0
+ background_icon_state = "bg_thaumaturge_hemomancy"
+
/datum/action/cooldown/power/thaumaturge/channel_blood/Grant(mob/granted_to)
. = ..()
RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
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
index d800fe8f9b6f66..33665095d7240d 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm
@@ -281,12 +281,12 @@
// Dispel functionality
/datum/action/cooldown/power/thaumaturge/magical_barrage/Grant(mob/granted_to)
. = ..()
- if(resonant)
+ if(is_magical())
RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel))
/datum/action/cooldown/power/thaumaturge/magical_barrage/Remove(mob/removed_from)
. = ..()
- if(resonant)
+ if(is_magical())
UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL)
/// On dispel, poof there go your orbitals.
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm
index 7dc57d3ac96b2c..2b8ef0d7ef00b4 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm
@@ -29,6 +29,7 @@
click_to_activate = TRUE
target_range = 1
aim_assist = FALSE // complex targeting
+ unset_after_click = FALSE
/// Currently selected prestidigitation mode.
var/selected_mode = PRESTI_SUMMON_SPARKS
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
index 76b311a3abae12..534b606a2a34a8 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_action.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_action.dm
@@ -1,8 +1,7 @@
/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'
+ background_icon_state = "bg_theologist"
+ overlay_icon_state = "bg_theologist_border"
/// The component that handles most piety components.
var/datum/component/theologist_piety/piety_component
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_datum.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_datum.dm
new file mode 100644
index 00000000000000..cfc158af5f969c
--- /dev/null
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_datum.dm
@@ -0,0 +1,10 @@
+/datum/power_path/theologist
+ path_key = "theologist"
+ power_path = POWER_PATH_THEOLOGIST
+ archetype_type = /datum/power_archetype/sorcerous
+ path_sort_order = POWER_PATH_SORT_TERTIARY
+ display_name = "Theologist"
+ icon_asset_name = "theologisticon.png"
+ mechanics_text = "Theologists 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."
+ overview_text = "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."
+ theme_color = POWER_COLOR_THEOLOGIST
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
index 0c0a292ddcf2a3..fa0a9f6956622f 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_piety.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_piety.dm
@@ -71,9 +71,7 @@
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
+ update_screen_loc()
hud_used.infodisplay += theologist_ui
// Set initial text so it isn't blank until first adjust.
@@ -81,6 +79,13 @@
hud_used.show_hud(hud_used.hud_version)
+/// Refreshes where the piety HUD should sit based on whether cultivator energy is also present.
+/datum/component/theologist_piety/proc/update_screen_loc()
+ if(!theologist_ui || !attached_mob)
+ return
+
+ theologist_ui.screen_loc = attached_mob.GetComponent(/datum/component/cultivator_energy) ? THEOLOGIST_ALT_UI_SCREEN_LOC : THEOLOGIST_UI_SCREEN_LOC
+
/// Handler for adjusting piety.
/datum/component/theologist_piety/proc/adjust_piety(amount, override_cap)
if(!isnum(amount))
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
index e2a00c4c07ad3d..08408880a5a3cf 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_power.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_power.dm
@@ -6,4 +6,5 @@
archetype = POWER_ARCHETYPE_SORCEROUS
path = POWER_PATH_THEOLOGIST
priority = POWER_PRIORITY_BASIC
+ magic_flags = POWER_MAGIC_STANDARD
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
index d7ec038114240a..093c5d3c6fe70c 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm
@@ -4,7 +4,7 @@
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
-
+ magic_flags = POWER_MAGIC_STANDARD
archetype = POWER_ARCHETYPE_SORCEROUS
path = POWER_PATH_THEOLOGIST
priority = POWER_PRIORITY_ROOT
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
index f7a3daa4334e33..de7e94e429fd63 100644
--- 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
@@ -4,3 +4,5 @@
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
+ menu_icon = 'icons/mob/actions/actions_spells.dmi'
+ menu_icon_state = "ash"
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/culling.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/culling.dm
index 7690890427206a..48df58607f973d 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/culling.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/culling.dm
@@ -8,6 +8,8 @@
value = 2
required_powers = list(/datum/power/theologist_root)
required_allow_subtypes = TRUE
+ menu_icon = 'icons/mob/actions/actions_elites.dmi'
+ menu_icon_state = "bonfire_teleport"
/// Reference to the owner's piety component
var/datum/component/theologist_piety/piety_component
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
index b8178a1ec0405f..0afb8c673fe587 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/divine_protection.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/divine_protection.dm
@@ -8,6 +8,8 @@
security_record_text = "Subject tends to unpredictably and miraculously avoid harm."
security_threat = POWER_THREAT_MAJOR
value = 4
+ menu_icon = 'icons/effects/effects.dmi'
+ menu_icon_state = "shield-yellow"
required_powers = list(/datum/power/theologist_root/)
required_allow_subtypes = TRUE
diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/flagellant.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/flagellant.dm
index b621fb8654f441..55cff414ed543a 100644
--- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/flagellant.dm
+++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/flagellant.dm
@@ -7,6 +7,8 @@
value = 4
required_powers = list(/datum/power/theologist_root)
required_allow_subtypes = TRUE
+ menu_icon = 'icons/obj/weapons/whip.dmi'
+ menu_icon_state = "whip"
/// Reference to the holder's piety component.
var/datum/component/theologist_piety/piety_component
diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm
index 395fa7b7e6f9b4..b3b6047375b3ad 100644
--- a/modular_doppler/modular_powers/code/powers_action.dm
+++ b/modular_doppler/modular_powers/code/powers_action.dm
@@ -7,11 +7,12 @@
*/
/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'
+ background_icon_state = "bg_default"
+ overlay_icon_state = "bg_default_border"
active_overlay_icon_state = "bg_spell_border_active_red"
ranged_mousepointer = 'icons/effects/mouse_pointers/weapon_pointer.dmi'
+ background_icon = 'modular_doppler/modular_powers/icons/powers/backgrounds.dmi'
+ overlay_icon = 'modular_doppler/modular_powers/icons/powers/backgrounds.dmi'
/// Maximum state of consciousness before the ability is blocked.
/// For example, `UNCONSCIOUS` prevents it from being used when in hard crit or dead,
@@ -19,18 +20,20 @@
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
+ var/datum/power/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
+ /// Do we need to be on a turf (not inside something) to use this power?
+ var/needs_to_stand_on_turf = TRUE
+ /// Bypasses the normal specified cooldown when set to TRUE. Useful if you don't want powers to always go on cooldown.
+ var/no_cooldown_on_use
/// If set, we must wait this long before use_action executes. Cast time basically.
var/use_time = 0
@@ -47,8 +50,39 @@
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
+ /// Extra antimagic types checked on target when the action is used.
+ var/magic_resistance_types = NONE
+
+/datum/action/cooldown/power/New(datum/new_origin_power)
+ if(istype(new_origin_power, /datum/power))
+ origin_power = new_origin_power
+ else
+ origin_power = null
+ sync_magic_resistance_types_from_power()
+ return ..()
+
+/// Whether the action is treated as magical for silence and baseline antimagic checks.
+/datum/action/cooldown/power/proc/is_magical()
+ return !!magic_resistance_types
+
+/// Maps the host power's magic flags into our action's antimagic bitflag.
+/datum/action/cooldown/power/proc/sync_magic_resistance_types_from_power()
+ // If there is no power directly tied to it e.g meditate
+ if(!origin_power)
+ magic_resistance_types = NONE
+ return magic_resistance_types
+
+ var/power_magic_flags = origin_power?.magic_flags || NONE
+ magic_resistance_types = NONE
+
+ if(power_magic_flags & POWER_MAGIC_STANDARD)
+ magic_resistance_types |= MAGIC_RESISTANCE
+ if(power_magic_flags & POWER_MAGIC_MENTAL)
+ magic_resistance_types |= MAGIC_RESISTANCE_MIND
+ if(power_magic_flags & POWER_MAGIC_UNHOLY)
+ magic_resistance_types |= MAGIC_RESISTANCE_HOLY
+
+ return 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)
@@ -58,11 +92,11 @@
// 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.
+ if(is_magical() && mob_target.can_block_resonance(1)) // Resonance checks are handled by the owning power's flags.
// 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))
+ if(is_magical() && magic_resistance_types && mob_target.can_block_magic(magic_resistance_types, charge_cost = 0))
return FALSE
if(!do_use_time(user, target))
return FALSE
@@ -84,12 +118,15 @@
if(disabled_by_incapacitate && HAS_TRAIT(user, TRAIT_INCAPACITATED))
owner.balloon_alert(user, "incapacitated!")
return FALSE
- if(resonant && HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED))
+ if(is_magical() && 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(needs_to_stand_on_turf && !isturf(user.loc))
+ owner.balloon_alert(user, "occupied!")
+ 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
@@ -163,7 +200,10 @@
if(!try_use(user, target = null))
return FALSE
- StartCooldown()
+ // Support for bypassing cooldowns on use.
+ if(!no_cooldown_on_use)
+ StartCooldown()
+
return TRUE
/** Intercepts client owner clicks to activate the ability.
@@ -318,5 +358,3 @@ Projectile action code down below
/// 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_living.dm b/modular_doppler/modular_powers/code/powers_living.dm
index 7279de7cb19d8b..c944f271918e99 100644
--- a/modular_doppler/modular_powers/code/powers_living.dm
+++ b/modular_doppler/modular_powers/code/powers_living.dm
@@ -73,6 +73,20 @@
return TRUE
return FALSE
+/**
+ * Checks whether the mob has any power in a given archetype.
+ *
+ * Arguments:
+ * * power_archetype - The archetype identifier to check against, e.g. POWER_ARCHETYPE_RESONANT
+ *
+ * Returns TRUE if any owned power matches the archetype, FALSE otherwise.
+ */
+/mob/living/proc/has_power_in_archetype(power_archetype)
+ for(var/datum/power/power in powers)
+ if(power.archetype == power_archetype)
+ return TRUE
+ return FALSE
+
/**
* Getter function for a mob's power
*
diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm
index 76cd6ecba39e3c..06f80d4f1c8d88 100644
--- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm
+++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm
@@ -1,14 +1,7 @@
/**
- * 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.
+ * The curse that once haunted this land is no more.
+ * Handles most TGUI interactions, send largely constant data (except for Augmented, snowflake mechanics.) and handles a few of the powers handoffs.
*/
/datum/preference_middleware/powers
@@ -21,20 +14,29 @@
/datum/preference_middleware/powers/post_set_preference(mob/user, preference, value)
preferences.sanitize_powers()
-/datum/preference_middleware/powers/get_ui_data(mob/user)
+/datum/preference_middleware/powers/get_constant_data()
var/list/data = list()
+ var/list/power_paths = build_empty_power_path_map()
+
+ // Iterates all powers and build the power entries for all of them.
+ for(var/power_name in SSpowers.powers)
+ var/datum/power/power_type = SSpowers.powers[power_name]
+ var/path_key = get_power_path_key(power_type.path)
+ if(!path_key)
+ continue
- var/list/thaumaturge = list()
- var/list/enigmatist = list()
- var/list/theologist = list()
+ power_paths[path_key] += list(build_power_constant_entry(power_type))
- var/list/psyker = list()
- var/list/cultivator = list()
- var/list/aberrant = list()
+ data["power_paths"] = power_paths
+ data["power_path_data"] = GLOB.power_path_ui_data
+ data["power_path_archetypes"] = GLOB.power_path_archetypes
+ data["fallback_power_path_id"] = GLOB.fallback_power_path_id
+ data["total_power_points"] = MAXIMUM_POWER_POINTS
+ return data
- var/list/warfighter = list()
- var/list/expert = list()
- var/list/augmented = list()
+/datum/preference_middleware/powers/get_ui_data(mob/user)
+ var/list/data = list()
+ var/list/power_state_paths = build_empty_power_path_map()
var/current_points = 0
for(var/power_name in preferences.all_powers)
@@ -52,12 +54,6 @@
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))
@@ -67,118 +63,159 @@
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"
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
+ state = "good"
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,
+ "augment" = build_power_runtime_augment_info(power_type, preferences),
))
- 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
+ var/path_key = get_power_path_key(power_type.path)
+ if(path_key)
+ power_state_paths[path_key] += 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_state_paths"] = power_state_paths
data["power_points"] = current_points
return data
+/// Builds a map of all currently available power paths.
+/datum/preference_middleware/powers/proc/build_empty_power_path_map()
+ var/list/power_path_map = list()
+ for(var/path_key in GLOB.all_power_paths)
+ power_path_map[path_key] = list()
+ return power_path_map
+
+/// Gets the relevant key for the power path based on the given define.
+/datum/preference_middleware/powers/proc/get_power_path_key(power_path)
+ var/datum/power_path/path_data = GLOB.power_paths_by_define[power_path]
+ return path_data?.path_key
+
+/// Here for now as the sole and only exception. Irregular is the only one that gets to bypass path limit: you need VERY GOOD excuses to allow powers to do so, since it muddies up path choices.
+/// Irregular also has niche-only powers to counteract that.
+/datum/preference_middleware/powers/proc/is_path_limit_exempt(datum/power/power_type)
+ var/datum/power_path/path_data = GLOB.power_paths_by_define[power_type.path]
+ return path_data?.path_limit_exempt
+
+/// Builds a constant entry for powers to be referenced at later points.
+/datum/preference_middleware/powers/proc/build_power_constant_entry(datum/power/power_type)
+ var/root_badge_icon
+ var/archetype_name = null
+
+ if(power_type.priority == POWER_PRIORITY_ROOT)
+ root_badge_icon = "crown"
+ else
+ root_badge_icon = ""
+ archetype_name = power_type.archetype
+
+ var/datum/power_constant_data/constant_data = GLOB.all_power_constant_data[power_type]
+ var/list/customization_options = constant_data?.get_customization_data()
+ var/action_icon = null
+ var/action_icon_state = null
+
+ // Sets the icon to be the menu icon.
+ if(power_type.menu_icon)
+ action_icon = "[power_type.menu_icon]"
+ if(power_type.menu_icon_state)
+ action_icon_state = "[power_type.menu_icon_state]"
+
+ // If there is no menu icon set, falls back to action path icons.
+ if((isnull(action_icon) || isnull(action_icon_state)) && power_type.action_path)
+ var/initial_action_icon = initial(power_type.action_path.button_icon)
+ var/initial_action_icon_state = initial(power_type.action_path.button_icon_state)
+ if(isnull(action_icon) && initial_action_icon)
+ action_icon = "[initial_action_icon]"
+ if(isnull(action_icon_state) && initial_action_icon_state)
+ action_icon_state = "[initial_action_icon_state]"
+
+ // If it is augmented and there is no icon, fall back to yoinking the icons from the attached augment.
+ if((isnull(action_icon) || isnull(action_icon_state)) && ispath(power_type, /datum/power/augmented))
+ var/datum/power/augmented/augmented_power_type = power_type
+ var/obj/item/organ/augment_path = initial(augmented_power_type.augment)
+ if(augment_path)
+ var/initial_augment_icon = initial(augment_path.icon)
+ var/initial_augment_icon_state = initial(augment_path.icon_state)
+ if(isnull(action_icon) && initial_augment_icon)
+ action_icon = "[initial_augment_icon]"
+ if(isnull(action_icon_state) && initial_augment_icon_state)
+ action_icon_state = "[initial_augment_icon_state]"
+
+ return list(
+ "description" = power_type.desc,
+ "name" = power_type.name,
+ "cost" = power_type.value,
+ "magic_flags" = build_power_magic_flags(power_type),
+ "root_badge_icon" = root_badge_icon,
+ "archetype_name" = archetype_name,
+ "required_powers" = get_required_power_names(power_type),
+ "required_allow_any" = power_type.required_allow_any,
+ "required_allow_subtypes" = power_type.required_allow_subtypes,
+ "action_icon" = action_icon,
+ "action_icon_state" = action_icon_state,
+ "augment" = build_power_constant_augment_info(power_type),
+ "customizable" = constant_data?.is_customizable(),
+ "customization_options" = customization_options,
+ )
+
+/// Builds the list of anti-magic interaction tags the UI should show for a power.
+/datum/preference_middleware/powers/proc/build_power_magic_flags(datum/power/power_type)
+ var/power_magic_flags = initial(power_type.magic_flags)
+ var/list/final_magic_flags = list()
+ if(power_magic_flags & POWER_MAGIC_UNHOLY)
+ final_magic_flags += "unholy"
+ if(power_magic_flags & POWER_MAGIC_MENTAL)
+ final_magic_flags += "mental"
+ if(power_magic_flags & POWER_MAGIC_SCRYING)
+ final_magic_flags += "scrying"
+ if(power_magic_flags & POWER_MAGIC_STANDARD)
+ final_magic_flags += "magical"
+ return final_magic_flags
+
+/// Gets the name of any power that requires another.
+/datum/preference_middleware/powers/proc/get_required_power_names(datum/power/power_type)
+ 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
+ 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
+ return required_power_names
+
+/// Builds the constant augment info specifically for augments and their ANNOYING ARM SNOWFLAKING.
+/datum/preference_middleware/powers/proc/build_power_constant_augment_info(datum/power/power_type)
+ if(ispath(power_type, /datum/power/augmented))
+ var/datum/power/augmented/power_instance = new power_type
+ var/augment_location = power_instance.get_augment_location_label()
+ var/is_arm_augment = (augment_location == "Arms")
+ qdel(power_instance)
+ return list(
+ "location" = augment_location,
+ "is_arm" = is_arm_augment,
+ )
+ return null
+
/// 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/preference_middleware/powers/proc/build_power_runtime_augment_info(
datum/power/power_type,
datum/preferences/preferences
)
- // Snowflake code for Augments: expose arm assignment + location.
- var/augment_location
- var/is_arm_augment
+ // Snowflake code for Augments: expose only runtime arm assignment state.
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")
+ var/augment_location = power_instance.get_augment_location_label()
+ var/is_arm_augment = (augment_location == "Arms")
qdel(power_instance)
if(is_arm_augment)
var/augment_left = preferences.read_preference(/datum/preference/choiced/augment_left)
@@ -192,8 +229,6 @@
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,
@@ -223,13 +258,15 @@
return FALSE
// Make sure we don't exceed 2 distinct paths.
- if(length(preferences.all_powers))
+ if(length(preferences.all_powers) && !is_path_limit_exempt(power_type))
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
+ if(is_path_limit_exempt(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)
@@ -503,11 +540,16 @@
* 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)
+ if(is_path_limit_exempt(power_type))
+ return FALSE
+
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
+ if(is_path_limit_exempt(existing_power_type))
+ continue
unique_paths[existing_power_type.path] = TRUE
// If this power adds a third distinct path, block it.
@@ -516,9 +558,21 @@
/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'
+ "thaumaturgeicon.png" = 'modular_doppler/modular_powers/icons/ui/powers/thaumaturgeicon.png',
+ "enigmatisticon.png" = 'modular_doppler/modular_powers/icons/ui/powers/enigmatisticon.png',
+ "theologisticon.png" = 'modular_doppler/modular_powers/icons/ui/powers/theologisticon.png',
+ "psykericon.png" = 'modular_doppler/modular_powers/icons/ui/powers/psykericon.png',
+ "cultivatoricon.png" = 'modular_doppler/modular_powers/icons/ui/powers/cultivatoricon.png',
+ "aberranticon.png" = 'modular_doppler/modular_powers/icons/ui/powers/aberranticon.png',
+ "imbuedicon.png" = 'modular_doppler/modular_powers/icons/ui/powers/imbuedicon.png',
+ "warfightericon.png" = 'modular_doppler/modular_powers/icons/ui/powers/warfightericon.png',
+ "experticon.png" = 'modular_doppler/modular_powers/icons/ui/powers/experticon.png',
+ "augmentedicon.png" = 'modular_doppler/modular_powers/icons/ui/powers/augmentedicon.png',
+ "irregularicon.png" = 'modular_doppler/modular_powers/icons/ui/powers/irregularicon.png',
+ "magic_standard_icon.png" = 'modular_doppler/modular_powers/icons/ui/powers/magic_standard_icon.png',
+ "magic_mental_icon.png" = 'modular_doppler/modular_powers/icons/ui/powers/magic_mental_icon.png',
+ "magic_scrying_icon.png" = 'modular_doppler/modular_powers/icons/ui/powers/magic_scrying_icon.png',
+ "magic_unholy_icon.png" = 'modular_doppler/modular_powers/icons/ui/powers/magic_unholy_icon.png'
)
/datum/preference_middleware/powers/get_ui_assets()
diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm
index 06809eab84d3c4..50ca6e1a011782 100644
--- a/modular_doppler/modular_powers/code/powers_subsystem.dm
+++ b/modular_doppler/modular_powers/code/powers_subsystem.dm
@@ -190,8 +190,9 @@ PROCESSING_SUBSYSTEM_DEF(powers)
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))
+ // Checks if we have no more than 2 paths, unless the path datum explicitly opts out.
+ var/datum/power_path/path_data = GLOB.power_paths_by_define[power_type.path]
+ if(!path_data?.path_limit_exempt && !(power_type.path in unique_paths))
if(length(unique_paths) >= 2)
continue // Third distinct path, discard.
unique_paths[power_type.path] = TRUE
diff --git a/modular_doppler/modular_powers/code/security/reality_anchor.dm b/modular_doppler/modular_powers/code/security/reality_anchor.dm
index 23aa30b3910666..25459496eff0ff 100644
--- a/modular_doppler/modular_powers/code/security/reality_anchor.dm
+++ b/modular_doppler/modular_powers/code/security/reality_anchor.dm
@@ -120,10 +120,10 @@
/// Delegates the appropriate moodlet to the approrpiate archetype. Sorc archetype hates it, Resonant dislikes it, Mortal don't give a f.
/datum/status_effect/power/reality_anchor_silenced/proc/get_anchor_moodlet()
/// Sorc archetype
- if(owner.has_power_in_path(POWER_PATH_THAUMATURGE) || owner.has_power_in_path(POWER_PATH_THEOLOGIST))
+ if(owner.has_power_in_archetype(POWER_ARCHETYPE_SORCEROUS))
return /datum/mood_event/reality_anchor_silenced/sorcerous
/// Resonant archetype
- if(owner.has_power_in_path(POWER_PATH_PSYKER) || owner.has_power_in_path(POWER_PATH_CULTIVATOR) || owner.has_power_in_path(POWER_PATH_ABERRANT))
+ if(owner.has_power_in_archetype(POWER_ARCHETYPE_RESONANT))
return /datum/mood_event/reality_anchor_silenced/resonant
/// Mortals
return /datum/mood_event/reality_anchor_silenced/mortal
diff --git a/modular_doppler/modular_powers/icons/powers/backgrounds.dmi b/modular_doppler/modular_powers/icons/powers/backgrounds.dmi
new file mode 100644
index 00000000000000..6ab2d7307fd9cf
Binary files /dev/null and b/modular_doppler/modular_powers/icons/powers/backgrounds.dmi differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/aberranticon.png b/modular_doppler/modular_powers/icons/ui/powers/aberranticon.png
new file mode 100644
index 00000000000000..25d15ad808c295
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/aberranticon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/augmentedicon.png b/modular_doppler/modular_powers/icons/ui/powers/augmentedicon.png
new file mode 100644
index 00000000000000..0f7298a55c6c29
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/augmentedicon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/cultivatoricon.png b/modular_doppler/modular_powers/icons/ui/powers/cultivatoricon.png
new file mode 100644
index 00000000000000..67b64af2fe4fca
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/cultivatoricon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/enigmatisticon.png b/modular_doppler/modular_powers/icons/ui/powers/enigmatisticon.png
new file mode 100644
index 00000000000000..5f9325ffe49c91
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/enigmatisticon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/experticon.png b/modular_doppler/modular_powers/icons/ui/powers/experticon.png
new file mode 100644
index 00000000000000..28dfe80c266fae
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/experticon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/imbuedicon.png b/modular_doppler/modular_powers/icons/ui/powers/imbuedicon.png
new file mode 100644
index 00000000000000..8545ec5d75c653
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/imbuedicon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/irregularicon.png b/modular_doppler/modular_powers/icons/ui/powers/irregularicon.png
new file mode 100644
index 00000000000000..a553a76d11e0e1
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/irregularicon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/magic_mental_icon.png b/modular_doppler/modular_powers/icons/ui/powers/magic_mental_icon.png
new file mode 100644
index 00000000000000..de142a7bb4b9e8
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/magic_mental_icon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/magic_scrying_icon.png b/modular_doppler/modular_powers/icons/ui/powers/magic_scrying_icon.png
new file mode 100644
index 00000000000000..526ff7546591b0
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/magic_scrying_icon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/magic_standard_icon.png b/modular_doppler/modular_powers/icons/ui/powers/magic_standard_icon.png
new file mode 100644
index 00000000000000..3f5f1f3fa506b3
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/magic_standard_icon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/magic_unholy_icon.png b/modular_doppler/modular_powers/icons/ui/powers/magic_unholy_icon.png
new file mode 100644
index 00000000000000..636e4f8fd36f2f
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/magic_unholy_icon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/psykericon.png b/modular_doppler/modular_powers/icons/ui/powers/psykericon.png
new file mode 100644
index 00000000000000..d8c2d549d03338
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/psykericon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/thaumaturgeicon.png b/modular_doppler/modular_powers/icons/ui/powers/thaumaturgeicon.png
new file mode 100644
index 00000000000000..69824668b24c29
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/thaumaturgeicon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/theologisticon.png b/modular_doppler/modular_powers/icons/ui/powers/theologisticon.png
new file mode 100644
index 00000000000000..362b5300b84049
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/theologisticon.png differ
diff --git a/modular_doppler/modular_powers/icons/ui/powers/warfightericon.png b/modular_doppler/modular_powers/icons/ui/powers/warfightericon.png
new file mode 100644
index 00000000000000..79a43fe6c68155
Binary files /dev/null and b/modular_doppler/modular_powers/icons/ui/powers/warfightericon.png differ
diff --git a/tgstation.dme b/tgstation.dme
index 212253e407290b..33c68e98291306 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -7501,6 +7501,7 @@
#include "modular_doppler\modular_mood\code\mood_events\race_drink.dm"
#include "modular_doppler\modular_powers\code\_power.dm"
#include "modular_doppler\modular_powers\code\_resonant_projectile.dm"
+#include "modular_doppler\modular_powers\code\power_paths.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"
@@ -7512,9 +7513,11 @@
#include "modular_doppler\modular_powers\code\powers_vv.dm"
#include "modular_doppler\modular_powers\code\misc\antiresonant_cuffs.dm"
#include "modular_doppler\modular_powers\code\misc\reality_anchor.dm"
+#include "modular_doppler\modular_powers\code\misc\stomach_resource_conversion.dm"
#include "modular_doppler\modular_powers\code\misc\thaumaturgic_supplies.dm"
#include "modular_doppler\modular_powers\code\misc\erasure_event\erasure_controller.dm"
#include "modular_doppler\modular_powers\code\misc\erasure_event\erasure_delam.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_datum.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"
@@ -7528,13 +7531,12 @@
#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_datum.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"
@@ -7542,8 +7544,14 @@
#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\irregular\_irregular_action.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\irregular\_irregular_datum.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\irregular\_irregular_power.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\irregular\false_power.dm"
+#include "modular_doppler\modular_powers\code\powers\mortal\irregular\hidden_powers.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_datum.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"
@@ -7561,9 +7569,9 @@
#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_datum.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"
@@ -7572,17 +7580,13 @@
#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\inexorable.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"
@@ -7591,6 +7595,7 @@
#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_datum.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"
@@ -7604,7 +7609,22 @@
#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\imbued\_imbued_action.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\_imbued_datum.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\_imbued_power.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\_imbued_root.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\_imbued_root_anomalous.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\_imbued_root_enchanted.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\alcohol_is_mana.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\mage_sight.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\radiosynthesis.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\resonant_immune.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\returning_throws.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\summonable.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\riftwalker\_riftwalker_datum.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\imbued\riftwalker\riftwalker.dm"
#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm"
+#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_datum.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\deflect.dm"
@@ -7639,11 +7659,13 @@
#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_organs\paracausal.dm"
#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_organs\surgery.dm"
#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm"
+#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_datum.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_datum.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"
@@ -7664,6 +7686,7 @@
#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_datum.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"
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/index.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/index.tsx
index 115aefe8a9b4c6..b2a8bb75defd57 100644
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/index.tsx
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/CharacterPreferences/index.tsx
@@ -10,11 +10,15 @@ import { exhaustiveCheck } from 'tgui-core/exhaustive';
import { PageButton } from '../components/PageButton';
import { LanguagesPage } from '../LanguagesMenu'; /* DOPPLER EDIT ADDITION */
import { LorePage } from '../LorePage'; /* DOPPLER EDIT ADDITION */
-import { MortalPage } from '../Mortal'; /* DOPPLER EDIT ADDITION */
+import {
+ getPowerCatalogData,
+ getPowerPathData,
+ useSelectedPowerPath,
+} from '../PowerPathBridge'; /* DOPPLER EDIT ADDITION */
+import { PowerPathPage } from '../PowerPathPage';
import { PowersPage } from '../PowersMenu'; /* DOPPLER EDIT ADDITION */
-import { ResonantPage } from '../Resonant'; /* DOPPLER EDIT ADDITION */
-import { SorcerousPage } from '../Sorcerous'; /* DOPPLER EDIT ADDITION */
-import { PreferencesMenuData } from '../types';
+import { SelectedPowersPage } from '../SelectedPowersPage'; /* DOPPLER EDIT ADDITION */
+import type { PreferencesMenuData } from '../types'; /* DOPPLER EDIT ADDITION */
import { AntagsPage } from './AntagsPage';
import { JobsPage } from './JobsPage';
import { LoadoutPage } from './loadout';
@@ -32,9 +36,8 @@ enum Page {
Languages /* DOPPLER EDIT ADDITION */,
Lore /* DOPPLER EDIT ADDITION */,
Powers /* DOPPLER EDITION ADDITION */,
- Mortal /* DOPPLER EDIT ADDITION */,
- Sorcerous /* DOPPLER EDIT ADDITION */,
- Resonant /* DOPPLER EDIT ADDITION */,
+ PowerPath /* DOPPLER EDIT ADDITION */,
+ SelectedPowers /* DOPPLER EDIT ADDITION */,
}
type ProfileProps = {
@@ -73,6 +76,18 @@ export function CharacterPreferenceWindow(props) {
const { act, data } = useBackend();
const [currentPage, setCurrentPage] = useState(Page.Main);
+ /* DOPPLER EDIT START - Powers data */
+ const { selectedPowerPathId, setSelectedPowerPathId } =
+ useSelectedPowerPath();
+ const powerCatalogData = getPowerCatalogData();
+ const powerPathConfig = getPowerPathData(
+ powerCatalogData,
+ selectedPowerPathId,
+ );
+ /* DOPPLER EDIT END */
+
+ const activePowersThemeColor =
+ currentPage === Page.PowerPath ? powerPathConfig.themeColor : undefined;
let pageContents;
@@ -90,29 +105,29 @@ export function CharacterPreferenceWindow(props) {
case Page.Languages:
pageContents = ;
break;
- case Page.Mortal:
- pageContents = (
- setCurrentPage(Page.Powers)} />
- );
- break;
- case Page.Resonant:
+ case Page.PowerPath:
pageContents = (
- setCurrentPage(Page.Powers)} />
+ setCurrentPage(Page.Powers)}
+ pathId={selectedPowerPathId}
+ />
);
break;
- case Page.Sorcerous:
+ case Page.SelectedPowers:
pageContents = (
- setCurrentPage(Page.Powers)}
+ setCurrentPage(Page.Powers)}
/>
);
break;
case Page.Powers:
pageContents = (
setCurrentPage(Page.Mortal)}
- handleOpenSorcerous={() => setCurrentPage(Page.Sorcerous)}
- handleOpenResonant={() => setCurrentPage(Page.Resonant)}
+ handleOpenSelectedPowers={() => setCurrentPage(Page.SelectedPowers)}
+ handleOpenPath={(pathId) => {
+ setSelectedPowerPathId(pathId);
+ setCurrentPage(Page.PowerPath);
+ }}
/>
);
break;
@@ -229,7 +244,16 @@ export function CharacterPreferenceWindow(props) {
currentPage={currentPage}
page={Page.Powers}
setPage={setCurrentPage}
- otherActivePages={[Page.Mortal, Page.Sorcerous, Page.Resonant]}
+ activeStyle={
+ activePowersThemeColor
+ ? {
+ backgroundColor: activePowersThemeColor,
+ borderColor: activePowersThemeColor,
+ color: 'black',
+ }
+ : undefined
+ }
+ otherActivePages={[Page.PowerPath, Page.SelectedPowers]}
>
Powers
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx
deleted file mode 100644
index 01853e138dbe88..00000000000000
--- a/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { Box, Button, Collapsible, Section, Stack } from 'tgui-core/components';
-
-import { useBackend } from '../../backend';
-import { Powers } from './PowersMenu';
-import type { PreferencesMenuData } from './types';
-
-type MortalPagePowers = {
- handleCloseMortal: () => void;
-};
-
-export const MortalPage = (props: MortalPagePowers) => {
- const { data } = useBackend();
- const descriptionBlock = (text: string) => (
-
- {text}
-
- );
- return (
-
-
-
-
-
-
-
-
-
- Hover over the learn button to view the required root power, if
- applicable.
-
-
-
-
-
-
- {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) => (
-
- ))}
-
-
-
-
-
- {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) => (
-
- ))}
-
-
-
-
-
- {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/PowerData.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/PowerData.ts
new file mode 100644
index 00000000000000..fd8a55d9f3b7ad
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/PowerData.ts
@@ -0,0 +1,50 @@
+/*
+Power data now arrives in two halves:
+- server prefs contain the constant catalog for each power
+- ui data contains the current character-specific state
+
+This helper recombines those halves into the full shape the powers pages want to render.
+Augmented is the only slightly special case, because its augment metadata is split between constant slot info and runtime arm assignment state.
+*/
+
+import type {
+ Power,
+ PowerByPathId,
+ PowerPathId,
+ PowerStateByPathId,
+ PowerStaticByPathId,
+} from './types';
+
+export function mergePowerPathData(
+ powerStaticPaths: PowerStaticByPathId,
+ powerStatePaths: PowerStateByPathId,
+): PowerByPathId {
+ const mergedPowerPaths = {} as PowerByPathId;
+
+ for (const pathId of Object.keys(powerStaticPaths) as PowerPathId[]) {
+ const staticPowers = powerStaticPaths[pathId] || [];
+ const statePowers = powerStatePaths[pathId] || [];
+ const stateByName = new Map(
+ statePowers.map((powerState) => [powerState.name, powerState]),
+ );
+
+ mergedPowerPaths[pathId] = staticPowers.map((powerStatic): Power => {
+ const powerState = stateByName.get(powerStatic.name);
+ return {
+ ...powerStatic,
+ ...powerState,
+ has_power: powerState?.has_power || false,
+ state: powerState?.state || 'transparent',
+ augment:
+ powerStatic.augment || powerState?.augment
+ ? {
+ ...(powerStatic.augment || {}),
+ ...(powerState?.augment || {}),
+ }
+ : null,
+ };
+ });
+ }
+
+ return mergedPowerPaths;
+}
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/PowerPathBridge.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/PowerPathBridge.ts
new file mode 100644
index 00000000000000..5b49e487a6a816
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/PowerPathBridge.ts
@@ -0,0 +1,96 @@
+/*
+ Passes powers path data to the TGUI side, as well as offering a few fallback options to prevent everything from breaking.
+*/
+
+import { useEffect, useState } from 'react';
+import type {
+ PowerArchetypeData,
+ PowerPathData,
+ PowerPathId,
+ ServerData,
+} from './types';
+import { useServerPrefs } from './useServerPrefs';
+
+type PowerCatalogData = ServerData['powers'];
+
+export const fallbackPowerPathId: PowerPathId = 'thaumaturge';
+
+/// Fallback path data in case of errors.
+const fallbackPowerPathData: PowerPathData = {
+ displayName: 'Fallback Path',
+ archetypeId: 'fallback',
+ isAvailable: false,
+ mechanicsText:
+ 'The power path bridge could not resolve DM-provided path data for this selection.',
+ overviewText:
+ 'Please message the developers and include the steps you took to produce this effect.',
+ themeColor: '#ffffff',
+};
+
+/// Gets the ID of the fallback power in case of errors
+export function getFallbackPowerPathId(
+ powerCatalogData?: PowerCatalogData,
+): PowerPathId {
+ return powerCatalogData?.fallback_power_path_id || fallbackPowerPathId;
+}
+
+/// Gets and returns a single power path's DM-provided constant data from the powers prefs payload.
+export function getPowerPathData(
+ powerCatalogData: PowerCatalogData | undefined,
+ pathId: PowerPathId,
+): PowerPathData {
+ return powerCatalogData?.power_path_data[pathId] || fallbackPowerPathData;
+}
+
+/// Returns the serverPref's powers property from serverprefs, which lists all powers, archetypes, etc..
+export function getPowerCatalogData() {
+ return useServerPrefs()?.powers;
+}
+
+/// Gets and returns all powers archetypes.
+export function getPowerArchetypes(
+ powerCatalogData: PowerCatalogData | undefined,
+): PowerArchetypeData[] {
+ return powerCatalogData?.power_path_archetypes || [];
+}
+
+/// Takes a given path ID, validates it, and then returns the requested path ID if valid or the fallback path's ID (see above) if invalid.
+export function useValidatedPowerPathId(
+ requestedPathId: PowerPathId,
+): PowerPathId {
+ const powerCatalogData = getPowerCatalogData();
+ const resolvedFallbackPowerPathId = getFallbackPowerPathId(powerCatalogData);
+
+ // If the fallback path's ID was given
+ if (requestedPathId === fallbackPowerPathId) {
+ return resolvedFallbackPowerPathId;
+ }
+
+ // If a path's ID was given that matches an existing path.
+ if (powerCatalogData?.power_path_data[requestedPathId]) {
+ return requestedPathId;
+ }
+
+ // If you did not give a valid path ID.
+ return resolvedFallbackPowerPathId;
+}
+
+/// Returns the power path that is currently selected in the UI.
+export function useSelectedPowerPath() {
+ const [requestedPowerPathId, setRequestedPowerPathId] =
+ useState(fallbackPowerPathId);
+ const resolvedPowerPathId = useValidatedPowerPathId(requestedPowerPathId);
+
+ useEffect(() => {
+ if (requestedPowerPathId === resolvedPowerPathId) {
+ return;
+ }
+
+ setRequestedPowerPathId(resolvedPowerPathId);
+ }, [requestedPowerPathId, resolvedPowerPathId]);
+
+ return {
+ selectedPowerPathId: resolvedPowerPathId,
+ setSelectedPowerPathId: setRequestedPowerPathId,
+ };
+}
diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/PowerPathPage.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/PowerPathPage.tsx
new file mode 100644
index 00000000000000..bb9c5edc3e660a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/PreferencesMenu/PowerPathPage.tsx
@@ -0,0 +1,909 @@
+/*
+ Gets the contents of all the revelant powers for the given path and adjust it's styling and contents based on that.
+*/
+
+import { filter } from 'es-toolkit/compat';
+import { type ReactNode, useState } from 'react';
+import {
+ Box,
+ Button,
+ Collapsible,
+ DmIcon,
+ Dropdown,
+ Floating,
+ Section,
+ Stack,
+ Tooltip,
+} from 'tgui-core/components';
+import { resolveAsset } from '../../assets';
+import { useBackend } from '../../backend';
+import { PreferenceList } from './CharacterPreferences/MainPage';
+import { mergePowerPathData } from './PowerData';
+import { getPowerCatalogData, getPowerPathData } from './PowerPathBridge';
+import type { Power, PowerPathId, PreferencesMenuData } from './types';
+
+type PowerPathPageProps = {
+ handleClosePath: () => void;
+ pathId: PowerPathId;
+};
+
+export type PowerTreeNode = {
+ children: PowerTreeNode[];
+ power: Power;
+};
+
+export type PowerBuckets = {
+ anyRootNodes: PowerTreeNode[];
+ rootNodes: PowerTreeNode[];
+};
+
+const availablePowerTextColor = 'white';
+const unavailablePowerTextColor = 'rgba(255, 255, 255, 0.52)';
+const powerTitleIconGutterWidth = '32px';
+const powerMagicIndicatorOrder = [
+ 'unholy',
+ 'mental',
+ 'scrying',
+ 'magical',
+] as const;
+const powerMagicIndicatorConfig = {
+ magical: {
+ assetName: 'magic_standard_icon.png',
+ tooltip: 'This power is magical and may be susceptible to anti-magic!',
+ },
+ mental: {
+ assetName: 'magic_mental_icon.png',
+ tooltip:
+ 'This power is mental and may be susceptible to mental anti-magics (and tinfoil!)',
+ },
+ scrying: {
+ assetName: 'magic_scrying_icon.png',
+ tooltip:
+ 'This power is a form of scrying and may be susceptible to anti-scrying powers.',
+ },
+ unholy: {
+ assetName: 'magic_unholy_icon.png',
+ tooltip: 'This power is unholy and may be susceptible to holy anti-magics!',
+ },
+} as const;
+
+export function hexToRgba(hexColor: string, alpha: number) {
+ const sanitizedHex = hexColor.replace('#', '');
+ if (sanitizedHex.length !== 6) {
+ return `rgba(255, 255, 255, ${alpha})`;
+ }
+
+ const red = Number.parseInt(sanitizedHex.slice(0, 2), 16);
+ const green = Number.parseInt(sanitizedHex.slice(2, 4), 16);
+ const blue = Number.parseInt(sanitizedHex.slice(4, 6), 16);
+ return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
+}
+
+function getCorrespondingPreferences(
+ customizationOptions: string[],
+ relevantPreferences: Record,
+) {
+ return Object.fromEntries(
+ filter(Object.entries(relevantPreferences), ([preferenceName]) =>
+ customizationOptions.includes(preferenceName),
+ ),
+ );
+}
+
+function isSubtypeRootRequirement(power: Power) {
+ return Boolean(
+ power.required_allow_subtypes && power.required_powers?.length,
+ );
+}
+
+function hasSelectedDescendant(node: PowerTreeNode): boolean {
+ return node.children.some(
+ (childNode) =>
+ childNode.power.has_power || hasSelectedDescendant(childNode),
+ );
+}
+
+function shouldStartExpanded(node: PowerTreeNode, depth: number) {
+ if (node.power.has_power || hasSelectedDescendant(node)) {
+ return true;
+ }
+
+ return depth !== 1 ? false : false;
+}
+
+export function buildPowerTreeNodes(powers: Power[]): PowerBuckets {
+ const nodeByName = new Map();
+ const attachedPowerNames = new Set();
+ const anyRootPowerNames = new Set(
+ powers.filter(isSubtypeRootRequirement).map((power) => power.name),
+ );
+
+ for (const power of powers) {
+ nodeByName.set(power.name, {
+ children: [],
+ power,
+ });
+ }
+
+ for (const power of powers) {
+ if (power.required_allow_subtypes) {
+ continue;
+ }
+
+ // A power can list multiple requirements, but the shared page only nests it under a concrete parent when one of those requirements exists on-page.
+ const parentName = power.required_powers?.find((requiredPowerName) =>
+ nodeByName.has(requiredPowerName),
+ );
+
+ if (!parentName) {
+ continue;
+ }
+
+ const parentNode = nodeByName.get(parentName);
+ const childNode = nodeByName.get(power.name);
+
+ if (!parentNode || !childNode) {
+ continue;
+ }
+
+ parentNode.children.push(childNode);
+ attachedPowerNames.add(power.name);
+ }
+
+ const rootNodes = powers
+ .filter(
+ (power) =>
+ !attachedPowerNames.has(power.name) &&
+ !anyRootPowerNames.has(power.name),
+ )
+ .map((power) => nodeByName.get(power.name))
+ .filter(Boolean) as PowerTreeNode[];
+
+ const anyRootNodes = powers
+ .filter((power) => anyRootPowerNames.has(power.name))
+ .map((power) => nodeByName.get(power.name))
+ .filter(Boolean) as PowerTreeNode[];
+
+ return {
+ anyRootNodes,
+ rootNodes,
+ };
+}
+
+export function flattenPowerTreeNodes(nodes: PowerTreeNode[]): Power[] {
+ const flattenedPowers: Power[] = [];
+
+ function visitNode(node: PowerTreeNode) {
+ flattenedPowers.push(node.power);
+ for (const childNode of node.children) {
+ visitNode(childNode);
+ }
+ }
+
+ for (const node of nodes) {
+ visitNode(node);
+ }
+
+ return flattenedPowers;
+}
+
+function formatRequirementText(power: Power) {
+ if (!power.required_powers?.length) {
+ return null;
+ }
+
+ const requirementPrefix = power.required_allow_subtypes
+ ? 'Requires any subtype of'
+ : power.required_allow_any
+ ? 'Requires any of'
+ : 'Requires';
+
+ return `${requirementPrefix}: ${power.required_powers.join(', ')}`;
+}
+
+function getPowerButtonIcon(power: Power) {
+ if (Array.isArray(power.root_badge_icon)) {
+ return (
+ power.root_badge_icon.find((iconName): iconName is string =>
+ Boolean(iconName),
+ ) || false
+ );
+ }
+
+ return power.root_badge_icon || false;
+}
+
+function getPowerTitleColor(power: Power) {
+ return power.state === 'transparent'
+ ? unavailablePowerTextColor
+ : availablePowerTextColor;
+}
+
+function getPowerDescriptionColor(power: Power) {
+ return getPowerTitleColor(power);
+}
+
+function getPowerButtonWord(power: Power) {
+ if (power.state === 'bad') {
+ return 'Forget';
+ }
+
+ if (power.state === 'good') {
+ return 'Learn';
+ }
+
+ return 'N/A';
+}
+
+function getOrderedPowerMagicFlags(power: Power) {
+ const powerMagicFlags = new Set(power.magic_flags || []);
+ return powerMagicIndicatorOrder.filter((magicFlag) =>
+ powerMagicFlags.has(magicFlag),
+ );
+}
+
+function InlineCollapsibleTitle(props: {
+ children: ReactNode;
+ color?: string;
+}) {
+ const { children, color = availablePowerTextColor } = props;
+
+ return (
+
+ {children}
+
+ );
+}
+
+function PowerTitle(props: { power: Power }) {
+ const { power } = props;
+ const hasActionIcon = Boolean(power.action_icon && power.action_icon_state);
+
+ return (
+
+
+ {hasActionIcon ? (
+ // Keep title alignment stable even when a power has no action icon to show.
+
+ }
+ height="32px"
+ icon={power.action_icon!}
+ icon_state={power.action_icon_state!}
+ style={{
+ height: '32px',
+ width: powerTitleIconGutterWidth,
+ }}
+ width="32px"
+ />
+ ) : (
+
+ )}
+
+
+ {power.name}
+
+
+ );
+}
+
+function PowerControls(props: {
+ act: (action: string, payload?: object) => void;
+ customizationPreferences: Record;
+ power: Power;
+ themeColor: string;
+}) {
+ const { act, customizationPreferences, power, themeColor } = props;
+ const [customizationExpanded, setCustomizationExpanded] = useState(false);
+ const requirementText = formatRequirementText(power);
+ const buttonIcon = getPowerButtonIcon(power);
+ const customizationOptions = power.customization_options || [];
+ const hasCustomization =
+ power.customizable && power.has_power && customizationOptions.length > 0;
+ const hasExpandableCustomization =
+ hasCustomization && Object.entries(customizationPreferences).length > 0;
+
+ return (
+
+
+
+
+ {`Cost: ${power.cost}`}
+
+ {requirementText ? (
+
+ {requirementText}
+
+ ) : null}
+
+ {getOrderedPowerMagicFlags(power).map((magicFlag) => {
+ const indicatorConfig = powerMagicIndicatorConfig[magicFlag];
+ return (
+
+
+
+
+
+ );
+ })}
+ {hasCustomization ? (
+
+ {
+ event.stopPropagation();
+ }}
+ style={{
+ boxShadow: '0px 4px 8px 3px rgba(0, 0, 0, 0.7)',
+ }}
+ >
+
+
+
+
+
+
+ ) : null
+ }
+ onOpenChange={setCustomizationExpanded}
+ placement="bottom-end"
+ stopChildPropagation
+ >
+