From f1c1fe619092baa2ba059cd11b3f36e47cf6268a Mon Sep 17 00:00:00 2001 From: _0Steven Date: Wed, 3 Sep 2025 18:07:30 +0200 Subject: [PATCH 001/212] initial commit [SUPER WIP] --- code/__DEFINES/~doppler_defines/powers.dm | 73 ++++- code/controllers/subsystem/ticker.dm | 1 + .../bitrunning/server/obj_generation.dm | 1 + code/modules/client/preferences_savefile.dm | 17 +- .../modules/mob/dead/new_player/new_player.dm | 1 + code/modules/mob_spawn/mob_spawn.dm | 2 + modular_doppler/_HELPERS/preferences.dm | 34 -- modular_doppler/icspawn/observer_spawn.dm | 1 + .../preferences/preferences.dm | 3 - .../preferences/preferences_setup.dm | 9 + modular_doppler/modular_powers/code/_power.dm | 241 ++++++++++++++ .../powers/sorcerous/enigmatist/_chalks.dm | 227 +++++++++++++ .../sorcerous/enigmatist/_enigmatist_root.dm | 18 + .../sorcerous/enigmatist/_enigmatist_spell.dm | 83 +++++ .../sorcerous/enigmatist/lodestone_legends.dm | 27 ++ .../code/powers/sorcerous/prestidigitation.dm | 60 ++++ .../thaumaturge/_thaumaturge_root.dm | 29 ++ .../modular_powers/code/powers_helpers.dm | 14 + .../modular_powers/code/powers_living.dm | 67 ++++ .../modular_powers/code/powers_prefs.dm | 15 + .../code/powers_prefs_middleware.dm | 255 ++++++++++++++ .../modular_powers/code/powers_subsystem.dm | 222 +++++++++++++ .../modular_powers/powers/_powers.dm | 169 ---------- .../modular_powers/powers/core_powers.dm | 62 ---- .../powers/mortal_powers/augmented.dm | 24 -- .../powers/mortal_powers/expert.dm | 56 ---- .../powers/mortal_powers/warfighter.dm | 62 ---- .../powers/resonant_powers/aberrant.dm | 41 --- .../powers/resonant_powers/cultivator.dm | 41 --- .../powers/resonant_powers/psyker.dm | 20 -- .../powers/sorcerous_powers/enigmatist.dm | 30 -- .../powers/sorcerous_powers/thaumaturge.dm | 23 -- .../powers/sorcerous_powers/theologist.dm | 119 ------- .../preferences/powers_middleware.dm | 310 ------------------ .../slimes/code/roundstartslimes.dm | 1 + tgstation.dme | 24 +- 36 files changed, 1355 insertions(+), 1027 deletions(-) create mode 100644 modular_doppler/modular_powers/code/_power.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_chalks.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_spell.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/prestidigitation.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm create mode 100644 modular_doppler/modular_powers/code/powers_helpers.dm create mode 100644 modular_doppler/modular_powers/code/powers_living.dm create mode 100644 modular_doppler/modular_powers/code/powers_prefs.dm create mode 100644 modular_doppler/modular_powers/code/powers_prefs_middleware.dm create mode 100644 modular_doppler/modular_powers/code/powers_subsystem.dm delete mode 100644 modular_doppler/modular_powers/powers/_powers.dm delete mode 100644 modular_doppler/modular_powers/powers/core_powers.dm delete mode 100644 modular_doppler/modular_powers/powers/mortal_powers/augmented.dm delete mode 100644 modular_doppler/modular_powers/powers/mortal_powers/expert.dm delete mode 100644 modular_doppler/modular_powers/powers/mortal_powers/warfighter.dm delete mode 100644 modular_doppler/modular_powers/powers/resonant_powers/aberrant.dm delete mode 100644 modular_doppler/modular_powers/powers/resonant_powers/cultivator.dm delete mode 100644 modular_doppler/modular_powers/powers/resonant_powers/psyker.dm delete mode 100644 modular_doppler/modular_powers/powers/sorcerous_powers/enigmatist.dm delete mode 100644 modular_doppler/modular_powers/powers/sorcerous_powers/thaumaturge.dm delete mode 100644 modular_doppler/modular_powers/powers/sorcerous_powers/theologist.dm delete mode 100644 modular_doppler/modular_powers/preferences/powers_middleware.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index c24462b94937b2..3451a5622580d5 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -3,16 +3,75 @@ * All defines related to the powers system */ -// Maximum amount of points a player can spend on their powers +/// Maximum amount of points a player can spend on their powers +#define MAXIMUM_POWER_POINTS 20 +#define POWER_PRIORITY_ROOT "Root" +#define POWER_PRIORITY_BASIC "Basic" +#define POWER_PRIORITY_ADVANCED "Advanced" -#define MAXIMUM_POWER_POINTS 20 +#define POWER_ARCHETYPE_SORCEROUS "Sorcerous" +#define POWER_ARCHETYPE_RESONANT "Resonant" +#define POWER_ARCHETYPE_MORTAL "Mortal" + +#define POWER_PATH_THAUMATURGE "Thaumaturge" +#define POWER_PATH_ENIGMATIST "Enigmatist" +#define POWER_PATH_THEOLOGIST "Theologist" +#define POWER_PATH_PSYKER "Psyker" +#define POWER_PATH_CULTIVATOR "Cultivator" +#define POWER_PATH_ABERRANT "Aberrant" +#define POWER_PATH_WARFIGHTER "Warfighter" +#define POWER_PATH_EXPERT "Expert" +#define POWER_PATH_AUGMENTED "Augmented" + +/// Any traits granted by powers. +#define POWER_TRAIT "power_trait" + +/// This power can only be applied to humans. +#define POWER_HUMAN_ONLY (1<<0) +/// This power processes on SSpowers (and should implement power process) +#define POWER_PROCESSES (1<<1) +/// This power is has a visual aspect in that it changes how the player looks. Used in generating dummies. +#define POWER_CHANGES_APPEARANCE (1<<2) + +/** + * SORCEROUS + * All defines related to the sorcerous archetype. + */ + +/// Trait held by all under the sorcerous archetype. +#define TRAIT_ARCHETYPE_SORCEROUS "archetype_sorcerous" + +/** + * SORCEROUS: ENIGMATIST + * All defines related to the enigmatist powers. + */ + +/// Standard value for how much damage enigmatist chalk can take. +#define ENIGMATIST_CHALK_STANDARD_INTEGRITY 100 + +// Standard damages an enigmatist spell can do. +#define ENIGMATIST_CHALK_TRIVIAL_DAMAGE (ENIGMATIST_CHALK_STANDARD_INTEGRITY / 100) +#define ENIGMATIST_CHALK_MINOR_DAMAGE (ENIGMATIST_CHALK_STANDARD_INTEGRITY / 10) +#define ENIGMATIST_CHALK_MODERATE_DAMAGE (ENIGMATIST_CHALK_STANDARD_INTEGRITY / 5) +#define ENIGMATIST_CHALK_MAJOR_DAMAGE (ENIGMATIST_CHALK_STANDARD_INTEGRITY / 2) +#define ENIGMATIST_CHALK_CRUSHING_DAMAGE (ENIGMATIST_CHALK_STANDARD_INTEGRITY) + +/// From /obj/item/enigmatist_chalk/click_alt(...): (enigmatist_flags, list/spell_options) +#define COMSIG_ENIGMATIST_CHALK_SELECTION "enigmatist_chalk_selection" + +// Bitflags for what type of chalk/power a given chalk/power is. +/// Basic resonant chalks/powers. +#define ENIGMATIST_RESONANT (1<<0) +/// Chalks/powers relating to unsealed lore. +#define ENIGMATIST_UNSEALED (1<<1) +/// Chalks/powers relating to illuminated lore. +#define ENIGMATIST_ILLUMINATED (1<<2) +/// Chalks/powers relating to divided lore. +#define ENIGMATIST_DIVIDED (1<<3) -GLOBAL_LIST_INIT(path_core_powers, list( - "path_sorcerous" = /datum/power/prestidigitation, - "path_resonant" = /datum/power/meditate, - "path_mortal" = /datum/power/tenacious -)) +/// Any Enigmatist lore whatsoever. +#define ENIGMATIST_ANY_ALL (ENIGMATIST_RESONANT|ENIGMATIST_UNSEALED|ENIGMATIST_ILLUMINATED|ENIGMATIST_DIVIDED) /**MORTAL DEFINES * I'm literally just using this to define Breacher Knuckle right now diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index 565a70f57088b8..d3365f89ba6bd7 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -550,6 +550,7 @@ SUBSYSTEM_DEF(ticker) if(new_player_mob.client?.prefs?.should_be_random_hardcore(player_assigned_role, new_player_living.mind)) new_player_mob.client.prefs.hardcore_random_setup(new_player_living) SSquirks.AssignQuirks(new_player_living, new_player_mob.client) + SSpowers.assign_powers(new_player_living, new_player_mob.client) // DOPPLER EDIT ADDITION - Archetype Powers //DOPPLER EDIT ADDITION if(ishuman(new_player_living)) var/list/loadout = loadout_list_to_datums(new_player_mob.client?.prefs?.read_preference(/datum/preference/loadout)) diff --git a/code/modules/bitrunning/server/obj_generation.dm b/code/modules/bitrunning/server/obj_generation.dm index a73fc8e86a1fc3..b712c54fadb278 100644 --- a/code/modules/bitrunning/server/obj_generation.dm +++ b/code/modules/bitrunning/server/obj_generation.dm @@ -170,6 +170,7 @@ if(!(domain_forbids_flags & DOMAIN_FORBIDS_ABILITIES)) avatar_preference.safe_transfer_prefs_to(avatar) SSquirks.AssignQuirks(avatar, prefs_disk.mock_client) + SSpowers.assign_powers(avatar, prefs_disk.mock_client) if(!(domain_forbids_flags & DOMAIN_FORBIDS_ITEMS) && prefs_disk.include_loadout) avatar.equip_outfit_and_loadout(/datum/outfit, avatar_preference) diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 08c915b919852f..acdb55229c0647 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -356,18 +356,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car languages = save_languages alt_job_titles = save_data?["alt_job_titles"] - var/list/save_powers = SANITIZE_LIST(save_data?["powers"]) - - for(var/power in save_powers) - var/value = save_powers[power] - save_powers -= power - - if(istext(value)) - value = _text2path(value) - - save_powers[power] = value - - powers = save_powers + all_powers = save_data?["all_powers"] // DOPPLER SHIFT ADDITION END //try to fix any outdated data if necessary @@ -381,7 +370,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car all_quirks = SANITIZE_LIST(all_quirks) // DOPPLER SHIFT ADDITION BEGIN languages = SANITIZE_LIST(languages) - powers = SANITIZE_LIST(powers) + all_powers = SANITIZE_LIST(all_powers) // DOPPLER SHIFT ADDITION END //Validate job prefs @@ -437,7 +426,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car save_data["all_quirks"] = all_quirks save_data["languages"] = languages // DOPPLER SHIFT ADDITION - we might want to migrate this save_data["alt_job_titles"] = alt_job_titles // DOPPLER SHIFT ADDITION: alt job titles - save_data["powers"] = powers // dopplor powerz :3c + save_data["all_powers"] = all_powers // DOPPLER SHIFT ADDITION - Powers system return TRUE diff --git a/code/modules/mob/dead/new_player/new_player.dm b/code/modules/mob/dead/new_player/new_player.dm index f0ddaf3f0507a2..0fa58b1b0b450a 100644 --- a/code/modules/mob/dead/new_player/new_player.dm +++ b/code/modules/mob/dead/new_player/new_player.dm @@ -238,6 +238,7 @@ if((job.job_flags & JOB_ASSIGN_QUIRKS) && humanc && CONFIG_GET(flag/roundstart_traits)) SSquirks.AssignQuirks(humanc, humanc.client) + SSpowers.assign_powers(humanc, humanc.client) // DOPPLER EDIT ADDITION - Archetype Powers if(humanc) // Quirks may change manifest datapoints, so inject only after assigning quirks GLOB.manifest.inject(humanc, person_client = humanc.client) // DOPPLER EDIT - RP Records - ORIGINAL: GLOB.manifest.inject(humanc) diff --git a/code/modules/mob_spawn/mob_spawn.dm b/code/modules/mob_spawn/mob_spawn.dm index 8520ad82fac5d9..6115a159ed08c2 100644 --- a/code/modules/mob_spawn/mob_spawn.dm +++ b/code/modules/mob_spawn/mob_spawn.dm @@ -108,6 +108,7 @@ if(allow_prefs && spawned_human.client) spawned_human.client?.prefs.safe_transfer_prefs_to(spawned_human) SSquirks.AssignQuirks(spawned_human, spawned_human.client) + SSpowers.assign_powers(spawned_human, spawned_human.client) if(allow_loadout) spawned_human.equip_outfit_and_loadout(outfit, spawned_human.client?.prefs) else @@ -118,6 +119,7 @@ var/mob/living/carbon/human/spawned_human = spawned_mob spawned_human.client?.prefs.safe_transfer_prefs_to(spawned_human) SSquirks.AssignQuirks(spawned_human, spawned_human.client) + SSpowers.assign_powers(spawned_human, spawned_human.client) if(allow_loadout) spawned_human.equip_outfit_and_loadout(new /datum/outfit(), spawned_human.client?.prefs) /// DOPPLER SHIFT ADDITION END diff --git a/modular_doppler/_HELPERS/preferences.dm b/modular_doppler/_HELPERS/preferences.dm index 4cc10b17833892..82e0bf5e5ba242 100644 --- a/modular_doppler/_HELPERS/preferences.dm +++ b/modular_doppler/_HELPERS/preferences.dm @@ -1,37 +1,3 @@ -/// List of power prototypes to reference, assoc [type] = prototype -GLOBAL_LIST_INIT_TYPED(power_datum_instances, /datum/power, init_power_prototypes()) - -// list of power datums -GLOBAL_LIST_INIT(all_powers, init_all_powers()) - -/proc/init_power_prototypes() - - var/list/power_list = list() - - for(var/datum/power/power_type as anything in typesof(/datum/power)) - if(!initial(power_type.name)) - continue - if(!power_type.is_accessible) - continue - - power_list[power_type] = new power_type() - - return power_list - -/// List f all powers -/proc/init_all_powers() - - var/list/powers_list = list() - - for(var/datum/power/power_type as anything in typesof(/datum/power)) - if(!initial(power_type.name)) - continue - if(!power_type.is_accessible) - continue - - powers_list += power_type - - return powers_list /// List of all Brain Traumas for the Quirk. NOT every Trauma in the game /// Many are left out because they're covered by other quirks or are strictly beneficial diff --git a/modular_doppler/icspawn/observer_spawn.dm b/modular_doppler/icspawn/observer_spawn.dm index 4f0948ec05b1d0..a93ff04adac48a 100644 --- a/modular_doppler/icspawn/observer_spawn.dm +++ b/modular_doppler/icspawn/observer_spawn.dm @@ -59,6 +59,7 @@ spawned_player.equipOutfit(dresscode) if(addquirks == "Quirks & Loadout" || addquirks == "Quirks Only") SSquirks.AssignQuirks(player_as_human, user.client) + SSpowers.assign_powers(player_as_human, user.client) player_as_human.dna.update_dna_identity() else if(dresscode != "Naked") spawned_player.equipOutfit(dresscode) diff --git a/modular_doppler/modular_customization/preferences/preferences.dm b/modular_doppler/modular_customization/preferences/preferences.dm index 5d75c7b616868f..7ff8be0610a906 100644 --- a/modular_doppler/modular_customization/preferences/preferences.dm +++ b/modular_doppler/modular_customization/preferences/preferences.dm @@ -6,9 +6,6 @@ /// Associative list, keyed by language typepath, pointing to list(percent_understood, (LANGUAGE_UNDERSTOOD, or LANGUAGE_SPOKEN, for whether we understand or speak the language)) var/list/languages = list() - /// Associative list containing all powers, pointing to their respective cost - var/list/powers = list() - // Updates the mob's chat color in the global cache /datum/preferences/safe_transfer_prefs_to(mob/living/carbon/human/character, icon_updates = TRUE, is_antag = FALSE) . = ..() diff --git a/modular_doppler/modular_customization/preferences/preferences_setup.dm b/modular_doppler/modular_customization/preferences/preferences_setup.dm index d54c36cd35836f..738978fc88d042 100644 --- a/modular_doppler/modular_customization/preferences/preferences_setup.dm +++ b/modular_doppler/modular_customization/preferences/preferences_setup.dm @@ -44,5 +44,14 @@ continue mannequin.add_quirk(quirk_type, parent) + // Apply visual powers too. Same logic applies. + if(SSpowers?.initialized) + mannequin.cleanse_power_datums() + for(var/power_name as anything in all_powers) + var/datum/power/power_type = SSpowers.powers[power_name] + if(!(initial(power_type.power_flags) & POWER_CHANGES_APPEARANCE)) + continue + mannequin.add_archetype_power(power_type, parent) + mannequin.update_body() return mannequin.appearance diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm new file mode 100644 index 00000000000000..3a4938b27b4caa --- /dev/null +++ b/modular_doppler/modular_powers/code/_power.dm @@ -0,0 +1,241 @@ + +// Every power should be coded around being applied on spawn. +/datum/power + /// The name of the power + var/name = "Test Power" + /// The description of the power + var/desc = "This is a test power." + /// What the power is worth in preferences, zero = neutral / free + var/value = 0 + /// Flags related to this power. + var/power_flags = POWER_HUMAN_ONLY + /// Reference to the mob currently tied to this power datum. Powers are not singletons. + var/mob/living/power_holder + /// if applicable, apply and remove this mob trait + var/mob_trait + /// Amount of points this trait is worth towards the hardcore character mode. + /// Minus points implies a positive power, positive means its hard. + /// This is used to pick the powers assigned to a hardcore character. + //// 0 means its not available to hardcore draws. + var/hardcore_value = 0 + /// When making an abstract power (in OOP terms), don't forget to set this var to the type path for that abstract power. + var/abstract_parent_type = /datum/power + /// max stat below which this power can process (if it has POWER_PROCESSES) and above which it stops. + /// If null, then it will process regardless of stat. + var/maximum_process_stat = HARD_CRIT + /// A list of additional signals to register with update_process() + var/list/process_update_signals + /// A list of traits that should stop this power from processing. + /// Signals for adding and removing this trait will automatically be added to `process_update_signals`. + var/list/no_process_traits + + /// The overarching archetype this belongs to. + var/archetype + /// The path this belongs to. + var/path + /// The priority this has. + var/priority = NONE + /// The powers this requires, if any. + var/list/required_powers + + +/datum/power/New() + . = ..() + for(var/trait in no_process_traits) + LAZYADD(process_update_signals, list(SIGNAL_ADDTRAIT(trait), SIGNAL_REMOVETRAIT(trait))) + +/datum/power/Destroy() + if(power_holder) + remove_from_current_holder() + return ..() + +/// Called when power_holder is qdeleting. Simply qdels this datum and lets Destroy() handle the rest. +/datum/power/proc/on_holder_qdeleting(mob/living/source, force) + SIGNAL_HANDLER + qdel(src) + +/** + * Adds the power to a new power_holder. + * + * Performs logic to make sure new_holder is a valid holder of this power. + * Returns FALSEy if there was some kind of error. Returns TRUE otherwise. + * Arguments: + * * new_holder - The mob to add this power to. + * * power_transfer - If this is being added to the holder as part of a power transfer. Powers can use this to decide not to spawn new items or apply any other one-time effects. + */ +/datum/power/proc/add_to_holder(mob/living/new_holder, power_transfer = FALSE, client/client_source, unique = TRUE) + if(!new_holder) + CRASH("Power attempted to be added to null mob.") + + if((power_flags & POWER_HUMAN_ONLY) && !ishuman(new_holder)) + CRASH("Human only power attempted to be added to non-human mob.") + + if(new_holder.has_archetype_power(type)) + CRASH("Power attempted to be added to mob which already had this power.") + + if(power_holder) + CRASH("Attempted to add power to a holder when it already has a holder.") + + power_holder = new_holder + power_holder.powers += src + // If we weren't passed a client source try to use a present one + client_source ||= power_holder.client + + if(mob_trait) + ADD_TRAIT(power_holder, mob_trait, POWER_TRAIT) + + add(client_source) + + if(power_flags & POWER_PROCESSES) + if(!isnull(maximum_process_stat)) + RegisterSignal(power_holder, COMSIG_MOB_STATCHANGE, PROC_REF(on_stat_changed)) + if(process_update_signals) + RegisterSignals(power_holder, process_update_signals, PROC_REF(update_process)) + if(should_process()) + START_PROCESSING(SSpowers, src) + + if(!power_transfer) + if (unique) + add_unique(client_source) + + if(power_holder.client) + post_add() + else + RegisterSignal(power_holder, COMSIG_MOB_LOGIN, PROC_REF(on_power_holder_first_login)) + + RegisterSignal(power_holder, COMSIG_QDELETING, PROC_REF(on_holder_qdeleting)) + + return TRUE + +/// Removes the power from the current power_holder. +/datum/power/proc/remove_from_current_holder(power_transfer = FALSE) + if(!power_holder) + CRASH("Attempted to remove power from the current holder when it has no current holder.") + + UnregisterSignal(power_holder, list(COMSIG_MOB_STATCHANGE, COMSIG_MOB_LOGIN, COMSIG_QDELETING)) + if(process_update_signals) + UnregisterSignal(power_holder, process_update_signals) + + power_holder.powers -= src + + if(mob_trait && !QDELETED(power_holder)) + REMOVE_TRAIT(power_holder, mob_trait, POWER_TRAIT) + + if(power_flags & POWER_PROCESSES) + STOP_PROCESSING(SSpowers, src) + + remove() + + power_holder = null + +/** + * On client connection set power preferences. + * + * Run post_add to set the client preferences for the power. + * Clear the attached signal for login. + * Used when the power has been gained and no client is attached to the mob. + */ +/datum/power/proc/on_power_holder_first_login(mob/living/source) + SIGNAL_HANDLER + + UnregisterSignal(source, COMSIG_MOB_LOGIN) + post_add() + +/// Any effect that should be applied every single time the power is added to any mob, even when transferred. +/datum/power/proc/add(client/client_source) + return + +/// Any effects from the proc that should not be done multiple times if the power is transferred between mobs. +/// Put stuff like spawning items in here. +/datum/power/proc/add_unique(client/client_source) + return + +/// Removal of any reversible effects added by the power. +/datum/power/proc/remove() + return + +/// Any special effects or chat messages which should be applied. +/// This proc is guaranteed to run if the mob has a client when the power is added. +/// Otherwise, it runs once on the next COMSIG_MOB_LOGIN. +/datum/power/proc/post_add() + return + +/// Returns if the power holder should process currently or not. +/datum/power/proc/should_process() + SHOULD_CALL_PARENT(TRUE) + SHOULD_BE_PURE(TRUE) + if(QDELETED(power_holder)) + return FALSE + if(!(power_flags & POWER_PROCESSES)) + return FALSE + if(!isnull(maximum_process_stat) && power_holder.stat >= maximum_process_stat) + return FALSE + for(var/trait in no_process_traits) + if(HAS_TRAIT(power_holder, trait)) + return FALSE + return TRUE + +/// Checks to see if the power should be processing, and starts/stops it. +/datum/power/proc/update_process() + SIGNAL_HANDLER + SHOULD_NOT_OVERRIDE(TRUE) + if(should_process()) + START_PROCESSING(SSpowers, src) + else + STOP_PROCESSING(SSpowers, src) + +/// Updates processing status whenever the mob's stat changes. +/datum/power/proc/on_stat_changed(mob/living/source, new_stat) + SIGNAL_HANDLER + update_process() + +/// If a power is able to be selected for the mob's species +/datum/power/proc/is_species_appropriate(datum/species/mob_species) + if(mob_trait in GLOB.species_prototypes[mob_species].inherent_traits) + return FALSE + return TRUE + +/// Subtype power that has some bonus logic to spawn items for the player. +/datum/power/item_power + /// Lazylist of strings describing where all the power items have been spawned. + var/list/where_items_spawned + /// If true, the backpack automatically opens on post_add(). Usually set to TRUE when an item is equipped inside the player's backpack. + var/open_backpack = FALSE + abstract_parent_type = /datum/power/item_power + +/** + * Handles inserting an item in any of the valid slots provided, then allows for post_add notification. + * + * If no valid slot is available for an item, the item is left at the mob's feet. + * Arguments: + * * power_item - The item to give to the power holder. If the item is a path, the item will be spawned in first on the player's turf. + * * valid_slots - List of LOCATION_X that is fed into [/mob/living/carbon/proc/equip_in_one_of_slots]. + * * flavour_text - Optional flavour text to append to the where_items_spawned string after the item's location. + * * default_location - If the item isn't possible to equip in a valid slot, this is a description of where the item was spawned. + * * notify_player - If TRUE, adds strings to where_items_spawned list to be output to the player in [/datum/power/item_power/post_add()] + */ +/datum/power/item_power/proc/give_item_to_holder(obj/item/power_item, list/valid_slots, flavour_text = null, default_location = "at your feet", notify_player = FALSE) + if(ispath(power_item)) + power_item = new power_item(get_turf(power_holder)) + + var/mob/living/carbon/human/human_holder = power_holder + + var/where = human_holder.equip_in_one_of_slots(power_item, valid_slots, qdel_on_fail = FALSE, indirect_action = TRUE) || default_location + + if(where == LOCATION_BACKPACK) + open_backpack = TRUE + + if(notify_player) + LAZYADD(where_items_spawned, span_boldnotice("You have \a [power_item] [where]. [flavour_text]")) + +/datum/power/item_power/post_add() + if(open_backpack) + var/mob/living/carbon/human/human_holder = power_holder + // post_add() can be called via delayed callback. Check they still have a backpack equipped before trying to open it. + if(human_holder.back) + human_holder.back.atom_storage.show_contents(human_holder) + + for(var/chat_string in where_items_spawned) + to_chat(power_holder, chat_string) + + where_items_spawned = null diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_chalks.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_chalks.dm new file mode 100644 index 00000000000000..5a652af6e7c913 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_chalks.dm @@ -0,0 +1,227 @@ +#define CAT_ENIGMATIST "Enigmatist" + +/** + * Base Enigmatist Chalk + */ + +// TODO: make a basic enigmatist chalk item. +// TODO: using it sends a signal to the user and collects a list of powers there. +// Signal probably includes the type of chalk. + +/obj/item/enigmatist_chalk + name = "enigmatist chalk" + desc = "An abstract chalk item. Looks tasty. Mmmm... \ + Wait it's abstract in the coding sense. Quick, report it!" + icon = 'icons/obj/art/crayons.dmi' + icon_state = "crayonwhite" + worn_icon_state = "crayon" + + // stats parallel to crayons + w_class = WEIGHT_CLASS_TINY + force = 0 + throwforce = 0 + throw_speed = 3 + throw_range = 7 + attack_verb_continuous = list("attacks", "colours") + attack_verb_simple = list("attack", "colour") + grind_results = list() + interaction_flags_atom = parent_type::interaction_flags_atom | INTERACT_ATOM_IGNORE_MOBILITY + + /// Bitflag of which types of enigmatist powers this can invoke. + var/enigmatist_flags = NONE + /// Our current integrity. When this reaches <0, we break. + var/resonant_integrity = ENIGMATIST_CHALK_STANDARD_INTEGRITY + /// Our maximum integrity. + var/max_resonant_integrity = ENIGMATIST_CHALK_STANDARD_INTEGRITY + /// The currently selected enigmatist power, if any. + var/datum/weakref/current_selected_power_ref + +/obj/item/enigmatist_chalk/Initialize(mapload) + . = ..() + register_context() + register_item_context() + +/obj/item/enigmatist_chalk/Destroy(force) + current_selected_power_ref = null + return ..() + + +/obj/item/enigmatist_chalk/examine(mob/user) + . = ..() + . += span_notice("It's at [EXAMINE_HINT("[integrity_percent()]%")] integrity.") + +/obj/item/enigmatist_chalk/add_context( + atom/source, + list/context, + obj/item/held_item, + mob/user, +) + var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve() + if(isnull(current_selected_power)) + context[SCREENTIP_CONTEXT_ALT_LMB] = "Select spell" + return CONTEXTUAL_SCREENTIP_SET + + if(current_selected_power.power_holder == user) + context[SCREENTIP_CONTEXT_ALT_LMB] = "Reset selection" + else + context[SCREENTIP_CONTEXT_ALT_LMB] = "Select spell" + current_selected_power.chalk_add_context(src, context, held_item, user) + return CONTEXTUAL_SCREENTIP_SET + +/obj/item/enigmatist_chalk/add_item_context( + obj/item/source, + list/context, + atom/target, + mob/living/user, +) + var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve() + if(isnull(current_selected_power)) + return NONE + return current_selected_power.chalk_add_item_context(src, context, target, user) + + +/obj/item/enigmatist_chalk/attack_self(mob/user, modifiers) + . = ..() + var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve() + if(isnull(current_selected_power)) + return + return current_selected_power.chalk_attack_self(src, user, modifiers) + +/obj/item/enigmatist_chalk/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers) + var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve() + if(isnull(current_selected_power)) + return NONE + return current_selected_power.chalk_interact_with_atom(src, interacting_with, user, modifiers) + +/obj/item/enigmatist_chalk/interact_with_atom_secondary(atom/interacting_with, mob/living/user, list/modifiers) + var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve() + if(isnull(current_selected_power)) + return NONE + return current_selected_power.chalk_interact_with_atom_secondary(src, interacting_with, user, modifiers) + + +/obj/item/enigmatist_chalk/click_alt(mob/user) + var/datum/power/enigmatist_spell/current_selected_power = current_selected_power_ref?.resolve() + if(current_selected_power) + current_selected_power_ref = null + current_selected_power.chalk_reset_spell(user, src) + // Act as if nothing happened only if our spell wasn't set by our previous user. + if(current_selected_power.power_holder == user) + balloon_alert(user, "selection reset") + return CLICK_ACTION_SUCCESS + + var/list/spell_options = list() + SEND_SIGNAL(user, COMSIG_ENIGMATIST_CHALK_SELECTION, enigmatist_flags, spell_options) + if(!length(spell_options)) + balloon_alert(user, "you know nothing!") + return CLICK_ACTION_BLOCKING + + var/list/radial_items = list() + for(var/spell_name as anything in spell_options) + var/datum/power/enigmatist_spell/spell_option = spell_options[spell_name] + var/datum/radial_menu_choice/radial_option = new() + radial_option.name = spell_option.get_option_name() + radial_option.info = spell_option.get_option_desc() + radial_option.image = image(icon = spell_option.option_icon, icon_state = spell_option.option_icon_state) + radial_items[spell_option.name] = radial_option + sort_list(radial_items) + + message_admins("click_alt PRE-RADIAL -
radial_items: [radial_items]
spell_options: [spell_options]") + for(var/radial_name as anything in radial_items) + message_admins("click_alt PRE-RADIAL-LOOP -
radial_name: [radial_name]
entry: [radial_items[radial_name]]") + + var/chosen_option = show_radial_menu(user, src, radial_items, custom_check = CALLBACK(src, PROC_REF(check_selection_menu), user), require_near = TRUE, tooltips = TRUE) + message_admins("click_alt POST RADIAL -
chosen_option: [chosen_option]") + if(isnull(chosen_option)) + return CLICK_ACTION_BLOCKING + var/datum/power/enigmatist_spell/chosen_spell = spell_options[chosen_option] + if(isnull(chosen_spell)) + return CLICK_ACTION_BLOCKING + + current_selected_power_ref = WEAKREF(chosen_spell) + chosen_spell.chalk_selected_spell(user, src) + balloon_alert(user, "spell selected") + return CLICK_ACTION_SUCCESS + +/obj/item/enigmatist_chalk/proc/check_selection_menu(mob/user) + if(QDELETED(src)) + return FALSE + if(!istype(user)) + return FALSE + if(user.incapacitated) + return FALSE + return TRUE + + +/// Gets the percentage of its maximum our current integrity is at. +/obj/item/enigmatist_chalk/proc/integrity_percent() + return PERCENT(resonant_integrity / max_resonant_integrity) + +/// Try to use a given amount of integrity. If we don't have enough, don't and return FALSE. +/obj/item/enigmatist_chalk/proc/use_integrity(damage, user) + if(damage > resonant_integrity) + return FALSE + resonant_integrity -= damage + if(resonant_integrity <= 0) + break_chalk(user) + return TRUE + +/// Breaks the chalk! Sends feedback if given a user. +/obj/item/enigmatist_chalk/proc/break_chalk(user) + if(user) + balloon_alert(user, "chalk shatters!") + // TODO: replace with remnants if possible. + // TODO: add sounds if possible. + qdel(src) + +/** + * Practical Chalk Items + */ + +/obj/item/enigmatist_chalk/resonant + name = "resonant chalk" + desc = "A stark-white stick of chalk. \ + Its texture shifts as you turn it." + icon_state = "crayonwhite" + enigmatist_flags = ENIGMATIST_RESONANT + +/obj/item/enigmatist_chalk/unsealed + name = "unsealed chalk" + desc = "A stick of chalk with an odd purple hue. \ + It doesn't obscure what's behind it." + icon_state = "crayonpurple" + enigmatist_flags = ENIGMATIST_UNSEALED + +/obj/item/enigmatist_chalk/illuminated + name = "illuminated chalk" + desc = "A stick of chalk with an odd yellow hue. \ + It seems well-lit regardless of lighting." + icon_state = "crayonyellow" + enigmatist_flags = ENIGMATIST_ILLUMINATED + +/obj/item/enigmatist_chalk/divided + name = "divided chalk" + desc = "A stick of chalk with an odd blue hue. \ + Its edges look sharp no matter the angle." + icon_state = "crayonblue" + enigmatist_flags = ENIGMATIST_DIVIDED + +/** + * Resonant Chalk + */ + +/datum/crafting_recipe/resonant_chalk + name = "Resonant Chalk" + result = /obj/item/enigmatist_chalk/resonant + reqs = list( + /obj/item/stack/sheet/mineral/plasma = 1, + /obj/item/toy/crayon = 1, + ) + blacklist = list( + /obj/item/toy/crayon/spraycan, + ) + time = 5 SECONDS + category = CAT_ENIGMATIST + crafting_flags = CRAFT_MUST_BE_LEARNED + +#undef CAT_ENIGMATIST diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm new file mode 100644 index 00000000000000..4c74ad6c908794 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm @@ -0,0 +1,18 @@ + +/datum/power/enigmatist_root + name = "Produce Resonant Chalk" + desc = "Learn how to produce Resonant Chalk with any crayon \ + and a sheet of plasma or Resonant Chalk Remnants. \ + This is mutually exclusive with Spell Preparation." + + value = 2 + mob_trait = TRAIT_ARCHETYPE_SORCEROUS + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_ENIGMATIST + priority = POWER_PRIORITY_ROOT + +/datum/power/enigmatist_root/add(client/client_source) + var/datum/action/cooldown/spell/touch/prestidigitation/that_magic_touch = new + that_magic_touch.Grant(power_holder) + + power_holder.mind?.teach_crafting_recipe(/datum/crafting_recipe/resonant_chalk) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_spell.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_spell.dm new file mode 100644 index 00000000000000..f25d2b0ab3a98f --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_spell.dm @@ -0,0 +1,83 @@ + +/datum/power/enigmatist_spell + name = "Abstract Enigmatist Spell" + desc = "The true art of seeing into the seventh dimension: \ + seeing this debug code. Please report this!" + abstract_parent_type = /datum/power/enigmatist_spell + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_ENIGMATIST + required_powers = list(/datum/power/enigmatist_root) + + /// The type of Enigmatist spell this is. Use ENIGMATIST_RESONANT/X flags. + var/enigmatist_type = ENIGMATIST_RESONANT + + /// The icon used for us in chalk radial selections. + var/option_icon = 'icons/obj/art/crayons.dmi' + /// The icon_state used for us in chalk radial selections. + var/option_icon_state = "crayonwhite" + /// The name to use in chalk radial selections instead of our basic name. + var/option_name + /// The description to use in chalk radial selections instead of our basic description. + var/option_desc + +/datum/power/enigmatist_spell/add(client/client_source) + RegisterSignal(power_holder, COMSIG_ENIGMATIST_CHALK_SELECTION, PROC_REF(get_spell_option)) + +/datum/power/enigmatist_spell/remove() + UnregisterSignal(power_holder, COMSIG_ENIGMATIST_CHALK_SELECTION) + + +/datum/power/enigmatist_spell/proc/get_option_name() + return option_name || name + +/datum/power/enigmatist_spell/proc/get_option_desc() + return option_desc || desc + +/datum/power/enigmatist_spell/proc/get_spell_option(datum/source, enigmatist_flags, list/spell_options) + SIGNAL_HANDLER + message_admins("get_spell_option -
enigmatist_type: [enigmatist_type]
enigmatist_flags: [enigmatist_flags]
both: [enigmatist_type & enigmatist_flags]") + if(enigmatist_type & enigmatist_flags) + spell_options[name] = src + message_admins("get_spell_option TWO -
spell_options length: [length(spell_options)]") + + +/datum/power/enigmatist_spell/proc/chalk_add_context( + obj/item/enigmatist_chalk/held_chalk, + list/context, + obj/item/held_item, + mob/user, +) + return NONE + +/datum/power/enigmatist_spell/proc/chalk_add_item_context( + obj/item/enigmatist_chalk/held_chalk, + list/context, + atom/target, + mob/living/user, +) + return NONE + + +/datum/power/enigmatist_spell/proc/chalk_selected_spell(mob/user, obj/item/enigmatist_chalk/used_chalk) + return + +/datum/power/enigmatist_spell/proc/chalk_reset_spell(mob/user, obj/item/enigmatist_chalk/used_chalk) + return + + +/datum/power/enigmatist_spell/proc/chalk_attack_self(obj/item/enigmatist_chalk/used_chalk, mob/user, modifiers) + return + +/datum/power/enigmatist_spell/proc/chalk_interact_with_atom(obj/item/enigmatist_chalk/used_chalk, atom/interacting_with, mob/living/user, list/modifiers) + return NONE + +/datum/power/enigmatist_spell/proc/chalk_interact_with_atom_secondary(obj/item/enigmatist_chalk/used_chalk, atom/interacting_with, mob/living/user, list/modifiers) + return chalk_interact_with_atom(used_chalk, interacting_with, user, modifiers) + + +/datum/power/enigmatist_spell/proc/damage_chalk(obj/item/enigmatist_chalk/used_chalk, mob/living/user, damage) + if(!used_chalk.use_integrity(damage, user)) + used_chalk.balloon_alert(user, "too damaged!") + return FALSE + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm new file mode 100644 index 00000000000000..a74f8f0d3fe347 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm @@ -0,0 +1,27 @@ + +/datum/power/enigmatist_spell/lodestone_legends + name = "Lodestone Legends" + desc = "Activate with any type of Chalk in hand to be told your GPS \ + position. Causes minor damage to the Chalk" + + value = 1 + priority = POWER_PRIORITY_BASIC + // Any chalk can use us, whatsoever. + enigmatist_type = ENIGMATIST_ANY_ALL + +/datum/power/enigmatist_spell/lodestone_legends/chalk_add_context( + obj/item/enigmatist_chalk/held_chalk, + list/context, + obj/item/held_item, + mob/user, +) + if(held_chalk != held_item) + return NONE + context[SCREENTIP_CONTEXT_LMB] = "Get GPS position" + return CONTEXTUAL_SCREENTIP_SET + +/datum/power/enigmatist_spell/lodestone_legends/chalk_attack_self(obj/item/enigmatist_chalk/used_chalk, mob/user, modifiers) + if(!damage_chalk(used_chalk, user, ENIGMATIST_CHALK_MINOR_DAMAGE)) + return + var/turf/current_turf = get_turf(used_chalk) + to_chat(user, span_notice("Your current coordinates are... [current_turf.x]x, [current_turf.y]y, [current_turf.z]z...")) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/prestidigitation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/prestidigitation.dm new file mode 100644 index 00000000000000..46155cc497965f --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/prestidigitation.dm @@ -0,0 +1,60 @@ + +/datum/action/cooldown/spell/touch/prestidigitation + name = "Prestidigitation" + desc = "Channel electricity to your hand to shock people with. Mostly harmless! Mostly... " + button_icon_state = "zap" + sound = 'sound/effects/magic/staff_healing.ogg' + cooldown_time = 7 SECONDS + invocation_type = INVOCATION_NONE + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + antimagic_flags = MAGIC_RESISTANCE + can_cast_on_self = TRUE + + hand_path = /obj/item/melee/touch_attack/prestidigitation + draw_message = span_notice("You channel resonance around your hand.") + drop_message = span_notice("You let the resonance around your hand dissipate.") + +/datum/action/cooldown/spell/touch/prestidigitation/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(!iscarbon(victim)) + return FALSE + victim.wash(CLEAN_SCRUB) + return TRUE + +/datum/action/cooldown/spell/touch/prestidigitation/cast_on_secondary_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) + if(!iscarbon(victim)) + return FALSE + var/obj/item/cigarette/cig = attached_hand.help_light_cig(victim) + if(isnull(cig)) + return FALSE + cig.light("With a single flick of [caster.p_their()] wrist, [caster] smoothly lights [caster == victim ? caster.p_their() : "[victim]'s"] [cig]. Damn [caster.p_theyre()] cool.") + return TRUE + +/obj/item/melee/touch_attack/prestidigitation + name = "\improper prestidigitation" + desc = "This is kind of like when you rub your feet on a shag rug so you can zap your friends, only a lot less safe." + icon = 'icons/obj/weapons/hand.dmi' + icon_state = "zapper" + inhand_icon_state = "zapper" + + // I can light my candles *from a distance*. + reach = 2 + // Doesn't need a permit, as opposed to other touch attacks. + item_flags = ABSTRACT | HAND_ITEM + // Allow you to light candles with this. + heat = HIGH_TEMPERATURE_REQUIRED - 100 + /// Sparks effect for special effects. + var/datum/effect_system/spark_spread/sparks + +/obj/item/melee/touch_attack/prestidigitation/Initialize(mapload) + . = ..() + sparks = new + sparks.set_up(2, 0, src) + sparks.attach(src) + +/obj/item/melee/touch_attack/prestidigitation/ignition_effect(atom/atom, mob/user) + if(!get_temperature()) + return + return span_infoplain(span_rose("With a single flick of [user.p_their()] wrist, [user] smoothly lights [atom]. Damn [user.p_theyre()] cool.")) + +/obj/item/melee/touch_attack/prestidigitation/attack_self(mob/user) + sparks.start() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm new file mode 100644 index 00000000000000..80e3c1b16d628e --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm @@ -0,0 +1,29 @@ + +/datum/power/item_power/thaumaturge_root + name = "Spell Preparation" + desc = "Wizards, sorcerers, sages. These are all Thaumaturges, who channel the Resonant song \ + with their bodies and their words, whether they discover these through careful study or \ + strong intuition." + + value = 5 + mob_trait = TRAIT_ARCHETYPE_SORCEROUS + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THAUMATURGE + priority = POWER_PRIORITY_ROOT + +/datum/power/item_power/thaumaturge_root/add_unique(client/client_source) + var/obj/item/book/random/spellbook = new(get_turf(power_holder)) + spellbook.name = "[power_holder.real_name]'s spellbook" + give_item_to_holder(spellbook, list(LOCATION_BACKPACK, LOCATION_HANDS)) + +/datum/power/item_power/thaumaturge_root/add(client/client_source) + var/datum/action/cooldown/spell/touch/prestidigitation/that_magic_touch = new + that_magic_touch.Grant(power_holder) + + + + + + + + diff --git a/modular_doppler/modular_powers/code/powers_helpers.dm b/modular_doppler/modular_powers/code/powers_helpers.dm new file mode 100644 index 00000000000000..c9be7846b9ef6e --- /dev/null +++ b/modular_doppler/modular_powers/code/powers_helpers.dm @@ -0,0 +1,14 @@ + +/proc/cmp_powers_asc(datum/power/first_power, datum/power/second_power) + var/first_priority_val = SSpowers.power_priorities.Find(first_power.priority) + var/second_priority_val = SSpowers.power_priorities.Find(second_power.priority) + var/a_sign = SIGN(first_priority_val) + var/b_sign = SIGN(second_priority_val) + + var/a_name = first_power::name + var/b_name = second_power::name + + if(a_sign != b_sign) + return a_sign - b_sign + else + return sorttext(b_name, a_name) \ No newline at end of file diff --git a/modular_doppler/modular_powers/code/powers_living.dm b/modular_doppler/modular_powers/code/powers_living.dm new file mode 100644 index 00000000000000..6b744f8f221283 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers_living.dm @@ -0,0 +1,67 @@ + +/** + * All the additional procs/vars we need on /mob/living for powers to function. + */ + +/mob/living + /// List of all powers we currently have. + var/list/powers = list() + +/** + * Adds the passed power to the mob + * + * Arguments + * * power_type - Power typepath to add to the mob + * If not passed, defaults to this mob's client. + * + * Returns TRUE on success, FALSE on failure (already has the power, etc) + */ +/mob/living/proc/add_archetype_power(datum/power/power_type, client/override_client, add_unique = TRUE) + if(has_archetype_power(power_type)) + return FALSE + var/qname = initial(power_type.name) + if(!SSpowers || !SSpowers.powers[qname]) + return FALSE + var/datum/power/new_power = new power_type() + if(new_power.add_to_holder(new_holder = src, client_source = override_client, unique = add_unique)) + return TRUE + qdel(new_power) + return FALSE + +/mob/living/proc/remove_archetype_power(power_type) + for(var/datum/power/power in powers) + if(power.type == power_type) + qdel(power) + return TRUE + return FALSE + +/mob/living/proc/has_archetype_power(power_type) + for(var/datum/power/power in powers) + if(power.type == power_type) + return TRUE + return FALSE + +/** + * Getter function for a mob's power + * + * Arguments: + * * power_type - the type of the power to acquire e.g. /datum/power/some_power + * + * Returns the mob's power datum if the mob this is called on has the power, null on failure + */ +/mob/living/proc/get_power(power_type) + for(var/datum/power/power in powers) + if(power.type == power_type) + return power + return null + +/mob/living/proc/cleanse_power_datums() + QDEL_LIST(powers) + +/mob/living/proc/transfer_power_datums(mob/living/to_mob) + // We could be done before the client was moved or after the client was moved + var/datum/preferences/to_pass = client || to_mob.client + + for(var/datum/power/power as anything in powers) + power.remove_from_current_holder(power_transfer = TRUE) + power.add_to_holder(to_mob, power_transfer = TRUE, client_source = to_pass) diff --git a/modular_doppler/modular_powers/code/powers_prefs.dm b/modular_doppler/modular_powers/code/powers_prefs.dm new file mode 100644 index 00000000000000..0149618d58912a --- /dev/null +++ b/modular_doppler/modular_powers/code/powers_prefs.dm @@ -0,0 +1,15 @@ + +/** + * All the additional procs/vars we need on /datum/preferences for powers to function. + */ + +/datum/preferences + /// List of all our powers, by name. + var/list/all_powers = list() + +/datum/preferences/proc/sanitize_powers() + // TODO: implement some way for this to give FEEDBACK to the player about what got kerploded + var/list/new_powers = SSpowers.filter_invalid_powers(all_powers) + if(length(new_powers) != length(all_powers)) + return TRUE + return FALSE diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm new file mode 100644 index 00000000000000..5d814621f16ea6 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -0,0 +1,255 @@ + +/** + * This place is a message... and part of a system of messages... pay attention to it! + * Sending this message was important to us. We considered ourselves to be a powerful culture. + * This place is not a place of honor... no highly esteemed deed is commemorated here... nothing valued is here. + * What is here was dangerous and repulsive to us. This message is a warning about danger. + * The danger is in a particular location... it increases towards a center... the center of danger is here... of a particular size and shape, and below us. + * The danger is still present, in your time, as it was in ours. + * The danger is to the body, and it can kill. + * The form of the danger is an emanation of energy. + * The danger is unleashed only if you substantially disturb this place physically. This place is best shunned and left uninhabited. + */ + +/datum/preference_middleware/powers + action_delegations = list( + "give_power" = PROC_REF(give_power), + "remove_power" = PROC_REF(remove_power), + ) + +/datum/preference_middleware/powers/get_ui_data(mob/user) + var/list/data = list() + + var/list/thaumaturge = list() + var/list/enigmatist = list() + var/list/theologist = list() + + var/list/psyker = list() + var/list/cultivator = list() + var/list/aberrant = list() + + var/list/warfighter = list() + var/list/expert = list() + var/list/augmented = list() + + var/current_points = 0 + for(var/power_name in preferences.all_powers) + var/datum/power/power_type = SSpowers.powers[power_name] + current_points += power_type.value + + for(var/power_name in SSpowers.powers) + var/datum/power/power_type = SSpowers.powers[power_name] + + var/has_given_power = (power_name in preferences.all_powers) + + // TODO: GRAY OUT powers you: + // Don't have the requirements for. + // Have powers building upon. + // Have an incompatible power for. + // ^ must touch tgui to set a new state/colour for this shit + + var/locked_in = FALSE + if(has_given_power) + if(get_requiring_power(power_type)) + locked_in = TRUE + else + if(get_incompatible_power(power_type) || get_required_power(power_type)) + locked_in = TRUE + + var/state + var/word + var/color + var/powertype + var/rootpower = null + + if(power_type.priority == POWER_PRIORITY_ROOT) + powertype = "crown" + else + powertype = "" + rootpower = power_type.archetype + + if(has_given_power) + word = "Forget" + state = "bad" + if(locked_in) + color = "0.5" + else + if(locked_in || ((power_type.value + current_points) > MAXIMUM_POWER_POINTS)) + state = "transparent" + word = "N/A" + color = "0.5" + else + state = "good" + word = "Learn" + color = "1" + + var/final_list = list(list( + "description" = power_type.desc, + "name" = power_type.name, + "cost" = power_type.value, + "state" = state, + "word" = word, + "color" = color, + "powertype" = powertype, + "rootpower" = rootpower, + )) + + switch(power_type.path) + if(POWER_PATH_THAUMATURGE) + thaumaturge += final_list + if(POWER_PATH_ENIGMATIST) + enigmatist += final_list + if(POWER_PATH_THEOLOGIST) + theologist += final_list + if(POWER_PATH_PSYKER) + psyker += final_list + if(POWER_PATH_CULTIVATOR) + cultivator += final_list + if(POWER_PATH_ABERRANT) + aberrant += final_list + if(POWER_PATH_WARFIGHTER) + warfighter += final_list + if(POWER_PATH_EXPERT) + expert += final_list + if(POWER_PATH_AUGMENTED) + augmented += final_list + + + data["total_power_points"] = MAXIMUM_POWER_POINTS + data["thaumaturge"] = thaumaturge + data["enigmatist"] = enigmatist + data["theologist"] = theologist + data["psyker"] = psyker + data["cultivator"] = cultivator + data["aberrant"] = aberrant + data["warfighter"] = warfighter + data["expert"] = expert + data["augmented"] = augmented + data["power_points"] = current_points + + return data + +/** + * Gives a power to a character using the params list provided by tgui. + * Runs through multiple checks to ensure that the power can be learned. + */ +/datum/preference_middleware/powers/proc/give_power(list/params, mob/user) + var/power_name = params["power_name"] + var/datum/power/power_type = SSpowers.powers[power_name] + if(isnull(preferences.all_powers)) + preferences.all_powers = list() + + if(isnull(power_type)) + return FALSE // Not a power. + + if(power_name in preferences.all_powers) + return FALSE // Already have this power. + + // Make sure we stay in the same archetype. + if(length(preferences.all_powers)) + var/datum/power/first_power_type = SSpowers.powers[preferences.all_powers[1]] + if(power_type.archetype != first_power_type.archetype) + to_chat(user, span_boldwarning("Mismatched archetype!")) + return FALSE + + // Make sure we have the required powers. + var/datum/power/required_power_type = get_required_power(power_type) + if(required_power_type) + to_chat(user, span_boldwarning("[power_name] is missing [required_power_type.name]!")) + return FALSE + + // Make sure we don't select an incompatible power. + var/datum/power/incompatible_power_type = get_incompatible_power(power_type) + message_admins("giver_power BLACKLIST -
incompatible_power_type: [incompatible_power_type]") + if(incompatible_power_type) + to_chat(user, span_boldwarning("[power_name] is incompatible with [incompatible_power_type.name]!")) + return FALSE + + // Make sure we don't go over our point cap. + var/point_balance = power_type.value + for(var/existing_power_name in preferences.all_powers) + var/datum/power/existing_power_type = SSpowers.powers[existing_power_name] + point_balance += existing_power_type.value + if(point_balance > MAXIMUM_POWER_POINTS) + to_chat(user, span_boldwarning("[power_name] costs too much!")) + return FALSE + + preferences.all_powers += power_name + return TRUE + +/** + * Remove Power + * + * Removes a power from a character using the params list provided by tgui. + */ +/datum/preference_middleware/powers/proc/remove_power(list/params, mob/user) + var/power_name = params["power_name"] + var/datum/power/power_type = SSpowers.powers[power_name] + if(isnull(preferences.all_powers)) + preferences.all_powers = list() + return FALSE // We don't have any powers. + + if(isnull(power_type)) + return FALSE // Not a power. + + if(!(power_name in preferences.all_powers)) + return FALSE // We don't have this power. + + // Make sure none of our other powers need this power. + + var/datum/power/requiring_power_type = get_requiring_power(power_type) + if(requiring_power_type) + to_chat(user, span_boldwarning("[power_name] is needed by [requiring_power_type.name]!")) + return FALSE + + preferences.all_powers -= power_name + return TRUE + +/** + * Checks whether we are missing at least one required power for a given power type, + * and returns the first one encountered if so. + */ +/datum/preference_middleware/powers/proc/get_required_power(datum/power/power_type) + var/list/required_powers = GLOB.powers_requirements_list[power_type] + if(!length(required_powers)) + return + for(var/datum/power/required_power_type as anything in required_powers) + var/required_power_name = required_power_type.name + if(!(required_power_name in preferences.all_powers)) + return required_power_type + +/** + * Checks whether at least one of our powers requires the given power type, + * and returns the first one encountered if so. + */ +/datum/preference_middleware/powers/proc/get_requiring_power(datum/power/power_type) + var/list/powers_requiring_this = GLOB.powers_inverse_requirements_list[power_type] + if(!length(powers_requiring_this)) + return + for(var/datum/power/requiring_power_type as anything in powers_requiring_this) + if(requiring_power_type.name in preferences.all_powers) + return requiring_power_type + +/** + * Checks whether a given power type is incompatible with our selected powers, + * and returns the first one encountered if so. + */ +/datum/preference_middleware/powers/proc/get_incompatible_power(datum/power/power_type) + for(var/list/blacklist as anything in GLOB.powers_blacklist) + if(!(power_type in blacklist)) + continue + for(var/datum/power/other_power_type as anything in blacklist) + if(other_power_type.name in preferences.all_powers) + return other_power_type + +/datum/asset/simple/powers + assets = list( + "gear.png" = 'modular_doppler/modular_powers/icons/ui/powers/gear.png', + "heart.png" = 'modular_doppler/modular_powers/icons/ui/powers/heart.png', + "seal.png" = 'modular_doppler/modular_powers/icons/ui/powers/seal.png' + ) + +/datum/preference_middleware/powers/get_ui_assets() + return list( + get_asset_datum(/datum/asset/simple/powers), + ) diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm new file mode 100644 index 00000000000000..40ba1b5e7e0c62 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -0,0 +1,222 @@ + +// Both of these lists are shifted to glob so they are generated at world start instead of risking players doing preference stuff before the subsystem inits. +GLOBAL_LIST_INIT_TYPED(powers_blacklist, /list/datum/power, list( + list(/datum/power/item_power/thaumaturge_root, /datum/power/enigmatist_root) +)) + +GLOBAL_LIST_INIT(powers_requirements_list, generate_powers_requirements_list()) + +GLOBAL_LIST_INIT(powers_inverse_requirements_list, generate_powers_inverse_requirements_list()) + +/proc/generate_powers_requirements_list() + var/list/requirements_list = list() + var/list/all_powers_list = subtypesof(/datum/power) + + for(var/datum/power/power_type as anything in all_powers_list) + if(power_type.abstract_parent_type == power_type) + continue + var/datum/power/power_instance = new power_type + if(!length(power_instance.required_powers)) + continue + for(var/datum/power/required_power_type as anything in power_instance.required_powers) + LAZYADDASSOCLIST(requirements_list, power_type, required_power_type) + qdel(power_instance) + + return requirements_list + +/proc/generate_powers_inverse_requirements_list() + var/list/inverse_requirements_list = list() + var/list/all_powers_list = subtypesof(/datum/power) + + for(var/datum/power/power_type as anything in all_powers_list) + if(power_type.abstract_parent_type == power_type) + continue + var/datum/power/power_instance = new power_type + if(!length(power_instance.required_powers)) + continue + for(var/datum/power/required_power_type as anything in power_instance.required_powers) + LAZYADDASSOCLIST(inverse_requirements_list, required_power_type, power_type) + qdel(power_instance) + + return inverse_requirements_list + + +//Used to process and handle roundstart powers +// - Power strings are used for faster checking in code +// - Power datums are stored and hold different effects, as well as being a vector for applying trait string +PROCESSING_SUBSYSTEM_DEF(powers) + name = "Powers" + flags = SS_BACKGROUND + runlevels = RUNLEVEL_GAME + wait = 1 SECONDS + + /// Assoc. list of all roundstart power datum types; "name" = /path/ + var/list/powers = list() + /// List of all power priorities in order. + var/list/power_priorities = list( + POWER_PRIORITY_ROOT, + POWER_PRIORITY_BASIC, + POWER_PRIORITY_ADVANCED, + ) + /// Assoc. list of all mutually exclusive power paths. // TODO: NO LONGER TRUE + var/static/list/power_paths = list( + POWER_ARCHETYPE_SORCEROUS = list( + POWER_PATH_THAUMATURGE, + POWER_PATH_ENIGMATIST, + POWER_PATH_THEOLOGIST, + ), + POWER_ARCHETYPE_RESONANT = list(), + POWER_ARCHETYPE_MORTAL = list(), + ) + +/datum/controller/subsystem/processing/powers/Initialize() + get_powers() + return SS_INIT_SUCCESS + +/// Returns the list of possible powers +/datum/controller/subsystem/processing/powers/proc/get_powers() + RETURN_TYPE(/list) + if(!powers.len) + setup_powers() + + return powers + +/datum/controller/subsystem/processing/powers/proc/setup_powers() + // Sort by priority from Root to Advanced, and then by name + var/list/powers_list = sort_list(subtypesof(/datum/power), GLOBAL_PROC_REF(cmp_powers_asc)) + + for(var/datum/power/power_type as anything in powers_list) + if(initial(power_type.abstract_parent_type) == power_type) + continue + powers[initial(power_type.name)] = power_type + +/datum/controller/subsystem/processing/powers/proc/assign_powers(mob/living/user, client/applied_client) + var/bad_power = FALSE + var/list/powers_by_priority = list() + for(var/power_name in applied_client.prefs.all_powers) + var/datum/power/power_type = powers[power_name] + if(!ispath(power_type)) + stack_trace("Invalid power \"[power_name]\" in client [applied_client.ckey] preferences") + applied_client.prefs.all_powers -= power_name + bad_power = TRUE + continue + if(!power_type.priority) + stack_trace("Power with invalid priority \"[power_name]\" in client [applied_client.ckey] preferences") + applied_client.prefs.all_powers -= power_name + bad_power = TRUE + continue + LAZYADDASSOCLIST(powers_by_priority, power_type.priority, power_type) + + message_admins("assign_powers FIRST -
powers_by_priority: [powers_by_priority]
bad_power: [bad_power]") + + if(bad_power) + applied_client.prefs.save_character() + + message_admins("assign_powers SECOND -
power_priorities: [power_priorities]") + + for(var/power_priority in power_priorities) + var/list/priority_powers = powers_by_priority[power_priority] + message_admins("assign_powers THIRD(LOOP) -
power_priority: [power_priority]
priority_powers: [priority_powers]") + if(isnull(priority_powers)) + continue + for(var/whatever in priority_powers) + message_admins("assign_powers 3-4(LOOP) -
whatever: [whatever]") + for(var/datum/power/power_type as anything in priority_powers) + message_admins("assign_powers FOURTH(LOOP) -
power_type: [power_type]
priority_powers: [priority_powers]") + if(!user.add_archetype_power(power_type, override_client = applied_client)) + continue + SSblackbox.record_feedback("tally", "powers_taken", 1, "[power_type.name]") + +/// Takes a list of power names, +/// and returns a new list of powers that would be valid. +/// If no changes need to be made, will return the same list. +/// Expects all power names to be unique, but makes no other expectations. +/datum/controller/subsystem/processing/powers/proc/filter_invalid_powers(list/powers_to_check) + var/current_balance = 0 + var/current_archetype + var/list/intermediary_powers = list() + + var/maximum_balance = MAXIMUM_POWER_POINTS + var/list/all_powers = get_powers() + + // TODO: work out how to filter powers missing their requirements. + // This could be higher priorities, but could also be at the same priority level. + // TODO: work out how to filter for going over the balance cap without introducing major issues. + // Like ideally we remove the advanced ones first. + // Maybe we just make a web of requirements at init? + // Maybe we can substitute priorities with this web...? + + // Validate whether directed graph is connected! + // Construct https://www.geeksforgeeks.org/dsa/check-if-a-directed-graph-is-connected-or-not/ + // pick a random power, then do depth first search both ways + // Do the same thing when picking a thing to remove for balance stuff + // Fuck we can have multiple root powers. + + // Okay so, collect root powers on our first go through. + // THEN depth first search from each root power. + // Then copy over all the seen ones that have their requirements. + // FUCK what if there's a power that depends on two root powers. + + // First discard multiple base paths and incompatible powers + for(var/power_name in powers_to_check) + var/datum/power/power_type = all_powers[power_name] + if(!ispath(power_type)) + continue + + // Make sure we only have one overarching archetype. + if(isnull(current_archetype)) + current_archetype = power_type.archetype + else if(current_archetype != power_type.archetype) + continue // Mismatched archetype, discard. + + // Make sure we don't have incompatible powers + var/blacklisted = FALSE + for(var/list/blacklist as anything in GLOB.powers_blacklist) + if(!(power_type in blacklist)) + continue + for(var/other_power in blacklist) + if(other_power in intermediary_powers) + blacklisted = TRUE + break + if(blacklisted) + break + if(blacklisted) + continue // Incompatible, discard. + + // TODO: remove this and finish stuff + intermediary_powers += power_type.name + + if(intermediary_powers.len == powers_to_check.len) + return powers_to_check + + return intermediary_powers + + /** TODO: ALL THE REST OF THIS + + var/value = initial(power_type.value) + if(value > 0) + if (max_positive_quirks >= 0 && positive_quirks.len == max_positive_quirks) + continue + + positive_quirks[quirk_name] = value + + current_balance += value + new_powers += quirk_name + + if (points_enabled && balance > 0) + var/balance_left_to_remove = balance + + for (var/positive_quirk in positive_quirks) + var/value = positive_quirks[positive_quirk] + balance_left_to_remove -= value + new_quirks -= positive_quirk + + if (balance_left_to_remove <= 0) + break + + // It is guaranteed that if no quirks are invalid, you can simply check through `==` + if (new_quirks.len == quirks.len) + return quirks + + return new_quirks + */ diff --git a/modular_doppler/modular_powers/powers/_powers.dm b/modular_doppler/modular_powers/powers/_powers.dm deleted file mode 100644 index 754df9e42dde2e..00000000000000 --- a/modular_doppler/modular_powers/powers/_powers.dm +++ /dev/null @@ -1,169 +0,0 @@ -/mob/living - var/list/all_powers = list() - -/** - * Power Handler - * - * Ensures that all powers are properly applied when a cremember spawns in. - */ -GLOBAL_DATUM_INIT(power_handler, /datum/power_handler, new) - -/obj/item/organ/resonant/ - slot = ORGAN_SLOT_RESONANT - -/datum/power_handler/New() - RegisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED, PROC_REF(handle_new_player)) - - -/datum/power_handler/proc/handle_new_player(datum/source, mob/living/carbon/human/new_crewmember, rank) - SIGNAL_HANDLER - - // sanity checking because we really do not want to be causing any runtimes - if(!istype(new_crewmember)) - return - if(isnull(new_crewmember.mind)) - return - - var/datum/preferences/prefs = new_crewmember.client?.prefs - - if(isnull(prefs)) - return - - apply_powers(new_crewmember, prefs) - -/datum/power_handler/proc/apply_powers(mob/living/carbon/human/target, datum/preferences/preferences, visuals_only = FALSE) - var/list/power_types = list() - - for(var/power_name in preferences.powers) - var/datum/power/power_to_add = preferences.powers[power_name] - power_to_add = new power_to_add() - power_to_add.apply_to_human(target) - var/core_power_type = get_path_type(power_to_add.power_type) - - if(core_power_type && !(core_power_type in power_types)) - power_types += core_power_type - - qdel(power_to_add) - - for(var/core_power in power_types) - var/datum/power/powah_to_add = GLOB.path_core_powers[core_power] - powah_to_add = new powah_to_add() - powah_to_add.apply_to_human(target) - qdel(powah_to_add) - -/datum/power_handler/Destroy() - ..() - UnregisterSignal(SSdcs, COMSIG_GLOB_CREWMEMBER_JOINED) - -/** - * Power datum. Used to contain and handle all information required for both TGUI and applying powers to a player. - */ - -/datum/power - - var/name - - var/desc - - // The relevant cost of the power in question. Must be an integer, not a string. - var/cost - - // The path subtype this power falls under. Is also a trait. - var/power_type - - // Whether or not the power is advanced, meaning if it can be taken with powers from other - var/advanced = FALSE - - // Traits to be added when a power is applied to a mob. - var/list/power_traits = list() - - // The power's root power. If the power is a root power, this should be the power datum itself, otherwise it should be it's respective root power's datum. - var/datum/power/root_power - - // A list of power datums that CANNOT be taken alongside this power. This only checks if the blacklist variable is true, so all power's must be vice versa added to their respective blacklists. - var/list/blacklist = list() - - // This value determines whether or not a power is initalized in the global list of powers used for the tgui menu. ONLY core powers should have this variable set to true. - var/is_accessible = TRUE - - // A string that is send to the user's chat when they gain this power. - var/gain_text - - // A list of power datums that MUST be taken for this power to be available. - var/list/required_powers = list() - - -/** - * Apply To Human. - * - * The initial checks ran when a power is added. Makes sure the target is valid and does not already have said power, before adding the relevant traits, displaying gain text and then running the power's add proc. - */ -/datum/power/proc/apply_to_human(mob/living/carbon/human/target) - if(!target) - CRASH("Power attempted to be added to null mob.") - - if(target.has_powerz(type)) - CRASH("Power attempted to be added to mob which already has this power.") - - target.all_powers += src - - if(power_traits) - for(var/add_trait in power_traits) - ADD_TRAIT(target, add_trait, TRAIT_POWER) - - if(gain_text) - to_chat(target, gain_text) - - ADD_TRAIT(target, power_type, TRAIT_POWER) - - ADD_TRAIT(target, get_path_type(power_type), TRAIT_POWER) - - add(target) - -/** - * Checks if a mob already has the provided power. - */ -/mob/living/proc/has_powerz(power_type) - - for(var/datum/power/power in all_powers) - - if(power.type == power_type) - return TRUE - - return FALSE - -/** - * Proc ran whenever a power is added to a mob. Should be used for unique effects that cannot be easily automated, such as organ insertion and action learning. - */ -/datum/power/proc/add(mob/living/carbon/human/target) - return - -/** - * Item power. Used to grant an item, wherein give_item_to_holder() should be added to the end of it's add proc. - */ - -/datum/power/item - var/list/where_items_spawned - - var/open_backpack - - -/datum/power/item/proc/give_item_to_holder(mob/living/carbon/human/target, obj/item/power_item, list/valid_slots, flavour_text = null, default_location = "at your feet", notify_player = TRUE) - if(ispath(power_item)) - power_item = new power_item(get_turf(target)) - - var/where = target.equip_in_one_of_slots(power_item, valid_slots, qdel_on_fail = FALSE, indirect_action = TRUE) || default_location - - if(where == LOCATION_BACKPACK) - open_backpack = TRUE - - if(notify_player) - LAZYADD(where_items_spawned, span_boldnotice("You have \a [power_item] [where]. [flavour_text]")) - - if(open_backpack && target.back) - target.back.atom_storage.show_contents(target) - - for(var/chat_string in where_items_spawned) - to_chat(target, chat_string) - - where_items_spawned = null diff --git a/modular_doppler/modular_powers/powers/core_powers.dm b/modular_doppler/modular_powers/powers/core_powers.dm deleted file mode 100644 index 30d6175cd89092..00000000000000 --- a/modular_doppler/modular_powers/powers/core_powers.dm +++ /dev/null @@ -1,62 +0,0 @@ -// Mortal - -/datum/power/tenacious - name = "Tenacious" - desc = "Try to remember some of the basics of CQC." - is_accessible = FALSE - power_traits = list(TRAIT_POWER_TENACIOUS) - -// Sorcerous - -/datum/power/prestidigitation - name = "Prestidigitation" - desc = "Allows a Sorcerous individual to perform magical tricks" - root_power = /datum/power/prestidigitation - power_type = TRAIT_PATH_SUBTYPE_THAUMATURGE - is_accessible = FALSE - -/datum/power/prestidigitation/add(mob/living/carbon/human/target) - var/datum/action/new_action = new /datum/action/cooldown/spell/prestidigitation(target.mind || target) - new_action.Grant(target) - -/datum/action/cooldown/spell/prestidigitation - name = "Prestidigitation" - desc = "The knowledge required to perform a variety of magical tricks." - button_icon_state = "arcane_barrage" - - school = SCHOOL_CONJURATION - cooldown_time = 12 SECONDS - cooldown_reduction_per_rank = 2.5 SECONDS - spell_requirements = NONE - - invocation_type = INVOCATION_EMOTE - - invocation = "Someone starts performing magic tricks!" - invocation_self_message = "You start performing magic tricks." - -// Resonant - -/datum/power/meditate - name = "Meditate" - desc = "ooughhh im meditating" - is_accessible = FALSE - power_type = TRAIT_PATH_SUBTYPE_PSYKER - -/datum/power/meditate/add(mob/living/carbon/human/target) - var/datum/action/new_action = new /datum/action/cooldown/spell/meditate(target.mind || target) - new_action.Grant(target) - -/datum/action/cooldown/spell/meditate - name = "Meditate" - desc = "This state of internal focus allows them to replenish any reserves they have and purge any impurities dredged up by abusing Nature's law." - button_icon_state = "nose" - - school = SCHOOL_CONJURATION - cooldown_time = 12 SECONDS - cooldown_reduction_per_rank = 2.5 SECONDS - spell_requirements = NONE - - invocation_type = INVOCATION_EMOTE - - invocation = "Someone starts meditating." - invocation_self_message = "You start meditating" diff --git a/modular_doppler/modular_powers/powers/mortal_powers/augmented.dm b/modular_doppler/modular_powers/powers/mortal_powers/augmented.dm deleted file mode 100644 index 0312067e21b43f..00000000000000 --- a/modular_doppler/modular_powers/powers/mortal_powers/augmented.dm +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Root powers - */ - -/datum/power/spinal_tap - name = "Spinal CNS Tap" - desc = "Vulnerable to EMPs. Acts as a frame for other augments." - cost = 6 - root_power = /datum/power/spinal_tap - power_type = TRAIT_PATH_SUBTYPE_AUGMENTED - -/datum/power/titanium - name = "Titanium Endoskeleton" - desc = "Makes you more resistant to bone wounds and better at tackling. Acts as a frame for other augments." - cost = 6 - root_power = /datum/power/titanium - power_type = TRAIT_PATH_SUBTYPE_AUGMENTED - -/datum/power/pneumatic - name = "Pneumatic Reservoir" - desc = "Vulnerable to EMPs. Acts as a frame for other augments." - cost = 6 - root_power = /datum/power/pneumatic - power_type = TRAIT_PATH_SUBTYPE_AUGMENTED diff --git a/modular_doppler/modular_powers/powers/mortal_powers/expert.dm b/modular_doppler/modular_powers/powers/mortal_powers/expert.dm deleted file mode 100644 index d70106d5066858..00000000000000 --- a/modular_doppler/modular_powers/powers/mortal_powers/expert.dm +++ /dev/null @@ -1,56 +0,0 @@ -/** - * Root Powers - */ -/datum/power/medical - name = "Medical Specialty" - desc = "Your specialty lies in caring for the wounded. Applying sutures and other similar items is faster." - cost = 6 - root_power = /datum/power/medical - power_type = TRAIT_PATH_SUBTYPE_EXPERT - power_traits = list(TRAIT_POWER_MEDICAL) - -/datum/power/engineering - name = "Engineering Specialty" - desc = "Your specialty lies in construction and deconstruction. You're slightly faster at using all tools." - cost = 6 - root_power = /datum/power/engineering - power_type = TRAIT_PATH_SUBTYPE_EXPERT - power_traits = list(TRAIT_POWER_ENGINEERING) - -/datum/power/service - name = "Service Speciality" - desc = "Your speciality lies in supporting the station." - cost = 6 - root_power = /datum/power/service - power_type = TRAIT_PATH_SUBTYPE_EXPERT - power_traits = list(TRAIT_POWER_SERVICE) - -/** - * Basic powers - */ -/datum/power/seasoned_chef - name = "Seasoned Chef" - desc = "You are a seasoned chef." - cost = 4 - root_power = /datum/power/service - power_type = TRAIT_PATH_SUBTYPE_EXPERT - -/datum/power/green_thumb - name = "Green Thumb" - desc = "You are a green thumb." - cost = 2 - root_power = /datum/power/service - power_type = TRAIT_PATH_SUBTYPE_EXPERT - -/** - * Advanced powers - */ - -/datum/power/master_chef - name = "Master Chef" - desc = "You are a master chef. Requires Green Thumb and Master Chef" - cost = 8 - root_power = /datum/power/service - power_type = TRAIT_PATH_SUBTYPE_EXPERT - advanced = TRUE - required_powers = list(/datum/power/green_thumb, /datum/power/seasoned_chef) diff --git a/modular_doppler/modular_powers/powers/mortal_powers/warfighter.dm b/modular_doppler/modular_powers/powers/mortal_powers/warfighter.dm deleted file mode 100644 index b8732b8c92953e..00000000000000 --- a/modular_doppler/modular_powers/powers/mortal_powers/warfighter.dm +++ /dev/null @@ -1,62 +0,0 @@ -#define MARTIALART_MARTIALART "martialart" -#define MARTIALART_CQB 'cqb"' - -/** - * Root powers - */ - -/datum/martial_art/martialart - name = "Martial Art" - id = MARTIALART_MARTIALART - -/datum/power/martialart - name = "Martial Art" - desc = "While not as advanced as the Resonant arts of Cultivators, with enough training, anyone can pack a punch. \ - This style boosts melee damage and lets the user block unarmed attacks by enabling throw mode." - cost = 6 - root_power = /datum/power/martialart - power_type = TRAIT_PATH_SUBTYPE_WARFIGHTER - -/datum/power/martialart/add(mob/living/carbon/human/target) - var/datum/martial_art/martial_to_learn = new /datum/martial_art/martialart() - if(!martial_to_learn.teach(target)) - to_chat(target, span_warning("You attempt to learn [martial_to_learn.name],\ - but your current knowledge of martial arts conflicts with the new style, so it just doesn't stick with you.")) - -/datum/power/cqb - name = "CQB" - desc = "Carbines, shotguns, and pistols. CQB is used in boarding actions or room clearing: as a result of their training, \ - users of CQB do significantly more damage when melee-attacking with firearms; e.x., pistolwhipping." - cost = 6 - root_power = /datum/power/cqb - power_type = TRAIT_PATH_SUBTYPE_WARFIGHTER - power_traits = list(TRAIT_POWER_CQB) - -/datum/power/precision_killer - name = "Precision Killer" - desc = "Snipers and their spotters. Most people who have fought these individuals do not know who killed them. \ - After being scoped in for four seconds, users of this style deal ten extra damage." - cost = 6 - root_power = /datum/power/precision_killer - power_type = TRAIT_PATH_SUBTYPE_WARFIGHTER - power_traits = list(TRAIT_POWER_SNIPER) - -/datum/power/leadership - name = "Leadership" - desc = "Expressed in many ways, from an iron fist to selfless responsibility. Grants the Designate Ally ability, \ - which lets you select up to 3 people as allies. Helping your allies (shaking them to their feet, CPR, fireman carrying) \ - is faster, to include your allies helping each other or you." - cost = 3 - root_power = /datum/power/leadership - power_type = TRAIT_PATH_SUBTYPE_WARFIGHTER - -/datum/power/leadership/add(mob/living/carbon/human/target) - var/datum/action/cooldown/mob_cooldown/designate_ally/designate = new(src) - designate.Grant(target) - -/datum/action/cooldown/mob_cooldown/designate_ally - name = "Designate Ally" - button_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "rcl_gui" - desc = "Some time in the future, this might let you designate allies. Maybe" - cooldown_time = 10 SECONDS diff --git a/modular_doppler/modular_powers/powers/resonant_powers/aberrant.dm b/modular_doppler/modular_powers/powers/resonant_powers/aberrant.dm deleted file mode 100644 index d7e01d0ec55d43..00000000000000 --- a/modular_doppler/modular_powers/powers/resonant_powers/aberrant.dm +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Root powers - */ - -/datum/power/cuprous_heart - name = "Cuprous Heart" - desc = "Your heart and blood are Living Copper. Your wounds and injuries naturally seal themselves, and you're resistant \ - to Resonant effects, but your blood does not replenish naturally and is hard to synthesize." - cost = 5 - root_power = /datum/power/cuprous_heart - power_type = TRAIT_PATH_SUBTYPE_ABERRANT - -/obj/item/organ/heart/resonant/copper - name = "cuprous heart" - desc = "This heart appears to be made out of pure copper. You could scrap this for a fair amount of dosh." - -/datum/power/cuprous_heart/add(mob/living/carbon/human/target) - - var/obj/item/organ/heart/copper_heart = null - var/obj/item/organ/heart/old_heart = target.get_organ_slot(ORGAN_SLOT_HEART) - if(old_heart && IS_ORGANIC_ORGAN(old_heart)) - copper_heart = /obj/item/organ/heart/resonant/copper - if(!isnull(copper_heart)) - copper_heart = new copper_heart - copper_heart.Insert(target, special = TRUE) - -/datum/power/muscly - name = "Condensed Musculature" - desc = "You're far more athletic than the average person." - cost = 5 - root_power = /datum/power/muscly - power_type = TRAIT_PATH_SUBTYPE_ABERRANT - power_traits = list(TRAIT_POWER_MUSCLY) - -/datum/power/bestial - name = "Latent Bestial Traits" - desc = "Your hearing is sharper than normal, but loud noises hurt your ears much more." - root_power = /datum/power/bestial - power_type = TRAIT_PATH_SUBTYPE_ABERRANT - cost = 5 - power_traits = list(TRAIT_POWER_BESTIAL) diff --git a/modular_doppler/modular_powers/powers/resonant_powers/cultivator.dm b/modular_doppler/modular_powers/powers/resonant_powers/cultivator.dm deleted file mode 100644 index 0d81970763b097..00000000000000 --- a/modular_doppler/modular_powers/powers/resonant_powers/cultivator.dm +++ /dev/null @@ -1,41 +0,0 @@ -/** - * Root Powers - */ - -/datum/power/astral_dantian - name = "Astral Dantian" - desc = "An organ entirely made of Resonance located just behind the navel. It seems to be a battery of some sort. \ - Meditation now requires direct view of the stars to be productive. You can only have one Dantian." - cost = 5 - root_power = /datum/power/astral_dantian - blacklist = list(/datum/power/umbral_dantian, /datum/power/paracausal) - power_type = TRAIT_PATH_SUBTYPE_CULTIVATOR - -/obj/item/organ/resonant/astral_dantian - name = "astral dantian" - desc = "An organ entirely made of Resonance located just behind the navel. It seems to be a battery of some sort. Meditation now requires direct view of the stars to be productive. You can only have one Dantian." - icon_state = "tongueplasma" - w_class = WEIGHT_CLASS_TINY - -/datum/power/astral_dantian/add(mob/living/carbon/human/target) - var/obj/item/organ/resonant/astral_dantian/astrawl = new () - astrawl.Insert(target, special = TRUE) - -/datum/power/umbral_dantian - name = "Umbral Dantian" - desc = "An organ entirely made of Resonance located just behind the navel. It seems to be a battery of some sort, \ - and grants the user night vision at the cost of requiring more food. Meditation requires absolute darkness to be productive. You can only have one Dantian." - cost = 5 - root_power = /datum/power/umbral_dantian - blacklist = list(/datum/power/astral_dantian, /datum/power/paracausal) - power_type = TRAIT_PATH_SUBTYPE_CULTIVATOR - -/obj/item/organ/resonant/umbral_dantian - name = "Umbral dantian" - desc = "An organ entirely made of Resonance located just behind the navel. It seems to be a battery of some sort, and grants the user night vision at the cost of requiring more food. Meditation requires absolute darkness to be productive. You can only have one Dantian." - icon_state = "tongueplasma" - w_class = WEIGHT_CLASS_TINY - -/datum/power/umbral_dantian/add(mob/living/carbon/human/target) - var/obj/item/organ/resonant/umbral_dantian/umbrawl = new () - umbrawl.Insert(target, special = TRUE) diff --git a/modular_doppler/modular_powers/powers/resonant_powers/psyker.dm b/modular_doppler/modular_powers/powers/resonant_powers/psyker.dm deleted file mode 100644 index e596daae2d7cdc..00000000000000 --- a/modular_doppler/modular_powers/powers/resonant_powers/psyker.dm +++ /dev/null @@ -1,20 +0,0 @@ - - -/datum/power/paracausal - name = "Paracausal Gland" - desc = "An organ found only in the central nervous systems of Psykers that doesn't entirely exist on our plane of existence. \ - Technically a Deviancy; however, due to its nature, this gland does not interfere with advanced psychic abilities. Violently interferes with a Dantian." - cost = 5 - root_power = /datum/power/paracausal - power_type = TRAIT_PATH_SUBTYPE_PSYKER - blacklist = list(/datum/power/astral_dantian, /datum/power/umbral_dantian) - -/obj/item/organ/resonant/paracausal - name = "paracausal gland" - desc = "An organ found only in the central nervous systems of Psykers that doesn't entirely exist on our plane of existence. Technically a Deviancy; however, due to its nature, this gland does not interfere with advanced psychic abilities. Violently interferes with a Dantian." - icon_state = "tongueplasma" - w_class = WEIGHT_CLASS_TINY - -/datum/power/paracausal/add(mob/living/carbon/human/target) - var/obj/item/organ/resonant/paracausal/para_organ = new () - para_organ.Insert(target, special = TRUE) diff --git a/modular_doppler/modular_powers/powers/sorcerous_powers/enigmatist.dm b/modular_doppler/modular_powers/powers/sorcerous_powers/enigmatist.dm deleted file mode 100644 index b9318172b2bd38..00000000000000 --- a/modular_doppler/modular_powers/powers/sorcerous_powers/enigmatist.dm +++ /dev/null @@ -1,30 +0,0 @@ -#define CAT_ENIGMATIST "Enigmatist" - -/** - * Root powers - */ - -/datum/power/chalk - - name = "Produce Resonant Chalk" - desc = "Allows a Sorcerous individual to prepare and use a spellbook, which can be re-skinned as a spell focus or a bag of materials. All Thaumaturge abilities require the use of a spellbook." - cost = 5 - root_power = /datum/power/chalk - power_type = TRAIT_PATH_SUBTYPE_ENIGMATIST - -/datum/power/chalk/add(mob/living/carbon/human/target) - target.mind.teach_crafting_recipe(/datum/crafting_recipe/resonant_chalk) - -/datum/crafting_recipe/resonant_chalk - name = "Resonant Chalk" - result = /obj/item/toy/crayon/purple/resonant_chalk - reqs = list( - /obj/item/stack/sheet/mineral/plasma = 1, - /obj/item/toy/crayon = 1, - ) - time = 5 SECONDS - category = CAT_ENIGMATIST - crafting_flags = CRAFT_MUST_BE_LEARNED - -/obj/item/toy/crayon/purple/resonant_chalk - name = "Resonant Chalk" diff --git a/modular_doppler/modular_powers/powers/sorcerous_powers/thaumaturge.dm b/modular_doppler/modular_powers/powers/sorcerous_powers/thaumaturge.dm deleted file mode 100644 index 98e06f40799716..00000000000000 --- a/modular_doppler/modular_powers/powers/sorcerous_powers/thaumaturge.dm +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Root powers - */ - -/datum/power/item/spellprep - - name = "Spell Preparation" - desc = "Allows a Sorcerous individual to prepare and use a spellbook, which can be re-skinned as a spell focus or a bag of materials. All Thaumaturge abilities require the use of a spellbook." - cost = 5 - root_power = /datum/power/item/spellprep - power_type = TRAIT_PATH_SUBTYPE_THAUMATURGE - gain_text = span_notice("You appear to have accidentaly picked up some random book instead of your spellbook...") - -/datum/power/item/spellprep/add(mob/living/carbon/human/target) - var/obj/item/book/random/spellbook = new(get_turf(target)) - spellbook.name = "[target.real_name]'s spellbook" - give_item_to_holder(target, spellbook, list( - LOCATION_LPOCKET, - LOCATION_RPOCKET, - LOCATION_BACKPACK, - LOCATION_HANDS, - ), - ) diff --git a/modular_doppler/modular_powers/powers/sorcerous_powers/theologist.dm b/modular_doppler/modular_powers/powers/sorcerous_powers/theologist.dm deleted file mode 100644 index b152fe166e2888..00000000000000 --- a/modular_doppler/modular_powers/powers/sorcerous_powers/theologist.dm +++ /dev/null @@ -1,119 +0,0 @@ -/** - * Root powers - */ - -/datum/power/burden_shared - - name = "A Burden Shared" - desc = "A channeled ability. Every four seconds, attempt to equalize both your and the target's health, in increments of 10 damage. \ - Has a cooldown of 2 minutes after use. Grants 1 Piety after health is equalized if you were at least 10 points less damaged than the target, \ - and takes 1 Piety if you were at least 10 points more damaged. Mutually exclusive with A Burden Twisted and A Burden Revered." - cost = 5 - root_power = /datum/power/burden_shared - power_type = TRAIT_PATH_SUBTYPE_THEOLOGIST - blacklist = list(/datum/power/burden_twist, /datum/power/burden_revered) - - -/datum/power/burden_shared/add(mob/living/carbon/human/target) - var/datum/action/new_action = new /datum/action/cooldown/spell/burden_shared(target.mind || target) - new_action.Grant(target) - -/datum/action/cooldown/spell/burden_shared - name = "A Burden Shared" - desc = "The knowledge required to share one's burden." - button_icon_state = "arcane_barrage" - - school = SCHOOL_CONJURATION - cooldown_time = 2 MINUTES - cooldown_reduction_per_rank = 2.5 SECONDS - spell_requirements = NONE - - invocation_type = INVOCATION_EMOTE - - invocation = "Someone starts sharing their burden!" - invocation_self_message = "You start sharing your burden." - -/datum/power/burden_twist - - name = "A Burden Twisted" - desc = "A channeled ability. Every ten seconds, heal an adjacent carbon for up to 30 damage, then deal half of that damage back to them as \ - a random proportion of brute, burn, and oxygen damage. Has a cooldown of 2 minutes after use. Has a chance to give Piety when used on someone \ - with more than 30 damage. Mutually exclusive with A Burden Shared and A Burden Revered." - cost = 5 - root_power = /datum/power/burden_twist - power_type = TRAIT_PATH_SUBTYPE_THEOLOGIST - blacklist = list(/datum/power/burden_shared, /datum/power/burden_revered) - - -/datum/power/burden_twist/add(mob/living/carbon/human/target) - var/datum/action/new_action = new /datum/action/cooldown/spell/burden_twist(target.mind || target) - new_action.Grant(target) - -/datum/action/cooldown/spell/burden_twist - name = "A Burden Twisted" - desc = "The knowledge required to twist one's burden." - button_icon_state = "arcane_barrage" - - school = SCHOOL_CONJURATION - cooldown_time = 2 MINUTES - cooldown_reduction_per_rank = 2.5 SECONDS - spell_requirements = NONE - - invocation_type = INVOCATION_EMOTE - - invocation = "Someone starts twisting their burden!" - invocation_self_message = "You start twisting your burden." - -/datum/power/burden_revered - - name = "A Burden Revered" - desc = "Use on an adjacent carbon or yourself to nullify their pain and heal up to 30 damage over a long duration of time. \ - Grants Piety based on how injured the target was. Mutually exclusive with A Burden Shared and A Burden Twisted." - cost = 5 - root_power = /datum/power/burden_revered - power_type = TRAIT_PATH_SUBTYPE_THEOLOGIST - blacklist = list(/datum/power/burden_twist, /datum/power/burden_shared) - - -/datum/power/burden_revered/add(mob/living/carbon/human/target) - var/datum/action/new_action = new /datum/action/cooldown/spell/burden_revered(target.mind || target) - new_action.Grant(target) - -/datum/action/cooldown/spell/burden_revered - name = "A Burden Revered." - desc = "The knowledge required to revere one's burden." - button_icon_state = "arcane_barrage" - - school = SCHOOL_CONJURATION - cooldown_time = 2 MINUTES - cooldown_reduction_per_rank = 2.5 SECONDS - spell_requirements = NONE - - invocation_type = INVOCATION_EMOTE - - invocation = "Someone starts revering their burden!" - invocation_self_message = "You start revering your burden." - -/datum/power/check_piety - - name = "Check Piety" - desc = "Tells you your current Piety." - cost = 0 - root_power = /datum/power/check_piety - power_type = TRAIT_PATH_SUBTYPE_THEOLOGIST - -/datum/power/check_piety/add(mob/living/carbon/human/target) - var/datum/action/new_action = new /datum/action/cooldown/mob_cooldown/check_piety(target.mind || target) - new_action.Grant(target) - - -/datum/action/cooldown/mob_cooldown/check_piety - name = "Check Piety" - button_icon = 'icons/mob/actions/actions_items.dmi' - button_icon_state = "scan_mode" - desc = "Allows you to check your piety." - cooldown_time = 1.5 SECONDS - -/datum/action/cooldown/mob_cooldown/dash/Activate(atom/target_atom) - to_chat(owner, span_notice("You have no piety. You are NOT pious. You will NEVER be pious . . . ")) - return TRUE diff --git a/modular_doppler/modular_powers/preferences/powers_middleware.dm b/modular_doppler/modular_powers/preferences/powers_middleware.dm deleted file mode 100644 index 34666e2d787b19..00000000000000 --- a/modular_doppler/modular_powers/preferences/powers_middleware.dm +++ /dev/null @@ -1,310 +0,0 @@ -/datum/preference_middleware/powers - var/static/list/name_to_powers - action_delegations = list( - "give_power" = PROC_REF(give_power), - "remove_power" = PROC_REF(remove_power), - ) - -/datum/preference_middleware/powers/get_ui_data(mob/user) - - if(length(name_to_powers) != length(GLOB.all_powers)) - initialize_names_to_powers() - - var/list/data = list() - - var/list/thaumaturge = list() - var/list/enigmatist = list() - var/list/theologist = list() - - var/list/psyker = list() - var/list/cultivator = list() - var/list/aberrant = list() - - var/list/warfighter = list() - var/list/expert = list() - var/list/augmented = list() - - var/max_power_points = MAXIMUM_POWER_POINTS - - var/current_points = point_check() - - for(var/power_name in GLOB.all_powers) - var/datum/power/power = GLOB.power_datum_instances[power_name] - - var/state - var/word - var/color - var/powertype - var/rootpower = null - - if(power.root_power == power.type) - powertype = "crown" - else if(power.advanced) - powertype = "diamond" - rootpower = power.root_power.name - else - powertype = "" - rootpower = power.root_power.name - - if(preferences.powers[power.name]) - state = "bad" - word = "Forget" - else - state = "good" - word = "Learn" - if((power.cost + current_points) > max_power_points) - state = "transparent" - word = "N/A" - color = "0.5" - rootpower = null - else - color = "1" - - var/final_list = list(list( - "description" = power.desc, - "name" = power.name, - "cost" = power.cost, - "state" = state, - "word" = word, - "color" = color, - "powertype" = powertype, - "rootpower" = rootpower - )) - - switch(power.power_type) - if(TRAIT_PATH_SUBTYPE_THAUMATURGE) - thaumaturge += final_list - if(TRAIT_PATH_SUBTYPE_ENIGMATIST) - enigmatist += final_list - if(TRAIT_PATH_SUBTYPE_THEOLOGIST) - theologist += final_list - if(TRAIT_PATH_SUBTYPE_PSYKER) - psyker += final_list - if(TRAIT_PATH_SUBTYPE_CULTIVATOR) - cultivator += final_list - if(TRAIT_PATH_SUBTYPE_ABERRANT) - aberrant += final_list - if(TRAIT_PATH_SUBTYPE_WARFIGHTER) - warfighter += final_list - if(TRAIT_PATH_SUBTYPE_EXPERT) - expert += final_list - if(TRAIT_PATH_SUBTYPE_AUGMENTED) - augmented += final_list - - - data["total_power_points"] = max_power_points - data["thaumaturge"] = thaumaturge - data["enigmatist"] = enigmatist - data["theologist"] = theologist - data["psyker"] = psyker - data["cultivator"] = cultivator - data["aberrant"] = aberrant - data["warfighter"] = warfighter - data["expert"] = expert - data["augmented"] = augmented - data["power_points"] = point_check() - - return data - -/datum/preference_middleware/powers/proc/initialize_names_to_powers() - name_to_powers = list() - for(var/power_name in GLOB.all_powers) - var/datum/power/power = GLOB.power_datum_instances[power_name] - name_to_powers[power.name] = power_name - -/** - * Gives a power to a character using the params list provided by tgui. Runs through multiple checks to ensure that the power can be learned, see respective procs for their description - * - * Always returns TRUE, ensuring the UI stays updated. - */ - -/datum/preference_middleware/powers/proc/give_power(list/params, mob/user) - var/datum/power/power = name_to_powers[params["power_name"]] - var/max_points = MAXIMUM_POWER_POINTS - - if(preferences.powers) - if(power.advanced && advanced_check(power)) - to_chat(user, span_boldwarning("[power.name] is an advanced power! You cannot cross-path with it!")) - return TRUE - - if(root_check(power)) - to_chat(user, span_boldwarning("[power.name] is missing it's root power!")) - return TRUE - - if((point_check() + power.cost) > max_points) - return TRUE - - var/datum/power/power_datum = new power() - - if(power_datum.blacklist.len && blacklist_check(power_datum, user)) - return TRUE - - if(power_datum.required_powers.len && required_check(power_datum)) - to_chat(user, span_boldwarning("[power.name] is missing one or more of it's required powers!")) - return TRUE - - qdel(power_datum) - - preferences.powers[power.name] = power - - return TRUE - -/** - * Remove Power - * - * Removes a power from a character using the params list provided by tgui. Recursively checks all learned powers for their root power, advanced power and required powers to make sure that they still pass all checks with said power removed. - */ - -/datum/preference_middleware/powers/proc/remove_power(list/params) - var/datum/power/power = name_to_powers[params["power_name"]] - - preferences.powers -= power.name - - for(var/power_name in preferences.powers) - var/datum/power/powor = preferences.powers[power_name] - - if(powor.advanced && advanced_check(powor)) - preferences.powers -= powor.name - continue - - if(root_check(powor)) - preferences.powers -= powor.name - continue - - var/datum/power/power_datum = new powor() - - if(power_datum.required_powers.len && required_check(power_datum)) - return TRUE - - qdel(power_datum) - - - return TRUE - -/** - * Advanced Power Check - * - * Gathers the advanced power's path, as well as the paths of all learned powers. If the list is longer than one, that means the user has cross-pathed, in which case the proc returns TRUE and the check fails. Otherwise, returns false and fails. - */ - -/datum/preference_middleware/powers/proc/advanced_check(datum/power/power_check) - var/list/types = list() - types += get_path_type(power_check.power_type) - - for(var/power_name in preferences.powers) - var/datum/power/power = preferences.powers[power_name] - var/type_to_check = get_path_type(power.power_type) - if(!(type_to_check in types)) - types += type_to_check - - if(types.len > 1) - return TRUE - - else return FALSE - -/** - * Root Check - * - * Checks for a power's root power. If the power is a root power itself, the check immediately returns false, passing. If the power's root power is in the player's learned powers, it returns false, also passing. Otherwise, fails. - */ - -/datum/preference_middleware/powers/proc/root_check(datum/power/power_check) - - if(power_check.root_power == power_check) - return FALSE - - for(var/power_name in preferences.powers) - var/datum/power/powah = preferences.powers[power_name] - if(power_check.root_power == powah) - return FALSE - - return TRUE - -/** - * Point Check - * - * Checks the total point value of a user's learned powers. - */ - -/datum/preference_middleware/powers/proc/point_check() - var/total_points = 0 - - for(var/power_name in preferences.powers) - var/datum/power/expensive_ass_power = preferences.powers[power_name] - total_points += expensive_ass_power.cost - - return total_points - -/** - * Blacklist Check - * - * Checks if any of the user's learned powers are in a specific power's blacklist. - */ - -/datum/preference_middleware/powers/proc/blacklist_check(datum/power/power_check, mob/user) - for(var/power_name in preferences.powers) - if(preferences.powers[power_name] in power_check.blacklist) - to_chat(user, span_boldwarning("[power_name] is in [power_check]'s blacklist!")) - return TRUE - - return FALSE - -/** - * Required Powers Check - * - * Cycles through a user's learned powers, and if that power is in the provided power's required_powers, increases the count by 1. If the count equals the length of the required_powers list, they have all the required powers, therefore the proc returns FALSE, meaning it passes. Otherwise, returns TRUE and fails. - */ - -/datum/preference_middleware/powers/proc/required_check(datum/power/power_check) - var/count = 0 - for(var/power_name in preferences.powers) - var/datum/power/required_power = preferences.powers[power_name] - if(required_power in power_check.required_powers) - count++ - - if(count == power_check.required_powers.len) - return FALSE - - return TRUE - -/datum/preferences/proc/sanitize_powers() - var/powers_edited = FALSE - for(var/power_name as anything in powers) - if(!power_name) - powers.Remove(power_name) - powers_edited = TRUE - continue - - var/datum/power/power = powers[power_name] - power = new power() - if(!(power.type in subtypesof(/datum/power))) - powers.Remove(power_name) - powers_edited = TRUE - qdel(power) - - return powers_edited - -/datum/asset/simple/powers - assets = list( - "gear.png" = 'modular_doppler/modular_powers/icons/ui/powers/gear.png', - "heart.png" = 'modular_doppler/modular_powers/icons/ui/powers/heart.png', - "seal.png" = 'modular_doppler/modular_powers/icons/ui/powers/seal.png' - ) - -/datum/preference_middleware/powers/get_ui_assets() - return list( - get_asset_datum(/datum/asset/simple/powers), - ) - -/proc/get_path_type(string) - - switch(string) - - if(TRAIT_PATH_SUBTYPE_THAUMATURGE, TRAIT_PATH_SUBTYPE_ENIGMATIST, TRAIT_PATH_SUBTYPE_THEOLOGIST) - return TRAIT_PATH_SORCEROUS - - if(TRAIT_PATH_SUBTYPE_PSYKER, TRAIT_PATH_SUBTYPE_CULTIVATOR, TRAIT_PATH_SUBTYPE_ABERRANT) - return TRAIT_PATH_RESONANT - - if(TRAIT_PATH_SUBTYPE_WARFIGHTER, TRAIT_PATH_SUBTYPE_EXPERT, TRAIT_PATH_SUBTYPE_AUGMENTED) - return TRAIT_PATH_MORTAL diff --git a/modular_doppler/modular_species/species_types/slimes/code/roundstartslimes.dm b/modular_doppler/modular_species/species_types/slimes/code/roundstartslimes.dm index 4632ebb686be87..88167803a4e80d 100644 --- a/modular_doppler/modular_species/species_types/slimes/code/roundstartslimes.dm +++ b/modular_doppler/modular_species/species_types/slimes/code/roundstartslimes.dm @@ -255,6 +255,7 @@ new_body.forceMove(get_turf(src)) new_body.blood_volume = BLOOD_VOLUME_SAFE+60 SSquirks.AssignQuirks(new_body, brainmob.client) + SSpowers.assign_powers(new_body, brainmob.client) src.replace_into(new_body) for(var/obj/item/bodypart/bodypart as anything in new_body.bodyparts) if(!istype(bodypart, /obj/item/bodypart/chest)) diff --git a/tgstation.dme b/tgstation.dme index d623063ff4d1e0..64e70481bb06ad 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7418,18 +7418,18 @@ #include "modular_doppler\modular_mood\code\mood_events\dog_wag.dm" #include "modular_doppler\modular_mood\code\mood_events\hotspring.dm" #include "modular_doppler\modular_mood\code\mood_events\race_drink.dm" -#include "modular_doppler\modular_powers\powers\_powers.dm" -#include "modular_doppler\modular_powers\powers\core_powers.dm" -#include "modular_doppler\modular_powers\powers\mortal_powers\augmented.dm" -#include "modular_doppler\modular_powers\powers\mortal_powers\expert.dm" -#include "modular_doppler\modular_powers\powers\mortal_powers\warfighter.dm" -#include "modular_doppler\modular_powers\powers\resonant_powers\aberrant.dm" -#include "modular_doppler\modular_powers\powers\resonant_powers\cultivator.dm" -#include "modular_doppler\modular_powers\powers\resonant_powers\psyker.dm" -#include "modular_doppler\modular_powers\powers\sorcerous_powers\enigmatist.dm" -#include "modular_doppler\modular_powers\powers\sorcerous_powers\thaumaturge.dm" -#include "modular_doppler\modular_powers\powers\sorcerous_powers\theologist.dm" -#include "modular_doppler\modular_powers\preferences\powers_middleware.dm" +#include "modular_doppler\modular_powers\code\_power.dm" +#include "modular_doppler\modular_powers\code\powers_living.dm" +#include "modular_doppler\modular_powers\code\powers_helpers.dm" +#include "modular_doppler\modular_powers\code\powers_prefs.dm" +#include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm" +#include "modular_doppler\modular_powers\code\powers_subsystem.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_root.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_spell.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\lodestone_legends.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" #include "modular_doppler\modular_quirks\bouncy\bouncy.dm" From 4c8ffda8abb69bc7f8c0271b0a33ace7cc9bf887 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 17 Jan 2026 14:06:43 +0100 Subject: [PATCH 002/212] working requirements-based nuke. --- .../enigmatist/test_powerthatrequirespower | 8 ++++++ .../modular_powers/code/powers_prefs.dm | 15 +++++++++- .../code/powers_prefs_middleware.dm | 8 +++++- .../modular_powers/code/powers_subsystem.dm | 28 ++++++++++++++++--- tgstation.dme | 1 + 5 files changed, 54 insertions(+), 6 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower new file mode 100644 index 00000000000000..cd3e2b1dfe53b0 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower @@ -0,0 +1,8 @@ + +/datum/power/enigmatist_spell/powerthatrequirespower + name = "Power That Requires Power" + desc = "I need more POWER. And a better way to debug this, I suppose." + + value = 1 + priority = POWER_PRIORITY_BASIC + required_powers = list(/datum/power/enigmatist_spell/lodestone_legends) diff --git a/modular_doppler/modular_powers/code/powers_prefs.dm b/modular_doppler/modular_powers/code/powers_prefs.dm index 0149618d58912a..54e139517cf762 100644 --- a/modular_doppler/modular_powers/code/powers_prefs.dm +++ b/modular_doppler/modular_powers/code/powers_prefs.dm @@ -6,10 +6,23 @@ /datum/preferences /// List of all our powers, by name. var/list/all_powers = list() + var/power_sanitize_notice + /datum/preferences/proc/sanitize_powers() - // TODO: implement some way for this to give FEEDBACK to the player about what got kerploded + // Returns TRUE if changes were made. var/list/new_powers = SSpowers.filter_invalid_powers(all_powers) + + // If the subsystem nuked the list, tell the player why. + if(!length(new_powers) && SSpowers.sanitize_nuke_reason) + all_powers = list() + power_sanitize_notice = SSpowers.sanitize_nuke_reason + return TRUE + + // Changes were made but we didn't nuke everythin if(length(new_powers) != length(all_powers)) + all_powers = new_powers return TRUE + return FALSE + diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index 5d814621f16ea6..c693e329989b04 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -18,6 +18,12 @@ ) /datum/preference_middleware/powers/get_ui_data(mob/user) + // Show "kerplode" notice once when the UI is opened (avoids init spam) + // TODO: Move this when we overhaul this mess. + if(preferences.power_sanitize_notice) + to_chat(user, span_boldwarning("[preferences.power_sanitize_notice]")) + preferences.power_sanitize_notice = null + var/list/data = list() var/list/thaumaturge = list() @@ -196,7 +202,7 @@ return FALSE // We don't have this power. // Make sure none of our other powers need this power. - + var/datum/power/requiring_power_type = get_requiring_power(power_type) if(requiring_power_type) to_chat(user, span_boldwarning("[power_name] is needed by [requiring_power_type.name]!")) diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index 40ba1b5e7e0c62..c63548acdfc95b 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -68,6 +68,8 @@ PROCESSING_SUBSYSTEM_DEF(powers) POWER_ARCHETYPE_RESONANT = list(), POWER_ARCHETYPE_MORTAL = list(), ) + /// If sanitize had to nuke, this stores why (for prefs to display to player) + var/sanitize_nuke_reason /datum/controller/subsystem/processing/powers/Initialize() get_powers() @@ -132,6 +134,7 @@ PROCESSING_SUBSYSTEM_DEF(powers) /// If no changes need to be made, will return the same list. /// Expects all power names to be unique, but makes no other expectations. /datum/controller/subsystem/processing/powers/proc/filter_invalid_powers(list/powers_to_check) + sanitize_nuke_reason = null var/current_balance = 0 var/current_archetype var/list/intermediary_powers = list() @@ -182,15 +185,32 @@ PROCESSING_SUBSYSTEM_DEF(powers) break if(blacklisted) continue // Incompatible, discard. + intermediary_powers += power_name - // TODO: remove this and finish stuff - intermediary_powers += power_type.name - + // Build a set of selected power types. + var/list/selected_types = list() + for(var/power_name in intermediary_powers) + var/datum/power/power_type = all_powers[power_name] + selected_types[power_type] = TRUE + + // If ANY selected power is missing ANY requirement, nuke the entire list. + for(var/power_name in intermediary_powers) + var/datum/power/power_type = all_powers[power_name] + var/list/required = GLOB.powers_requirements_list[power_type] + if(!length(required)) + continue + + for(var/datum/power/req_type as anything in required) + if(!selected_types[req_type]) + sanitize_nuke_reason = "Saved powers reset: \"[power_name]\" requires [req_type], which was not present." + return list() + + // Everything is fine = return as normal if(intermediary_powers.len == powers_to_check.len) return powers_to_check - return intermediary_powers + /** TODO: ALL THE REST OF THIS var/value = initial(power_type.value) diff --git a/tgstation.dme b/tgstation.dme index 64e70481bb06ad..9a1122e234b62d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7429,6 +7429,7 @@ #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\enigmatist\test_powerthatrequirespower" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" From d7d4ca31be5b99579590a8b81bd16b479239c555 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 19 Jan 2026 11:55:44 +0100 Subject: [PATCH 003/212] middleware message rathern than embedding in get_ui_data --- .../code/powers_prefs_middleware.dm | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index c693e329989b04..3303b9cdfc2c11 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -17,6 +17,21 @@ "remove_power" = PROC_REF(remove_power), ) +/datum/preference_middleware/powers/ui_act(action, list/params, datum/tgui/ui, datum/preferences/prefs) + // Notice that power preferences were nuked; queqed to occur when the user opens the character setup UI + . = ..() + if(.) + return + + // The message, resets after being send so you don't see it more than once. + if(prefs.power_sanitize_notice) + var/mob/user = ui.user + if(user) + to_chat(user, span_boldwarning(prefs.power_sanitize_notice)) + prefs.power_sanitize_notice = null + + return FALSE + /datum/preference_middleware/powers/get_ui_data(mob/user) // Show "kerplode" notice once when the UI is opened (avoids init spam) // TODO: Move this when we overhaul this mess. From e40bcc41a27cc6c79136c7ce2a5ffb42939024b4 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 19 Jan 2026 11:59:58 +0100 Subject: [PATCH 004/212] minor updoots --- .../modular_powers/code/powers_prefs_middleware.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index 3303b9cdfc2c11..ca0cb4b2707e9a 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -24,8 +24,8 @@ return // The message, resets after being send so you don't see it more than once. - if(prefs.power_sanitize_notice) - var/mob/user = ui.user + if(prefs?.power_sanitize_notice) + var/mob/user = ui?.user if(user) to_chat(user, span_boldwarning(prefs.power_sanitize_notice)) prefs.power_sanitize_notice = null From 1f051d6e48b505f47f8edcc7ce5b937c5fd88d5d Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 19 Jan 2026 23:06:29 +0100 Subject: [PATCH 005/212] compact error message --- .../modular_powers/code/powers_prefs.dm | 19 +++++++---------- .../code/powers_prefs_middleware.dm | 21 ------------------- .../modular_powers/code/powers_subsystem.dm | 6 ++---- 3 files changed, 10 insertions(+), 36 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers_prefs.dm b/modular_doppler/modular_powers/code/powers_prefs.dm index 54e139517cf762..46c15a09b605d4 100644 --- a/modular_doppler/modular_powers/code/powers_prefs.dm +++ b/modular_doppler/modular_powers/code/powers_prefs.dm @@ -6,23 +6,20 @@ /datum/preferences /// List of all our powers, by name. var/list/all_powers = list() - var/power_sanitize_notice /datum/preferences/proc/sanitize_powers() - // Returns TRUE if changes were made. var/list/new_powers = SSpowers.filter_invalid_powers(all_powers) + var/list/powers_removed = SSpowers.powers_removed + var/list/feedback - // If the subsystem nuked the list, tell the player why. - if(!length(new_powers) && SSpowers.sanitize_nuke_reason) + // If filter_invalid_powers came back with removed powers, we apply the changes and give feedback + if(LAZYLEN(powers_removed) && !length(new_powers)) all_powers = list() - power_sanitize_notice = SSpowers.sanitize_nuke_reason + LAZYADD(feedback, "Your powers were removed because of the following reasons:") + LAZYADD(feedback, powers_removed) + if(LAZYLEN(feedback)) + to_chat(parent, span_greentext(feedback.Join("\n"))) return TRUE - - // Changes were made but we didn't nuke everythin - if(length(new_powers) != length(all_powers)) - all_powers = new_powers - return TRUE - return FALSE diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index ca0cb4b2707e9a..18cfe2a07fb62a 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -17,28 +17,7 @@ "remove_power" = PROC_REF(remove_power), ) -/datum/preference_middleware/powers/ui_act(action, list/params, datum/tgui/ui, datum/preferences/prefs) - // Notice that power preferences were nuked; queqed to occur when the user opens the character setup UI - . = ..() - if(.) - return - - // The message, resets after being send so you don't see it more than once. - if(prefs?.power_sanitize_notice) - var/mob/user = ui?.user - if(user) - to_chat(user, span_boldwarning(prefs.power_sanitize_notice)) - prefs.power_sanitize_notice = null - - return FALSE - /datum/preference_middleware/powers/get_ui_data(mob/user) - // Show "kerplode" notice once when the UI is opened (avoids init spam) - // TODO: Move this when we overhaul this mess. - if(preferences.power_sanitize_notice) - to_chat(user, span_boldwarning("[preferences.power_sanitize_notice]")) - preferences.power_sanitize_notice = null - var/list/data = list() var/list/thaumaturge = list() diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index c63548acdfc95b..ee52845903b64e 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -68,8 +68,7 @@ PROCESSING_SUBSYSTEM_DEF(powers) POWER_ARCHETYPE_RESONANT = list(), POWER_ARCHETYPE_MORTAL = list(), ) - /// If sanitize had to nuke, this stores why (for prefs to display to player) - var/sanitize_nuke_reason + var/list/powers_removed /datum/controller/subsystem/processing/powers/Initialize() get_powers() @@ -134,7 +133,6 @@ PROCESSING_SUBSYSTEM_DEF(powers) /// If no changes need to be made, will return the same list. /// Expects all power names to be unique, but makes no other expectations. /datum/controller/subsystem/processing/powers/proc/filter_invalid_powers(list/powers_to_check) - sanitize_nuke_reason = null var/current_balance = 0 var/current_archetype var/list/intermediary_powers = list() @@ -202,7 +200,7 @@ PROCESSING_SUBSYSTEM_DEF(powers) for(var/datum/power/req_type as anything in required) if(!selected_types[req_type]) - sanitize_nuke_reason = "Saved powers reset: \"[power_name]\" requires [req_type], which was not present." + LAZYADD(powers_removed, "[power_name]\" requires [req_type], which was not present.") return list() // Everything is fine = return as normal From eaa5d5781ef45df1ce6e236639a3e8ad45b97e62 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 20 Jan 2026 00:38:28 +0100 Subject: [PATCH 006/212] Might be prudent to leave this comment. --- modular_doppler/modular_powers/code/powers_prefs.dm | 1 + 1 file changed, 1 insertion(+) diff --git a/modular_doppler/modular_powers/code/powers_prefs.dm b/modular_doppler/modular_powers/code/powers_prefs.dm index 46c15a09b605d4..e9e5c52b84dcdf 100644 --- a/modular_doppler/modular_powers/code/powers_prefs.dm +++ b/modular_doppler/modular_powers/code/powers_prefs.dm @@ -19,6 +19,7 @@ LAZYADD(feedback, "Your powers were removed because of the following reasons:") LAZYADD(feedback, powers_removed) if(LAZYLEN(feedback)) + // This doesn't work if the player joins the game with an invalid file. SAD! to_chat(parent, span_greentext(feedback.Join("\n"))) return TRUE return FALSE From 8516606c39ee07d36bc8a8bb4ee140efdf87f9e8 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 21 Jan 2026 21:25:19 +0100 Subject: [PATCH 007/212] Nukes old powers --- code/modules/client/preferences_savefile.dm | 6 ++++++ .../modular_powers/code/powers_migration.dm | 14 ++++++++++++++ tgstation.dme | 1 + 3 files changed, 21 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers_migration.dm diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index acdb55229c0647..81fdf18250c4fb 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -364,6 +364,12 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car if(SHOULD_UPDATE_DATA(data_validity_integer)) update_character(data_validity_integer, save_data) + // DOPPLER SHIFT ADDITION BEGIN + if(check_for_old_powers(save_data)) + //We get rid of all the powers that once prevously existed from the old system. + save_data?["powers"] = list() + // DOPPLER SHIFT ADDITION END + //Sanitize randomise = SANITIZE_LIST(randomise) job_preferences = SANITIZE_LIST(job_preferences) diff --git a/modular_doppler/modular_powers/code/powers_migration.dm b/modular_doppler/modular_powers/code/powers_migration.dm new file mode 100644 index 00000000000000..55e0e9f8a2928d --- /dev/null +++ b/modular_doppler/modular_powers/code/powers_migration.dm @@ -0,0 +1,14 @@ +// Grabs the savefile +/datum/preferences/proc/get_savefile_powers(list/save_data) + var/powers = save_data["powers"] + return powers + +//Checks if there's old powers; if so clean slate. +/datum/preferences/proc/check_for_old_powers(list/save_data) + if(isnull(save_data)) + save_data = list() + var/powers = get_savefile_powers(save_data) + if(!isnull(powers)) + return TRUE + return FALSE + diff --git a/tgstation.dme b/tgstation.dme index 9a1122e234b62d..b801b2bdc8a43d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7419,6 +7419,7 @@ #include "modular_doppler\modular_mood\code\mood_events\hotspring.dm" #include "modular_doppler\modular_mood\code\mood_events\race_drink.dm" #include "modular_doppler\modular_powers\code\_power.dm" +#include "modular_doppler\modular_powers\code\powers_migration.dm" #include "modular_doppler\modular_powers\code\powers_living.dm" #include "modular_doppler\modular_powers\code\powers_helpers.dm" #include "modular_doppler\modular_powers\code\powers_prefs.dm" From aad4e276a0889b686ba169d83d4346828593b9ca Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 21 Jan 2026 22:08:39 +0100 Subject: [PATCH 008/212] redone to be an agnostic system --- code/modules/client/preferences_savefile.dm | 11 +----- .../code/_preferences_savefile.dm | 37 +++++++++++++++++++ .../code/powers_migration.dm | 7 ++++ .../modular_powers/code/powers_migration.dm | 14 ------- tgstation.dme | 3 +- 5 files changed, 48 insertions(+), 24 deletions(-) create mode 100644 modular_doppler/_savefile_migration/code/_preferences_savefile.dm create mode 100644 modular_doppler/_savefile_migration/code/powers_migration.dm delete mode 100644 modular_doppler/modular_powers/code/powers_migration.dm diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 81fdf18250c4fb..b64d8cdf9894ed 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -363,12 +363,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car //preference updating will handle saving the updated data for us. if(SHOULD_UPDATE_DATA(data_validity_integer)) update_character(data_validity_integer, save_data) - - // DOPPLER SHIFT ADDITION BEGIN - if(check_for_old_powers(save_data)) - //We get rid of all the powers that once prevously existed from the old system. - save_data?["powers"] = list() - // DOPPLER SHIFT ADDITION END + check_doppler_character_savefile(save_data) // DOPPLER EDIT ADDITION - Modular Character Savefile Migration //Sanitize randomise = SANITIZE_LIST(randomise) @@ -430,9 +425,7 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car //Quirks save_data["all_quirks"] = all_quirks - save_data["languages"] = languages // DOPPLER SHIFT ADDITION - we might want to migrate this - save_data["alt_job_titles"] = alt_job_titles // DOPPLER SHIFT ADDITION: alt job titles - save_data["all_powers"] = all_powers // DOPPLER SHIFT ADDITION - Powers system + save_character_doppler(save_data) // DOPPLER ADDITION - Modular Savefile return TRUE diff --git a/modular_doppler/_savefile_migration/code/_preferences_savefile.dm b/modular_doppler/_savefile_migration/code/_preferences_savefile.dm new file mode 100644 index 00000000000000..49d1185b7c0771 --- /dev/null +++ b/modular_doppler/_savefile_migration/code/_preferences_savefile.dm @@ -0,0 +1,37 @@ + +#define DOPPLER_SAVEFILE_VERSION_MAX 1 + +#define VERSION_NEW_POWERS 1 + +#define SHOULD_UPDATE_DOPPLER_DATA(version) (version < DOPPLER_SAVEFILE_VERSION_MAX) + +// Grabs the savefile +/datum/preferences/proc/get_savefile_version(list/save_data) + var/savefile_version = save_data["doppler_version"] + return savefile_version + +//Checks if the savefile is older. +/datum/preferences/proc/check_doppler_character_savefile(list/save_data) + if(isnull(save_data)) + save_data = list() + var/current_version = get_savefile_version(save_data) + if(!SHOULD_UPDATE_DOPPLER_DATA(current_version)) + return + update_character_doppler(current_version, save_data) + +/// Updates our character save data. +/datum/preferences/proc/update_character_doppler(current_version, list/save_data) + // Version for old powers system + if(current_version < VERSION_NEW_POWERS) + nuke_old_powers(save_data) + +/datum/preferences/proc/save_character_doppler(list/save_data) + save_data["languages"] = languages + save_data["alt_job_titles"] = alt_job_titles + save_data["all_powers"] = all_powers + // load_character will sanitize any bad data, so assume up-to-date. + save_data["version"] = DOPPLER_SAVEFILE_VERSION_MAX + +#undef DOPPLER_SAVEFILE_VERSION_MAX +#undef VERSION_NEW_POWERS +#undef SHOULD_UPDATE_DOPPLER_DATA diff --git a/modular_doppler/_savefile_migration/code/powers_migration.dm b/modular_doppler/_savefile_migration/code/powers_migration.dm new file mode 100644 index 00000000000000..b308ca5d5d7dec --- /dev/null +++ b/modular_doppler/_savefile_migration/code/powers_migration.dm @@ -0,0 +1,7 @@ + +/** + * Removes the old powers from people's savefiles + */ +/datum/preferences/proc/nuke_old_powers(list/save_data) + save_data?["powers"] = list() + log_game("The nuke was dropped, yay") diff --git a/modular_doppler/modular_powers/code/powers_migration.dm b/modular_doppler/modular_powers/code/powers_migration.dm deleted file mode 100644 index 55e0e9f8a2928d..00000000000000 --- a/modular_doppler/modular_powers/code/powers_migration.dm +++ /dev/null @@ -1,14 +0,0 @@ -// Grabs the savefile -/datum/preferences/proc/get_savefile_powers(list/save_data) - var/powers = save_data["powers"] - return powers - -//Checks if there's old powers; if so clean slate. -/datum/preferences/proc/check_for_old_powers(list/save_data) - if(isnull(save_data)) - save_data = list() - var/powers = get_savefile_powers(save_data) - if(!isnull(powers)) - return TRUE - return FALSE - diff --git a/tgstation.dme b/tgstation.dme index b801b2bdc8a43d..ea5fbaaed63c3f 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7418,8 +7418,9 @@ #include "modular_doppler\modular_mood\code\mood_events\dog_wag.dm" #include "modular_doppler\modular_mood\code\mood_events\hotspring.dm" #include "modular_doppler\modular_mood\code\mood_events\race_drink.dm" +#include "modular_doppler\_savefile_migration\code\_preferences_savefile.dm" +#include "modular_doppler\_savefile_migration\code\powers_migration.dm" #include "modular_doppler\modular_powers\code\_power.dm" -#include "modular_doppler\modular_powers\code\powers_migration.dm" #include "modular_doppler\modular_powers\code\powers_living.dm" #include "modular_doppler\modular_powers\code\powers_helpers.dm" #include "modular_doppler\modular_powers\code\powers_prefs.dm" From 0e3280886ab9a59e4ca26d31db47f2693117eb36 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 22 Jan 2026 20:33:48 +0100 Subject: [PATCH 009/212] modify quirks in VV --- code/__DEFINES/vv.dm | 1 + .../modular_powers/code/powers_vv.dm | 60 +++++++++++++++++++ tgstation.dme | 1 + 3 files changed, 62 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers_vv.dm diff --git a/code/__DEFINES/vv.dm b/code/__DEFINES/vv.dm index 6c9d4e2b8509b9..df359dc0240e0c 100644 --- a/code/__DEFINES/vv.dm +++ b/code/__DEFINES/vv.dm @@ -161,6 +161,7 @@ #define VV_HK_PURRBATION "purrbation" #define VV_HK_APPLY_DNA_INFUSION "apply_dna_infusion" #define VV_HK_TURN_INTO_MMI "turn_into_mmi" +#define VV_HK_MOD_POWERS "powermod" // DOPPLER EDIT ADDITION - Power System // misc #define VV_HK_SPACEVINE_PURGE "spacevine_purge" diff --git a/modular_doppler/modular_powers/code/powers_vv.dm b/modular_doppler/modular_powers/code/powers_vv.dm new file mode 100644 index 00000000000000..e51fafa73e03ff --- /dev/null +++ b/modular_doppler/modular_powers/code/powers_vv.dm @@ -0,0 +1,60 @@ +/* + We hook into the process normally located in human.dm for adding verbs. + This is all extremely similar to how quirks does it. +*/ + +// Adds it to the list of dropdowns +/mob/living/carbon/human/vv_get_dropdown() + . = ..() + VV_DROPDOWN_OPTION(VV_HK_MOD_POWERS, "Add/Remove Powers") + +// Adds the actual verb that gets executed when selected. +/mob/living/carbon/human/vv_do_topic(list/href_list) + . = ..() + if(href_list[VV_HK_MOD_POWERS]) + if(!check_rights(R_SPAWN)) + return + var/list/options = list("Clear"="Clear") + for(var/x in subtypesof(/datum/power)) + var/datum/power/pow = x + var/name = initial(pow.name) + options[src.has_power(pow) ? "[name] (Remove)" : "[name] (Add)"] = pow + var/result = input(usr, "Choose power to add/remove","Power Mod") as null|anything in sort_list(options) + if(result) + if(result == "Clear") + for(var/datum/power/p in powers) + remove_power(p.type) + else + var/T = options[result] + if(has_power(T)) + remove_power(T) + else + add_power(T) + +// Checks if a power is on the selected target +/mob/living/carbon/human/proc/has_power(powertype) + for(var/datum/power/power in powers) + if(power.type == powertype) + return TRUE + return FALSE + +// Adds a power by calling the power subsystem. +/mob/living/carbon/human/proc/add_power(datum/power/powertype, power_transfer = FALSE, client/override_client, unique = TRUE) + if(has_power(powertype)) + return FALSE + var/pname = initial(powertype.name) + if(!SSpowers || !SSpowers.powers[pname]) + return FALSE + var/datum/power/power = new powertype() + if(power.add_to_holder(new_holder = src, power_transfer = power_transfer, client_source = override_client, unique = unique)) + return TRUE + qdel(power) + return FALSE + +// Removes a power. +/mob/living/carbon/human/proc/remove_power(powertype) + for(var/datum/power/power in powers) + if(power.type == powertype) + qdel(power) + return TRUE + return FALSE diff --git a/tgstation.dme b/tgstation.dme index ea5fbaaed63c3f..91fdee995afc93 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7426,6 +7426,7 @@ #include "modular_doppler\modular_powers\code\powers_prefs.dm" #include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm" #include "modular_doppler\modular_powers\code\powers_subsystem.dm" +#include "modular_doppler\modular_powers\code\powers_vv.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_root.dm" From 5425b732564f256619c8f9f102444a30707d9af8 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 22 Jan 2026 20:54:40 +0100 Subject: [PATCH 010/212] minor nitpicks like var names and ending in success statements --- .../modular_powers/code/powers_vv.dm | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers_vv.dm b/modular_doppler/modular_powers/code/powers_vv.dm index e51fafa73e03ff..fa4395ad891adb 100644 --- a/modular_doppler/modular_powers/code/powers_vv.dm +++ b/modular_doppler/modular_powers/code/powers_vv.dm @@ -15,21 +15,21 @@ if(!check_rights(R_SPAWN)) return var/list/options = list("Clear"="Clear") - for(var/x in subtypesof(/datum/power)) - var/datum/power/pow = x - var/name = initial(pow.name) - options[src.has_power(pow) ? "[name] (Remove)" : "[name] (Add)"] = pow + for(var/listedpower in subtypesof(/datum/power)) + var/datum/power/power = listedpower + var/name = initial(power.name) + options[src.has_power(power) ? "[name] (Remove)" : "[name] (Add)"] = power var/result = input(usr, "Choose power to add/remove","Power Mod") as null|anything in sort_list(options) if(result) if(result == "Clear") - for(var/datum/power/p in powers) - remove_power(p.type) + for(var/datum/power/toberemoved in powers) + remove_power(toberemoved.type) else - var/T = options[result] - if(has_power(T)) - remove_power(T) + var/chosen = options[result] + if(has_power(chosen)) + remove_power(chosen) else - add_power(T) + add_power(chosen) // Checks if a power is on the selected target /mob/living/carbon/human/proc/has_power(powertype) @@ -46,15 +46,15 @@ if(!SSpowers || !SSpowers.powers[pname]) return FALSE var/datum/power/power = new powertype() - if(power.add_to_holder(new_holder = src, power_transfer = power_transfer, client_source = override_client, unique = unique)) - return TRUE - qdel(power) - return FALSE + if(!power.add_to_holder(new_holder = src, power_transfer = power_transfer, client_source = override_client, unique = unique)) + qdel(power) + return FALSE + return TRUE // Removes a power. /mob/living/carbon/human/proc/remove_power(powertype) for(var/datum/power/power in powers) - if(power.type == powertype) - qdel(power) - return TRUE - return FALSE + if(!power.type == powertype) + return FALSE + qdel(power) + return TRUE From 15778dd3368dd306f1786e4a1e47919d59a12cd4 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 23 Jan 2026 18:24:24 +0100 Subject: [PATCH 011/212] late on this commit; adds the framework for psyker abilities, adds telekinesis in a relatively simple form. --- code/__DEFINES/DNA.dm | 4 + code/__DEFINES/~doppler_defines/powers.dm | 8 + modular_doppler/modular_powers/code/_power.dm | 22 +- .../powers/resonant/psyker/_psyker_power.dm | 17 ++ .../powers/resonant/psyker/_psyker_root.dm | 18 ++ .../powers/resonant/psyker/psyker_organ.dm | 46 ++++ .../powers/resonant/psyker/telekinesis.dm | 226 ++++++++++++++++++ ...espower => test_powerthatrequirespower.dm} | 0 .../modular_powers/code/powers_vv.dm | 6 +- tgstation.dme | 6 +- 10 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_power.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_root.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm rename modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/{test_powerthatrequirespower => test_powerthatrequirespower.dm} (100%) diff --git a/code/__DEFINES/DNA.dm b/code/__DEFINES/DNA.dm index c4bd0c447cd63d..bad9bb1cc38ef5 100644 --- a/code/__DEFINES/DNA.dm +++ b/code/__DEFINES/DNA.dm @@ -137,6 +137,10 @@ #define ORGAN_SLOT_VOICE "vocal_cords" #define ORGAN_SLOT_ZOMBIE "zombie_infection" +// DOPPLER ADDITION START - Power-based organs +#define ORGAN_SLOT_PSYKER "psyker_organ" +// DOPPLER ADDITION END + /// Organ slot external #define ORGAN_SLOT_EXTERNAL_TAIL "tail" #define ORGAN_SLOT_EXTERNAL_SPINES "spines" diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 3451a5622580d5..13215f2d6940a0 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -81,3 +81,11 @@ #define MARTIALART_BREACHERKNUCKLE "breacher knuckle" #define MARTIALART_MAD_DOG "the mag dog style" + +/** + * RESONANT + * All defines related to the resonant archetype. + */ + +/// Trait held by all under the resonant archetype. +#define TRAIT_ARCHETYPE_RESONANT "archetype_resonant" diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index 3a4938b27b4caa..e1945d1a9b6e09 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -37,7 +37,9 @@ var/priority = NONE /// The powers this requires, if any. var/list/required_powers - + + // The path, if applicable, to the action. + var/datum/action/cooldown/action_path /datum/power/New() . = ..() @@ -158,8 +160,26 @@ /// This proc is guaranteed to run if the mob has a client when the power is added. /// Otherwise, it runs once on the next COMSIG_MOB_LOGIN. /datum/power/proc/post_add() + // Grants appropriate actions in the UI + grant_action() return +// Adds activateable power buttons. +/datum/power/proc/grant_action() + if(!ispath(action_path) || !power_holder) + return FALSE + + var/datum/action/cooldown/new_power = new action_path(src) + new_power.background_icon_state = "bg_tech_blue" + new_power.base_background_icon_state = new_power.background_icon_state + new_power.active_background_icon_state = "[new_power.base_background_icon_state]_active" + new_power.overlay_icon_state = "bg_tech_blue_border" + new_power.active_overlay_icon_state = "bg_spell_border_active_blue" + new_power.panel = "Powers" + new_power.Grant(power_holder) + + return new_power + /// Returns if the power holder should process currently or not. /datum/power/proc/should_process() SHOULD_CALL_PARENT(TRUE) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_power.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_power.dm new file mode 100644 index 00000000000000..1e45a8ccf0971a --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_power.dm @@ -0,0 +1,17 @@ + +/datum/power/psyker_power + name = "Abstract Psyker Power" + desc = "My claivoyance lets me see into the unseen: \ + and oh god it has shown this debug code. Please report this!" + abstract_parent_type = /datum/power/psyker_power + + archetype = POWER_ARCHETYPE_RESONANT + path = POWER_PATH_PSYKER + required_powers = list(/datum/power/psyker_root) + +/* +/datum/power/psyker_power/proc/add_stress(amount) + var/obj/item/organ/resonant/psyker_organ/psyker_organ = power_owner.get_organ_slot(ORGAN_SLOT_PSYKER) + if(psyker_organ) + psyker_organ.stress += amount +*/ diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_root.dm new file mode 100644 index 00000000000000..67949f34fdfded --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_root.dm @@ -0,0 +1,18 @@ + +/datum/power/psyker_root + name = "Paracausal Gland" + desc = "An organ found only in the central nervous system of Psykers \ + grown by prolonged exposure to certain types of Resonance. \ + The catalyst for psychic abilities; but beware overexerting it." + + value = 5 + mob_trait = TRAIT_ARCHETYPE_RESONANT + archetype = POWER_ARCHETYPE_RESONANT + path = POWER_PATH_PSYKER + priority = POWER_PRIORITY_ROOT + + var/obj/item/organ/resonant/psyker/psyker_organ + +/datum/power/psyker_root/add(client/client_source) + psyker_organ = new /obj/item/organ/resonant/psyker + psyker_organ.Insert(power_holder, special = TRUE) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm new file mode 100644 index 00000000000000..129cd586346671 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm @@ -0,0 +1,46 @@ +/obj/item/organ/resonant/psyker + name = "paracausal gland" + desc = "An intrusive organ that should not even be able to function in most bodies. Commonly found in the bodies of Psykers. Though many would try to implement these into themselves to try and awaken psychic powers, its presence in those without such powers is often life-threatening." + icon = 'icons/obj/medical/organs/organs.dmi' + icon_state = "demon_heart-on" + decay_factor = 5 * STANDARD_ORGAN_DECAY //about 12mins to fully decay. + slot = ORGAN_SLOT_PSYKER + zone = BODY_ZONE_CHEST + + // The psyker organ handles most of the stress to do with psyker abilities. Without this organ, you can't use these abilities. + // Stress is not correlated to organ damage, but organ damage does affect this gland. + var/stress = 0 + // Stress threshold is how much the psyker organ can handle before the bad events start befalling the user. + // Usually, 1x is the minor events, 1.5x are the major events, and 2x are the catastrophic events. + var/stress_threshold = 100 + // Base recovery per second + var/recovery_per_second = 1.1 + + +/obj/item/organ/resonant/psyker/on_life(seconds_per_tick, times_fired) + . = ..() + + // If you have the associated power read; you are a psyker. + if(owner.has_power(/datum/power/psyker_root)) + if(stress <= 0) + stress = 0 + return + var/stress_to_recover = recovery_per_second + // Harder to recover at higher stress + stress_to_recover -= (stress * 0.01) + + // Organ damage makes recovery worse + stress_to_recover -= (damage * 0.015) + + // Don’t let recovery go negative (would increase stress) + stress_to_recover = max(stress_to_recover, 0) + + // Apply recovery, don't let it send stress into the negatives. + stress = max(stress - stress_to_recover * seconds_per_tick, 0) + + // In the event that you implant this into someone else. + // Currently placeholder til we settle on what it do on people that don't have it. + else + damage += 1 + owner.apply_damage(damage * 0.1, TOX) + diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm new file mode 100644 index 00000000000000..c028fbe0e4027f --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -0,0 +1,226 @@ + +/datum/power/psyker_power/telekinesis + name = "Telekinesis" + desc = "Grants you temporary telekinetic powers. Passively increases stress while active." + + value = 5 + priority = POWER_PRIORITY_BASIC + required_powers = list(/datum/power/psyker_root) + action_path = /datum/action/cooldown/spell/pointed/telekinesis + +/datum/action/cooldown/spell/pointed/telekinesis + name = "Telekinesis" + desc = "Middle-click to grab an object. Middle-click again to drop." + button_icon = 'icons/mob/actions/actions_revenant.dmi' + button_icon_state = "r_transmit" + + unset_after_click = FALSE + cast_range = 8 + aim_assist = FALSE + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + antimagic_flags = MAGIC_RESISTANCE_MIND + + /// Range of the kinesis grab. + var/grab_range = 8 + /// Stat required for us to grab a mob. + var/stat_required = DEAD + + /// Atom we grabbed with kinesis. + var/atom/movable/grabbed_atom + /// Ref of the beam following the grabbed atom. + var/datum/beam/kinesis_beam + /// Overlay we add to each grabbed atom. + var/mutable_appearance/kinesis_icon + /// Mouse movement catcher (for dragging) + var/atom/movable/screen/fullscreen/cursor_catcher/kinesis/kinesis_catcher + + + /// Psyker organ for stress. + var/obj/item/organ/resonant/psyker/psyker_organ + var/stress_cost_grab = 6 + +/datum/action/cooldown/spell/pointed/telekinesis/on_activation(mob/on_who) + . = ..() + if(!.) + return + psyker_organ = on_who.get_organ_slot(ORGAN_SLOT_PSYKER) + return TRUE + +/datum/action/cooldown/spell/pointed/telekinesis/on_deactivation(mob/on_who, refund_cooldown = TRUE) + clear_grab(playsound = FALSE) + psyker_organ = null + return ..() + +/datum/action/cooldown/spell/pointed/telekinesis/InterceptClickOn(mob/living/clicker, params, atom/target) + // We only care about the owner using it + if(clicker != owner) + return TRUE + + var/list/modifiers = params2list(params) + + // Only middle click does anything for now + if(!LAZYACCESS(modifiers, MIDDLE_CLICK)) + return TRUE + + // Middle click: drop if already holding something + if(grabbed_atom) + clear_grab() + return TRUE + + // Otherwise: attempt grab + if(!target) + return TRUE + + if(!range_check(clicker, target)) + to_chat(clicker, span_warning("Too far!")) + return TRUE + + if(!can_grab(clicker, target)) + to_chat(clicker, span_warning("Can't grab that!")) + return TRUE + + add_stress(stress_cost_grab) + grab_atom(clicker, target) + return TRUE + +// TODO: Move this to the more universal one in _psyker_power.dm +/datum/action/cooldown/spell/pointed/telekinesis/proc/add_stress(amount) + if(!amount) + return + var/mob/living/user = owner + if(!psyker_organ && user) + psyker_organ = user.get_organ_slot(ORGAN_SLOT_PSYKER) + if(psyker_organ) + psyker_organ.stress += amount + +/datum/action/cooldown/spell/pointed/telekinesis/proc/range_check(mob/living/user, atom/target) + if(!isturf(user.loc)) + return FALSE + if(ismovable(target) && !isturf(target.loc)) + return FALSE + if(!can_see(user, target, grab_range)) + return FALSE + return TRUE + +/datum/action/cooldown/spell/pointed/telekinesis/proc/can_grab(mob/living/user, atom/target) + if(user == target) + return FALSE + if(!ismovable(target)) + return FALSE + if(iseffect(target)) + return FALSE + + var/atom/movable/movable_target = target + if(movable_target.anchored) + return FALSE + if(movable_target.throwing) + return FALSE + if(movable_target.move_resist >= MOVE_FORCE_OVERPOWERING) + return FALSE + + if(ismob(movable_target)) + if(!isliving(movable_target)) + return FALSE + var/mob/living/living_target = movable_target + if(living_target.buckled) + return FALSE + if(living_target.stat < stat_required) + return FALSE + else if(isitem(movable_target)) + var/obj/item/item_target = movable_target + if(item_target.w_class >= WEIGHT_CLASS_GIGANTIC) + return FALSE + if(item_target.item_flags & ABSTRACT) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/pointed/telekinesis/proc/grab_atom(mob/living/user, atom/movable/target) + grabbed_atom = target + + if(isliving(grabbed_atom)) + grabbed_atom.add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), REF(src)) + RegisterSignal(grabbed_atom, COMSIG_MOB_STATCHANGE, PROC_REF(on_statchange)) + + ADD_TRAIT(grabbed_atom, TRAIT_NO_FLOATING_ANIM, REF(src)) + RegisterSignal(grabbed_atom, COMSIG_MOVABLE_SET_ANCHORED, PROC_REF(on_setanchored)) + + playsound(grabbed_atom, 'sound/items/weapons/contractor_baton/contractorbatonhit.ogg', 75, TRUE) + + kinesis_icon = mutable_appearance(icon = 'icons/effects/effects.dmi', icon_state = "cursehand0", layer = grabbed_atom.layer - 0.1, appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART) + kinesis_icon.overlays += emissive_appearance(icon = 'icons/effects/effects.dmi', icon_state = "cursehand0", offset_spokesman = grabbed_atom) + grabbed_atom.add_overlay(kinesis_icon) + + kinesis_beam = user.Beam(grabbed_atom, "curse0") + if(!kinesis_catcher) + kinesis_catcher = user.overlay_fullscreen( + "curse0", + /atom/movable/screen/fullscreen/cursor_catcher/kinesis, + 0 + ) + kinesis_catcher.assign_to_mob(user) + + START_PROCESSING(SSfastprocess, src) + +/datum/action/cooldown/spell/pointed/telekinesis/process(seconds_per_tick) + var/mob/living/user = owner + if(!grabbed_atom || !user?.client) + STOP_PROCESSING(SSfastprocess, src) + return + + if(!kinesis_catcher?.mouse_params) + return + + kinesis_catcher.calculate_params() + if(!kinesis_catcher.given_turf) + return + + var/turf/target_turf = kinesis_catcher.given_turf + if(grabbed_atom.loc == target_turf) + return + + grabbed_atom.Move( + get_step_towards(grabbed_atom, target_turf), + get_dir(grabbed_atom, target_turf), + 8 + ) + +/datum/action/cooldown/spell/pointed/telekinesis/proc/clear_grab(playsound = TRUE) + if(!grabbed_atom) + return + + if(playsound) + playsound(grabbed_atom, 'sound/effects/empulse.ogg', 75, TRUE) + + UnregisterSignal(grabbed_atom, list(COMSIG_MOB_STATCHANGE, COMSIG_MOVABLE_SET_ANCHORED)) + + grabbed_atom.cut_overlay(kinesis_icon) + QDEL_NULL(kinesis_beam) + + if(isliving(grabbed_atom)) + grabbed_atom.remove_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), REF(src)) + + REMOVE_TRAIT(grabbed_atom, TRAIT_NO_FLOATING_ANIM, REF(src)) + + if(!isitem(grabbed_atom)) + animate(grabbed_atom, 0.2 SECONDS, pixel_x = grabbed_atom.base_pixel_x, pixel_y = grabbed_atom.base_pixel_y) + + grabbed_atom = null + + STOP_PROCESSING(SSfastprocess, src) + + if(kinesis_catcher) + var/mob/living/user = owner + user?.clear_fullscreen("kinesis") + kinesis_catcher = null + + +/datum/action/cooldown/spell/pointed/telekinesis/proc/on_statchange(mob/grabbed_mob, new_stat) + SIGNAL_HANDLER + if(new_stat < stat_required) + clear_grab() + +/datum/action/cooldown/spell/pointed/telekinesis/proc/on_setanchored(atom/movable/grabbed_atom_ref, anchorvalue) + SIGNAL_HANDLER + if(grabbed_atom_ref.anchored) + clear_grab() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower.dm similarity index 100% rename from modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower rename to modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower.dm diff --git a/modular_doppler/modular_powers/code/powers_vv.dm b/modular_doppler/modular_powers/code/powers_vv.dm index fa4395ad891adb..67b0dc858dbded 100644 --- a/modular_doppler/modular_powers/code/powers_vv.dm +++ b/modular_doppler/modular_powers/code/powers_vv.dm @@ -32,14 +32,14 @@ add_power(chosen) // Checks if a power is on the selected target -/mob/living/carbon/human/proc/has_power(powertype) +/mob/living/carbon/proc/has_power(powertype) for(var/datum/power/power in powers) if(power.type == powertype) return TRUE return FALSE // Adds a power by calling the power subsystem. -/mob/living/carbon/human/proc/add_power(datum/power/powertype, power_transfer = FALSE, client/override_client, unique = TRUE) +/mob/living/carbon/proc/add_power(datum/power/powertype, power_transfer = FALSE, client/override_client, unique = TRUE) if(has_power(powertype)) return FALSE var/pname = initial(powertype.name) @@ -52,7 +52,7 @@ return TRUE // Removes a power. -/mob/living/carbon/human/proc/remove_power(powertype) +/mob/living/carbon/proc/remove_power(powertype) for(var/datum/power/power in powers) if(!power.type == powertype) return FALSE diff --git a/tgstation.dme b/tgstation.dme index 91fdee995afc93..7c22d90399f34b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7432,8 +7432,12 @@ #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\enigmatist\test_powerthatrequirespower" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_organ.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" #include "modular_doppler\modular_quirks\bouncy\bouncy.dm" From cf33d126c67d83805040c099414a0ccd623476a2 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 23 Jan 2026 23:19:15 +0100 Subject: [PATCH 012/212] telekinesis fully functional --- icons/map_icons/items/pda.dmi | Bin 11183 -> 9469 bytes .../powers/resonant/psyker/_psyker_power.dm | 6 +- .../powers/resonant/psyker/telekinesis.dm | 320 ++++++++++++------ 3 files changed, 225 insertions(+), 101 deletions(-) diff --git a/icons/map_icons/items/pda.dmi b/icons/map_icons/items/pda.dmi index 29ba74b1ca34cceace8dc4e479cad20a55701279..62f53f25d34e4dddc9f61045ea38bfcaec7ec4d6 100644 GIT binary patch literal 9469 zcmaiacT`hL*M1ZfPywYQRiuj`AiV^nN$+r}ks`hK9z~@iNS6RX;DVqKrFWEGrGwM} zp?5+Ffe^mKy}sXi*Z2G5w^q)XlbJoUX3p93?C06%MC)j&klwm~3j_j@syJSa6XJ1gZ}j^a!l$q<#t{yD~)jQ5PQAab1K^LlF8Y9UhY zdZt2jdz(3WqN(8;(EfJlB zUFA967u*Zn8cH+dcBz#sHK6{zZK13Cik8^8(=hQ%GxJ;~Qem!lZ=%S}|KiMOuB09^_eml0htF28_4lFqcV?#YmHw}yH;ic1 z*8>}+w^)jL8K4fa!?@s9-i5M zA@LV-3%imV$y~d!7Y~7cIm85oeY89O+nj*L0Ja`}^+rw&$&9`|HGUg!}5Ygo60=ZsOl1h-kM+$l%uX#XZlr$DfAp zMG+)wFC1=dNuL(%>~95Tm$pc-e)uNKE_q$%T1H!&Tg=*|)StFtw-&0M>Eouar>e=j z>8kssu9iu^%=#-~Nk-Z4870^rg`Ms%CM_uj|JYxLwYU@?h`ABpx}|mxX~G1i1^rk|99}oIKcWckcE#RnV5$S% z{M?2*IH9MTC0MT-XGr~ufP>?cTR=u}hxA;}f6$UKQQ%zmd!D-P$<9@)bRv%^*{kP; z10mu*&oT3#VF;Cg_@jkGqS2wIbDu5}#_z{DM0?!|kv@^3`;?eH+I_6;CKk(Vv5QKW zH_&!zvPX07A`yM`Qq>*R5DklZpAS@Ph~U`Q-R^15SBuitgz0}87K3Ms6g|s(_;iCb zRW0_!e`DlEwxsWfLqg_jchS)mh5HcQ>k$#}DAGe!cZBS~J;CoxU zt?s4$GBn!n{9|SEGGR)RgI=<3bOED-9=S3{(!c*4YA%h&3$uuZKjjg;vMJweLL+Qb z($j5v<&d6bgzU#UP87Xn7Vbs)1rP@jqLiBYd9AMJGAWsF(MFRq8{9i5k92i(M!gy1 zU6m z!=1M`D>l#SOEl@yAr8gA`JQoB@1Du7A2nlv`|4N&FUGQ;+Ig38c{n^=2-^u5 z?#s@P6{3_?VV@7V*!#(hgEa0R+M);84%W6{auE^AKE4QJ?fGloZyL><)|y7UN)nNQTUypl zr#^v$3@5k{?Jq7ZMW^~tOr6M@5xP2%kmCH@aPM@-V9drh%xu>jg4TE%rWlwQzxz8}BcwC~Pqu zfA$}Xb=&&Zb?&IQt1cqGsk`!SFyW@C_~tO{>pg5(R!Ud<{VXjycvw>Rnu-ELmey18 z?7SIt2sqL2JoWi*!mEi}RLyzJLoz*^OvermtC-C>^75N1GWTJyy);`=Ev{O-`D@cV z_9I$li1%Z+C&nj8v{+R4QUlHQOeV7xg20Jmx5b^e2#d7qEKduLbA0C{3NAQ~!-3iV z+(6wBD1!Z|#1H^aG5S3_=9OLeL+ zi-SWohMIk0aclA>fM(%bR?;gu4h03?(7Xtsp9X%KU0m|nK6_NrEcw$Bd1KWD^}1h6 zh=HTw#s9~4dWUPH_1!a&963lIUdJ+H?9w*&aufstE)*Pe?s7JXjvMCGyWFov{Ei3@ z*E6rxiL%0Qx7PcdoLvg~YV`}lzCSBcp$8~M)aZuQ(1y;~Uwga)sQcf5+TG)NCrI~W zUf%1V-#dQn92^fGJ|yVr=>ffpi@O85o06QIY$XcQwY1zV*-~$3*4gr3TW-F+K9(U+ zBSShqJr7Ab=V#qHN%q;RBmy?~<{2D9B&V}#Q#qk;PT+;OY*{_47b=r~A`=%JHxab| zljv%w{}>ghCBE97w0k(5BCa-%Lcs$*zyfwBIXxXDJyN7=y1@qzFh9IoMAa4bm$(BH z5}->;Zh-%RRY&0;i}WR_7mw^$i<|evIUXu-`SP|_Ft703?~h4KO3G7LSJxlIr=(CF zSL7Y2vWw7Hg&k&tqY9YgOfmBz6snrarO1$?xv&LgHrERWCQJj8BX^>&%I>efxcSYw z$Jh^MJGe%EE!&m}WNJl5hdkx@L=(NnFZ*F+WTf`=G(enj+4&+d@h%lLwW^x5rQ|B9 zr?dL*tBx54W9&Am-GJrS9UoifY4#C@Ut{7}>Ep94#QCy-MZEzHoMYiJzum8A#@jUf zl$1)}32m+OxCx|gSXt+BO%MnP>ZU9cY77J}`jOHQ(e#CGa z)@NijXJLP_?FOy~zI+!Y`!S1~n?=H_T>!^Y$i&q-*WmiurFlL13DPL6L#a|sElHqgEFtTpe()^y;p@Pfyo(JMjq#T%D5AOFE4)Ky6zj`I>a#C)cvOHZrIPa+Y7k zn$IY}6TdBmo0lg)g!P}QI&p&*a+WWXs09j_+{GHw)|NlS3&aQ*)ul^BzTaHGiD zv>M~|B%7~d*7e7uC$E<~gjBv5tNNfnl%&Gv1Ioc?;KL}vaS1W&G-*{Wy}vWX_HG`I zn0Q_PB^8V!3Ye?+4MWkcQc>D8f5g0VX?D1twq^(d@iYBOt(3PU=)|~}%xD=^!BQma zAKxTrj8(LAJ(GxV%>o+Gz>^dGOwo>tYcxGHm|a&oBRV?C;P{nZoIH-F(;heR%h((S&zV7v zhP!785h9D?QU@+&U-F4+(J$3pIF+3UhrKwY9{0hhpe(E%`UeJ2m%9f^MDZYxys4K`4xp7824BP!jNpk8#r* zc99~mxyg)ZRDf-sOIZ6uO5KU-cqjbVd7J&EesbGGNEY^*-7#~1=B3&$!C#CY_+c&L zY)pt{+1$*_{zy*tJxbYNo_Fv@1l?eWgVRBXArL1Q20KsVw389FGsIL-(N6o2%#dNi zS!ruwq2y&L7hBG`HvK2lO`E9ry`Sh;V90R zre1zJHM;vOdmtj0dh^scrh)D7+~Y64!1B|NNUkNd@1(=LEyQh14Ifyfh-qSvhFxMi z!g0T)>2|xrE2^uN4wZs`OC7`2|C=%BK>G(JyU5;(pCHi*CC(OhTVj_di)Moe|H_ z-~@i=&J2Q=i_pgxHC(KZ9^ED)XFj@pIj-ft035ZayD#fPhE+f63C}Mt?HQaf8w}T3^QncI zjAt==i|#J5H5}7^oe@V93;LZbE{1z)hKY*y=M==WcudWh`<%Ol`$Er-xdl)9W+F&> z$EmRa&K-P)IF{AHHDyI5o)Va2)zW;Gv*CfJ*_xNCzv8Wlj(AIjxW`i8@eEu$&O)#nmK(CaAP)pm$6uFN_Z(#h&HD54=5lP3oe~hTK20y9 zMC?)g*Di}?mnE$H%(owNf6#m^hyj&faOb z7Q*|-?Aq>u5LbVG=(}XEVoh0hTq`tbaGQde#?L_SG>IaS7Rey#Vu>4WRd~8_+!(z_ z%COn3&AP78Ci3TRmMv5|)Z;T}Q;yg{la(*Q)HP94#ALzbA!oDL*J2Lx29<~#jw4%v zlt>0cL&JNQ%#@V*s))&~5EBw&(4%>?-NVU%soQ-Qn}a7KFqX6BmP536*veq&6oxT- z3pr-TujPJbec}Sgf!$}4OPfL!IlXIti_bTF1 z0os)_#+szNT~U|s+Q$rTHLvS<)ZQL)q}qJ1Q81zG@pS9En##u}t9awGeP3d1g!Azp<6qD9x}%Pvo*R z#%UFMKxBT2kGDiD_rJ96_pHgNgNA0%#lMQkr*6+U zYh_4ScibUAMQ_oX_i@sT?k_exGq7fqFeM3 zDsO*+#z>H@v?#FalZ8x6L=e>&%q?{gi40jK?*@?h!{AM;j12A=EP;?LN`JW~0Q$o$ zd(W9_jJ8EWS>FBKmF2Mh(0Etdw?eg3^CBu}rHCQ9&GEsLajEBe-e*_c&XWgm*XZE~ zJm`;LRv`8%$sQ72F)KfYaA4Akh--(8J-?GF$zxwOEytVk(QNkw(g(h0^D(LlZguUA z*%?=VSpF;w$?OBjm4w|CY>n=!da?s~hA%Je+JKUS%W`7%ktY=um9^e8Vor{?xw*M_ z-@T)emvG%c0gWEOWB96Hm*n3Lb2vw>q)z14jzkbPFp*4)BRn>n+zOHSV#x`N~#f zpLK!D+uqL_b8#ILGcQ^iz*^bWOZ(c(Vrwf=_230C9xp5AAgBGa{t)uv0=s*fKLi)p zGT_GtFG0+z!1Ge>{iu3>gA-Fu-lXFquwWU)8xLNSU6c0Pg+%E910EkMnO};pA+Y>nI(uajK0&C^Tj?r;NALPx2 z0Z>IsvaI@XcJ1x8%0r@~teE}w4YKxH@X;gM6adQ*G$`_JOoYhfc6*D8GTlFLR1IcE zGE6hsHVY~uR1s^Mz#W-kN!%m-QhE#+snB0RSh3P6fg2K?HV8o#5Axq}fHqcgG+!L( z8J$hCB>0^>o)h3v!bnQWOAMtu;IaU$I9g+_P$Y(f^?wO=+A9qWDh`Qjnr!({yb)Rj zax>*|s(Djm2L{LoDK%VFVqunt36FFBMV5B6j|g8jwR+O;FZ*behV0>?69RY_J&q2z zCJ7YHCDC51&B=B4u0?vsMrj93Lz*&hxxPmw((+Rf_~rh+{Af=VA@b9AP^%eO zjO-;+GuY%J`H5Mz%VM0eHd4f~d}gO4IdOTA(@;rCsicLF0_qYZD(H6^htE7rFAD9y z5y=gkZrk{5nEkfk1QPms+xYjYOl0mhVz9*@-VpVDHP-&=8szs0=h^_dzJ_5H*)V&G zG>4KF8P*sHqoAbhcw^f=2iJ@#IIG}Ugh&Rz0|MfNJ5H#N{(x1i!}kG8f!fMEz8>B! z_{mKWkHbAe;CS!ni$D`-wKd#>j`$q~mZCWq+Baj@i-fJsO+J2Q>R$iUwc&&o&i5LV z@f8~qxu?v7cX11nJXt1WP>ZZMv6lc>XdJZ&tZxR+4Ea+sB)VaheR3Vftn;#cD^{`)4a(tdoD< z$x89UF4GofCLpTlz)!H-%F@77jXrs`Va{k>O9xUa7i~;Ym!md*hgHLD*ZU=GyqAA- zW<9Hd-dvHHqBSw`8=IIYnI)*NKbzzK-DYEZsh0*Vr|~nReiBj*If+$e_y^|zd>dx= zh$y>r;bRy;a|T;z?&q&x8Q)ErpRp0w*{ zSM+V7G_L;jp=0k!U^8ca?mQJYKBN-c&ysjR4`+juwmtzO8~{ckokiWo01fVADFsk1 z^Z;f}^J0n)e&G1uNOslPK0g<5;tq;L0MOVW z1ii;9`e!r^Mj@r+)29bb4SFlirAA%a>paZFBr-2vzT_w=DS4s29t)_|4Bz3XgarS` zuz1gKuaFSwZIV!3L&JByYL}-KA2m29%coqZQdX`!rArI>3gBMmA(--s5Lnf9;5Kca z5I()%(CRYrY54bem5V6DjTL1Zh+{LI+WmDSqBypHG`0a#r2IvE8GAc7Kt^>midVEVh^g^n^aJmL- z7qJ0s1ufr7YyR}366Xq|7pTlh5%Bd~6F9ee*N-3f1uY z_Rg9i&0;Pt9u$|%1>_S~WNUh-XkaZ^pGtuCeX|N2 zS9+u*Rg9+P`%5nMa=IR>w;~{w$edX1P#!Fo7R7hZ5l!&;9>-inw-ov{=&Vf^EgXS) zPb(`l9(oFSTE{h7XH}P{44Dv^d<(yPZ+Nd|MqM{^e{^Id3fCeaOA5-UEn)d?siqdg ziE~x4p{jyTf8*6Wl{#C#eM-W$$Jt3uhBwg(+qABs+0*99d=;o7yD+UtGUuVu(4(b_ z;%q1BH7@lVSN&aQB|%He@b$CX6m`)idq1?Rw~EB@gl~JiUTs`8BQ34--CN>|4Xs`F z2K=2Iq31e&;*2H99y7yq{Cu0DO1{peEhSp5OrhwyZ|_|Rx9VA#)Zzvh{LH%rno1-| zE_Sqeu)YOzMEtdBw-Je4`Ngsx<5kjDl^KG`(xuaUgUqf6hn`FFLY3zuq9zBFL7Jl} zdw9uyo6#OH@vfj=c1XcyzrXX#4y1_kj*t1XmjW#Zx&C22_pCLlipQ#Ep%U2Fo`0j$ z!GAkkr%5v;0O5Vh%zbSBUMs6h>%x6{bgQpKUQ{@(On9|$s>**aU94fMB0udfzu;w zh%48oW9SZ`?tj>eE&Xqd{0{kOln%EJ^~u9V&rKhIzTa4 zW$yD>Stn=Ojrs1}?ROLA=8Zu{B1osIR=bMDvQA~Xc;$8~svizd>(iuYXm0f?=il?U zU}67eT3UjozQLty7{shf5ciy};^x8k6`<&>nk`cQJ2&fz9cj`{x~pcu%J8r+!Ll8A zHg}>#^19v^M^&ZTjLrJeiTE>FyRNJ+eWgkX-p+`*_FdV0fL{?i99x2I1Y42k_M8t{ zdt!BOn(;+l>3dC1${Nb(!`vv!C_)Dm}YT2_ia(dueY&+q@5riZhc@ zGlO>C3g?lrL=-;KGZ92NiS}uW#lX(5lXX4gEeSr@H0ghyofp&c;^%dKUf!6~n)ut9 z^AqrZlw(hdQl37_Tr+t4&dzHvX~-;t`@fXy-}?7T09iX<$d)yBX$<;N=7KIz z{jc^bWvebpfhutqpuVDq0?;%4UN4oUV1;0&DV8BRr=dTR^!ta%(1&er{-{}JhVH}B zw;OH$5x4id@jgLKmdTcbu+#Y=6P0R9GP3xwf4lma_Q0VY1{qgU7uyEcsg{{KYq>}m zguJOk;1Q6EDOpYEFxO=d{oYw>OBkLLxdV|JMc^d7S5KGSe}DUlz$twZ3PJ1Ba-FAM z_U<0bV!v?pyBgx({(s}u%oJ}G6M7pG%TQ+SlQ$-WKG?42vyKNs*6H{ekFqRfXQ}X>DzN0l1A{sWe2&-OC-iZ|{E3CM)b{ zO{yH1@%41EFZt<;KpOS}F;aWsJuk`oD6Fm3Die*FFo-3p+xXD!pz4`N@*G{1%vOrq z$kO6sBqItt*PQ>N8PvqARirNh|Bbf^R@`zN$D_A7r92LcTDnWbUf8u1)?00!7M0al zMfMxR>?9b)Use<|+kpe;(!CCI)XO*j*0>1Ow@5|&ZCU0n8Bxx<$9FUzanroapG}v& ztmB-Zu&BspC|CZuhDJ)qV1K`&kx|-q&S?7eVfg!l4K%^;e_310vuS!*Npv7gLD;Te z&eoQ_wRJgLJ)TbRx`>EK!1+$iMWwxl#%4#3eOC(bQ3e6y)wcj|ffW8{nmzK2oSgUl z{Tfp^;8ajlbnZQQz4f2b|0vVuJZCw7=u=SHOK882M*Yo{R?M2n3-4<90Re2V>`J+m z024)Hu64yTaK-K=oWJrq#Z#e($l(7akm-f(UbIKuE5|-mj{6ZuFN`bKe!Z;Sb?WmU z;@JX>5!67=-HE}3{*XH;vI(gV5xvz5Ao;e54v=m^!@#Vr(E5{|8U2+}Hz>tTy_I}p zf%=d;pZdsSQ>Y#V-ReCb<>2IOb$H4qBs8+3LQ78{O~xcie1(V6(b1XMQO%D+H?}4b zU2<=(0ZflcP0c*a1vn2?ebvwr{is3b@Z`nC*>Q%@%NQqFWLSgDHK35` zn;8R=83W4lQw~pC-L!|kUMQ)k>*QFx1`v_`k-O1_|626Rk~V%%z!htF=$%alxJaIjo{GlTQ_^1nT%1O@ zRa0da;$F%}VG3OZ?`zHX6w5{*)Q1jBScPCmqg8JgF+OJT!^Cop5I(f_I#cCvM{G5O{J4X8vj1PKNq58r8{LLPtW}J796f3v$c-S9k zRV!jM07Q6-meOZYY`~Qe=fi?N?mn-)*uX6ga;gCDT`>`4kFaB$?7WKq0_3-8_4EAW zAk82d_od*rlKWogHf#!ipmc{(f+b!uyTEeLqI{>v;e6C_XOPpla^~`E_oXaWo$KrV zqs!Q-=DC>5#$oX4=f>vwx>l~sPdt6NUWhPC0*Rz=H zQk5i@No!w@aSX*gakeV+;Y~_PnwrtOtAB8qFPnXJeKoN;6u64V5imlip-QK=F|C2`3dN zZjeE4bs!Vd2;X<+7i~3<8R24K^4&m69iEZz&_wYu#y~!uq~tHLX2&Hi1svb`8khs6 zpMT_i-Z`Odtv%IXr#ATJbLAc zSBCo&!IS=2j};WZu`A_J@_bLKE zVwo|V^_D}ZyM_Z<+AqtoPp>$uxRQdgF>YFKm#9vej-#W(dyFys1%Hrifdzkb+dy9W z(E0<$ZF-MZ@es^gA1COfNoBIvfoJ)eVnmSaJ-Mw$OwM2=sSE!;K{pwlhq?p=(xaH| zj&>qGxl-MT*X)X=1Xzr8zM_0k|4Vb~Hayi|eX4A$`wLdxreYK=C#OaOPDE7uRW!HY zn()&^63%z);%nEVCBe9<@U`=<;gW|-C%im7I2g2d8wC|UTLXlOE-s>EzNOOu0*v1^ z8~r+f|E7>ez4xSh31(xK$MM+57G=_?wCvCC*s_7G?9$`YL>&gNbS0#w z^ExTDu_JqmHu`pCq*VFJP9+rFOW;02;T)^+ESk=Iy@}=z`-ie=U3m{9BEnc4ao(Jk z+B5cc3iPoaj!GsH1V4Wf*g}WfT3q)fXVw3kr;>&=XDFNC(?rIda5mUz$-{qY((OpP z#K>ZL>E<#=45Hz(=HGyWl8TD{8AGf=Z?c#f*-~W=um>_TD$YN5)-U&N7Um_UaF{#8 zk^pH#X(-w>1M$qvxVJ>o((#)04Zmnjym!a9{mw_yY)H_VT$J_A|Feio`z(~RnZf$@ zLS9i33nN3rs+?WM?f{T}!Kxkq#g_<{TnO>|e3jUq^A3xC9mD7ZNd$0*MJ;__70nWN zy$5ZYMI~4R)P2>Q71dupz1>1wHQiN%8ripV=jE=(Quc5B&n$$r_n* z1mwd=?kqHom46Reb|aR~BeK~Qtv<2camIcgO`PuoLBX(mAji9PCNki}0@EjJTLq^x zpU7=tQ=7PJrONJc7m30A75`EJVd1_5cWv=UgsdQaDw1eRunO(^>)UFEQJU-5AkC;e zmp72}e1KdL3im6#v+0y^PIcv_Q-|5Qar?3b0<@voV?9X&|8g3LJ(cW3rQ>}13hm?w zM=wS4>^tK>IJTAj&uZBfg+FRWo$4_l=ir;L)ked4uX#t~Gvcp4m5HAlmU$`9o{>n8 z3dSth)FcJRb>$gaudP-lR5f>1nS@4I!W|=FtwolbO#qrB8iCp3Reuyk#C(wlJ~ckv zETBPVI+8H*=8T+}yjtO=Jr`ZEwzsB`ob!^poGy?^wzajEi(~+y98KTJGN-ZZBgUfR zU#V3LpL{zfjwXlQw&$ul3a70&PMY725f`lbvkoCiC(^Vg-_KzAurBwD6b|e{6Syygt%1dX4^M)Y;fEZ82Q}>t^ES1)%X}H1oGtPN_ z1K`nT<{dGOBRfea>t^5>MY^dPj;#~i#>OV@<0HJA)kM3Xp%&^MC2C*?d1Qk78nf-^ z0wb%7dA1D~{=itquFv2}C19IvU{}J?OVpAY0~I$l-d4C*UToZ$uN+}?!jan23A+p2 z9S?)?X?H{F^+&{%LrT+UF$`05;rnBPzJ&Q=?~&;znuAw`%UVW8QPah$&V!j@pCmI{ zF2H`B&)m45yqrCF+arjH(RKvo+jp-K6!sYjZ)HQbx?HIkHd{oyP9mt8^!QAxZ}aW3I^--%m+5@&Yx6b&pF(d z72A<4mOnGf-?dR}M5Mau0=zQPGArZJHN5N#9YQ_%(VstOEG#TIZNZk7MR4888uNCG5v^ zqNKTpoxkIrQWBlSGQ)Jhw*7!EsH z!(VvT5mT$Ryffstz%lhqs@&z`_F~|f)595HgLUjnP*MOLt>xGosw>>&o*Z0NY3v{- zY)|}N(}^Hhz+-HA7WUs^$IS;zZ|Fqd=-wCpI4V5^i#yTlf-dK%N)H*t+&Nfa7S{nU zN%fhgW{4Z*X3n=3ADrvLAQYi%>pRV967l+;axEz%!o(-f2Ugn=GBUS~%YM!8-`%vpah}|_RLSPD0iK%q*Dj~w~#Je zvAUuImiyDQV`3s8U!(YNd1Wv>6C>}uImpCZjo6X2@*0YCq~-Nw(kRT;_V<^QVOq2D!`Qo3l)t+Z4Om6x;^)Z2cfC{;uBWHn@=ByP1MfC z$_zx^GuocXFTzfL!D&1!p4=VuP@J1$2a%QeNOKM__5>fiMqX7_w+KNbylRpk0fjw8 zx`sP6n^3~3*Ji6T8#2DY%31#+h9!8Itq&sqoQGdvZr2K~2I)MiPv=9gWQz3q;4r|m zX)Cw@On3E>10lFc+DiuOpn1S}BIS`fGEupO%Yo z0&^kro<)T2(YW7w1eA7TL6sAJAX=Dm+tvE(s9fw3;iG>igzHr3&5!KJgV&%mcphGh z%?#VIX8c)AE#O4Mo~BV*q9#(d9jOLrfGwKiqQB0(`2fT6)4siRudlQ4)DI;%<+EYX zvutS#UCONE%0nbx6eqhLvwla{9)HD66??^8WF6GW_=d@rH}T^n!6Ebrpfpe$op08N zVp6?dfTq7ab7!(I6+;pfcGljfTYsqhS_i!b#|IJ=YV?=h$x>sV%DwG%s4*y9|J7tr z$)Um#Y-TF8;^E<70Bpc%$b2bpVMZB;8Ih71=3Gw6Y+3Bnq%M@6Bk|GmMcm}8*m=)5 z3AYbKjq-z9?=5B0M!(ja6U18AGG2G4$0F8#DK&iH>n^4h@_#;M#50qquVv`2W+eU4 zSCrR#6rJ(S)IbVbQsyrj&F-8c!txLy3D39#BT8kviWO1*M zMf-1y_5(X(t8cSYUUE34T1Y47mWr4h_vZ#0%)(n+TZhKSLq-!H)-OH|mk$YkY8T6A z&i(QU-ODHWIrc*78?LO+0RRScKbc8z<6in_^g$0gt1|*k%BSjmg8X77(iS2f@vNiZ zEr#UTsz-?)No}YJ6Hy);dd*{gv>)(dezdccXZ|6bUp^6~8b0slV(dh5j99%5>R%YI zZkN?+dQZ8LS;$*5b?><*;izL@k=KTxBUeegIgQEu=RAEn(L97b6t&5B@k22 zd|ZhTEQ1o$iS@6#jY|fC!XZ3N13aB)3NoU?!c=Gtkal!`!wzI_c;e{oQDYBMF3;mM z4%2eDIRB}C9V&f4jJYjnCM7Wq)Y41$lju?a>;Ckg*F1hgY7@Gd7zH}Z^ZbGxSk(6FqW=Me1(b4k-^LFYWeLP$Y)%y;X-__@vdr1#xvmLIM&v0he|BOt_ z%!1DoyY1YoybaSEMUdzE3JTK5^=d;Z9i-+QghUHGLU0sRmK`5Yt_AK5#ye2~dOmnX z{rJRm91{&oH4i`WarBbK{6~v%jyqz<-4#QXAPR0WJ;m@D` z4;D;-QuhXvKbX98xMvwy6$0hu7=`yd--y26s@e&II;D0nvMik)5nFeJO9+g?GM$f0 z^-uxL&_#|3AYxvozeN-*!pU*};l>_ujU%w}aK6&6!xZR8u~(9L$Yvg(y$bU_k0uZ0SIIqzzLw8Ymlz5^FI|FfO*%rF0XZ9{l>9}t+lM5Q zl}JpW_f~@yV(8rVqnd+mp%vmpUf8Bg^A|%+_tZrX*b!BIGdWT{paz2SP<*i8p9MPy z;BQV`4RTdFo)02Q<<@4>#Bo~R8l3cVzko56Rx3vI1UyJR8MkvB*n#`J)GpP$!s$2` z&Qjz8cjpfpE_EW`ofWX(V`N;oR*iiw@=`S>=hu1llOYDp10-`+r~tL>gLgCk5J-bJ zxI1dhPF|Rd~o+oDVFGW*yE$u2am;TDN^Th5{0x|w}E00ca zv%xhmc}PLws59oxs-4USo-7&$vQ)+|dh zZ>D@4Og@Ak7Gk1Si^H}%=Bu!D1^&B+|6DvQT+f9B#Yg|(m?T1Ay@;NOn4_R6DWJJ-e0>W8TYo!&yQEI5z+2hWJ(XfLEzE+A`Zdkejii`_ZjG!7493~#fCd=~GrS>FEjiZ*Y4WG2N9j;vDPm5; z7p)z2tbbOUP0>%@02A(_r!|yVlw?$uOZrZy4?L1X!^+3eBt^SaVY#hW{$W{Pieip|o-O-DP$OU!xoJn}ak;4I zbM0-2y<{`dy&CVQ+&)2iHNcJ}2|=*iYX;}n{By3mR=;4HPQK}vdm)?(>c&CVWGW_c zn=_mj)m;E>ZSDS`Prpyb*&dam>{Xy(QI{{YZcNXu;goaPKY}{;{jqLU%P7vS=I^DW zLZM-JyL&*;Mu&C89K0R*`6GNQH@#53pIp6L@x+Qx&Z@s93}4@gQCvTI%kv`TtteE# zmV7_+K|Tdoh#?|R$yD+dar4f^tV1cv`6Kn&XN=lToZS7$5R2I(fm>sd zFwHQWFlk3f_UpX*_vQ_@5cEWaU%}_nk`l)+Mt9Ut+KQd9Dg-sYF9yH*7Zz|P8MKxK zId=}@+0`1ey5{(h6aS*n_p5;P*3<8Mo0toq=suGs2h&txUnX?ba~;zz<20`dQEy4b zys!Rq-ZM1rm0MM%u_K-{S_lR|c<+DW*6Z1+z*Mk&xC7#E_;ULfKQ!N>C00gZ zc{hnMVOAiD=3IK0!f9W{*{B%Bv(J3b^YEnt^m>uEXnk*sT-qh~ussK5%?)nTFfV3o zx@I(55K~O@Z)V@MaiyAJ$lV-QG|c{Y*zDC{Gc>o|VWvt=7nf8Y@>ny18|GRFXp|s= zr-4o10LD4(IbWcLTRherHz{Irn8WXDd_H{zs!~u_Cp1zHicLwu4d)>W4Gqn52(N9=$w1fN zPnpil%;dC*iJle`)j{1>B>)+KvZdoDU8`1bzTFt>V6e*3yXj+Vvt%a_>{m7H8 zuWhzk{>YC%@@rttxi#*$>sXK+x_eBAz_Z9e3tjTIs`#thNa@#w zOR^#LRitBW3Yv({Q3EgvqBGlTuoZ0L)SXa(n~;>`eaEM&I;y2-@|;6cIV`}$DWGv6 z6j;`eB$b~J_C9HxZ{aa+dl+%9=;0xl#BEsA3P{)47MoR?1>Sj(@_QbDgl(&``N5=A zbj!MLjf?Z~kMz27y%D^SYg~SzU9cDaNDut({)YB;aTkc{&~QUWR*!GNJ#=@8Z&h`< zvp}zF-X#|3PMY|h2+EhgDOH!EFI1f@MgBn^oL$wk{ge$Ne`rw&eZ9-8J>!o?yRBsu z9CSvu#iR(}<-j?*T<-$m@H;Lwl*&xLZ==ZlaDn-1H5M(jYGaSbE%~Sg4q$hrkHJk> z1>@J>eyKZd9+|d}9}rm^fQ?x44v! zx^y$aE4e>iYL;z7Bh~v(+J$VFHGFSW1e&1HddZgv4Q|1#|2N{rW(1s7n<&jbIV{)2 zS_k0^r32Swbit^*TOAk>h}0~DhhIAqDH{s>3wZ)CS#DBR$!{DIf3l>Z%d{8NeeLYH zA!@Lvw-9t*;b68w?rr|~L9%rou%Rbc>3(3bHf`U^!h#fb-}FZW|;@|*y?+yr^X>z6oe8pMY68nL_>ikoDSVMV> z{(y%A=-Q&gZdP5yYIGg!Ebqa^)1u36Hi?Zh&#ArmVL=r(TNclUPQZkJCxK7zoG`Yd z3N$*^wH&RjwK9X3i3Z%>*3NU58SaTxi{Sxiwff~@`z#Cdd%tl%+HcT>e1-2Mqerhn za6v4j$~ICznb3l-}>EjW9f zMRfV+@da_9SA-5bL+Rb!nQq;t6Mea-D9`i3BqO?gV;#=`YIEUa-=+|r=fu?SRch`y z(fNUbULuT}@nMz8pv3Hs<3)O3U#A3}1sS2&Nn_(lNwbY!{@hL;^~G%M=k-6nYGBPC zB#Vqaz#w{m7O(4Mj|}2)$@mdTU8Ir?^d5C=_Pqj~=R@|(1q1}F>ulevdt+kc+-=kA z!{AY$FH}~KbPs@HdJix-ko&f0dM|QK?yUx!W(3ztRDe-{Tw!e_Uh2QnDrld$g!=;4 zXk1c^Y|K7r#sOV1X8k9+|EJ&sg~7w@E=S#(uj+oIWnV~~)FsX*xLS6eR6F)xjs72w zabSAC>G?O3{?GCMwZ}Xh|M76(E-|jP_;bB3DGTr$i}-!P>9chRKo`lPKD%?o{%$Tg z%$oRf(KeIHi{t&L=F;$#Q|mGwS?x@EWe*<#-DDZyyXPFm(Fajk{MhJebSc`-Lrbmb zzQK%}#mzU#GOz`x?Q5PvRG@mbkZ;qoD)J=Z&ZP*Rpu@kj+c!9~&L-t{AU9gD(&C>o zXTeh;7>hPlR?D8ECR$nO29kMV)~I?Bw}W6?P)HwU@pNcMo8Mlg95_WOg6_VENnV|&G=Qov$97!1+`qULO;KwFY8e5 zmCjX5e8x_WN;LftNIJ}*AsbjvmJc+FoaC)iKo^Bu${+1)6BnDIucABUT>O4I3-17! zy_(nrSvUIJar9N{rPvF}EZPe(KaDxotmqf1jWm8}f4A5HD`Hph7^2JzRx*L1h6tKhV_54W`W*}&u z@-gE;76XS`%Sez_jr-3Ep%YC`8(o3*LR{6C+gYro5WcrF|2usBH;RQ6;ub%aB*wVe zruLp>L1SqCKf-aVox;pWpZH@267DLaj}D9(!{TsB-DR-UN zg%SN5m(B^s&KHMHDcbxK!=|SVq1=7`i(&Vx*bR9@6x06VZqM~MN1WSB;^@dJbVRGj zcZX-O9A9}meNS*>_h+;Go=OMY6fF#3(7Hd@Kf~4q|LTbRvJ5#~%sCuSea)VOT57Zl zD!achT*QREf2l@Xh6>EIdvM4L-u~gaqd9c?rlRszHT`i9R^)iEtU+ub{`*h~D3975-DN`pLC>mg_iQ+u&c5*O@e}64FsW(Vm{>Ts0X<5!)hkM%?0kX}RLL?4YEq99swh-}`Ux z?Twy_Vv&ZS+|{zH{BaTg$U=z9($n;^uil11`t_DQpI$wGp#s`c%)r3FYx=tvV6Ang z;Qv9g!y3cHx|=jV(iDL6 zm=Y{jr1?I3^^pY{&NfP_!Z4m!8N-?Ay!<`w63 z0E|^s@FNtM)!5<(qtXBMa}Apv1M4QObrigiz*@C)|JnzZSL!pyQA4(4j96JTXnRMIwoCqQo+lo^AGy z4JomB)|py;DzNgE)-?0B`ouEztp&X>`uz!)Ai~uY-t1hHTuWb98vA;TYZ$#woX0(H zSj|My1!$P(^RrK^SG$R2bAThq?p6h1p8{BSzAn{sfiI($N>U zNXHBh;qUZ7cl8u$l>0~PjTv%#un@n}iEtE>nSTy;2EiRMXaF3g?-*^;&2yF6iGTR^ z)0gVs%wj?>xFTcCRcieDZE~}ry02sG(fVo!hVfEV$cXY&Hu%*SDzA$P^R6HThe zU?EYo)LefiBr;SiL|XfAT2Qt<`)cq2_HT|b&A=70rT*U~D#bBHbSk%W{!*by@~Hz@ ztP;7n{iYr1bZkVc1)gM9MT4Tgf@$;SdNo0m=!hps;J&l+7=J8os8VGJJd2M29L(D$t&eQ~Sz8;;A@E!H>U*px2~eqhMBWU1qZxx_tq4 z>&a8$l=7RR{?`7r@PSvnskjqXb`81@?$KHo@9WpEUWuSufkqhueS~X@Z>s0bjboff zUoQ#QS;;PpEt5EfDgIid;aQ^~&S6#FQa`WBY_>sRWBy)tg6v=%;B}ifFS}f?dU0uB zoNb3kLR8-e&K6n{S@}`RVoP;RS0j_%^SVd^R2=Brlu!;ptH4U$L2oAX7l+fm3$q zzC3&N3Ae1w;O^WXe!#8|JL|aA!1%+$05-O4UhcUrxvNEzQc>aLPvSN&bNhPBHZMos z+c`0n-Q320{ z?0;jJ4#hlAJ4jb!OHun`#o4S@|4JRunEpKdWU zo40@bO=qbCkdp-W<5fCDqCK<@>!n!X2Gf%;*)K#J#Gg-BL}gMn<<{3UG>8|nmKeg| zxq&a7aEqr8Ttaheg&;a0sf`;ssJaq;sidi|9k}Emgp6+U-4=P2rW}~rZb6VSzws{v z80=pk&Z;`EdZ7p0Y|%#Xl+68emr$V-;(gy_?NX_1=ezqZykX{MoX3yAZC4cx_-G1} z=7xeaB^%qY6UwRu>5KshTF_E&ME6#*%%Jku ztr5PKZ+av5l7r)qIGMtsTldwJf1^$o?38fP2F?DA*2aP*e1bSwPMruqrFYvR6Lq!BVB1L8)`Ch>FkVHo2Q z*6FY1@Q!$ICG41XA*u~MF4y_pfBu7IpzFidsYC6;PSxvMYw^y+>>jfXQ&ZY3SScbd zOudzQY8fJv^$vB54|hVz*_m!t=yG=6D#*xSo`d-hkB(e!FDzP&cv$i+!rr#b;zw;s zEVHGnYwd~<4O=UtMw5B%;sSpB*ipj%GxO7z)Uk21H1+zQJKv!jV*LuEp4|B~k+5rK z$-D}H9VIFyoe_eNz-yM3 zzH>YkpG+(ChiRm*{}c{A$X9~Y)bRa2ng@bG-5zFHV3aqJb!rONwR-|E1rK8lbZx9F z*)vD1=O(&{YHzHC=MJUUYLZ-}67M28w|d5V(;KT{w!@OmtFNz*348tewcvfPZ#D4? z$|S!jQ@jOyRDhyu6mFb?%?1CP=%}oK$_vIUR@xw$B3X&=R(wk@?iup5iKfj$#!={v PxERXv8gf-{OauQ1oL8{c 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 1e45a8ccf0971a..e17b82811f6109 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 @@ -9,9 +9,9 @@ path = POWER_PATH_PSYKER required_powers = list(/datum/power/psyker_root) -/* + /datum/power/psyker_power/proc/add_stress(amount) - var/obj/item/organ/resonant/psyker_organ/psyker_organ = power_owner.get_organ_slot(ORGAN_SLOT_PSYKER) + var/obj/item/organ/resonant/psyker/psyker_organ = power_holder.get_organ_slot(ORGAN_SLOT_PSYKER) if(psyker_organ) psyker_organ.stress += amount -*/ + 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 c028fbe0e4027f..d6b5691503ee89 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -1,7 +1,7 @@ /datum/power/psyker_power/telekinesis name = "Telekinesis" - desc = "Grants you temporary telekinetic powers. Passively increases stress while active." + desc = "Grants the ability to manipulate and move various objects. Generates stress based upon weight on pick-up and throw, as well as passively while holding an object." value = 5 priority = POWER_PRIORITY_BASIC @@ -10,40 +10,58 @@ /datum/action/cooldown/spell/pointed/telekinesis name = "Telekinesis" - desc = "Middle-click to grab an object. Middle-click again to drop." - button_icon = 'icons/mob/actions/actions_revenant.dmi' - button_icon_state = "r_transmit" + desc = "Middle-click to grab an object, Right-Click to drop, Middle-Click again to punt!" + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "repulse" + deactive_msg = "You unfocus your telekinetic powers." unset_after_click = FALSE - cast_range = 8 + cast_range = 255 // this is just for show. aim_assist = FALSE spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC antimagic_flags = MAGIC_RESISTANCE_MIND - /// Range of the kinesis grab. + // Range of the kinesis grab. var/grab_range = 8 - /// Stat required for us to grab a mob. + + // Stat required for us to grab a mob. var/stat_required = DEAD - /// Atom we grabbed with kinesis. + // Atom we grabbed with kinesis. var/atom/movable/grabbed_atom - /// Ref of the beam following the grabbed atom. - var/datum/beam/kinesis_beam - /// Overlay we add to each grabbed atom. - var/mutable_appearance/kinesis_icon - /// Mouse movement catcher (for dragging) - var/atom/movable/screen/fullscreen/cursor_catcher/kinesis/kinesis_catcher + // Overlay we add to each grabbed atom. + var/mutable_appearance/kinesis_icon + // Overlay we add to the player when using this power. + var/mutable_appearance/player_icon - /// Psyker organ for stress. + // Psyker organ for stress. var/obj/item/organ/resonant/psyker/psyker_organ - var/stress_cost_grab = 6 + + // Mouse tracker overlay (telekinesis-specific) + var/atom/movable/screen/fullscreen/cursor_catcher/kinesis/psyker_tk/kinesis_catcher + + // Reference for the base psyker power, so we can call add_stress. + var/datum/power/psyker_power/psyker_power + +/datum/action/cooldown/spell/pointed/telekinesis/New(Target) + . = ..() + // We do this so we can call add_stress from the spell itself. + if(istype(Target, /datum/power/psyker_power)) + psyker_power = Target /datum/action/cooldown/spell/pointed/telekinesis/on_activation(mob/on_who) + // I am to commit a most heinous crime. + // If I do not call parent, we'll get compile warnings. If I don't do this, there'll be misleading messages that we cannot suppress (we don't use left click because it mimmicks the MODsuit controls) + // Maintainers forgive my sins. + var/mob/real_on_who = on_who + on_who = null + // Sins end. . = ..() if(!.) return - psyker_organ = on_who.get_organ_slot(ORGAN_SLOT_PSYKER) + psyker_organ = real_on_who.get_organ_slot(ORGAN_SLOT_PSYKER) + to_chat(real_on_who, span_notice("You focus your telekinetic powers...
Middle-click: Grab/Punt | Right-click: Drop | Move mouse: to drag")) return TRUE /datum/action/cooldown/spell/pointed/telekinesis/on_deactivation(mob/on_who, refund_cooldown = TRUE) @@ -52,49 +70,142 @@ return ..() /datum/action/cooldown/spell/pointed/telekinesis/InterceptClickOn(mob/living/clicker, params, atom/target) - // We only care about the owner using it if(clicker != owner) - return TRUE + return FALSE - var/list/modifiers = params2list(params) + var/list/mods = params2list(params) + + // Right click: drop if holding. Doesn't need target or range checks. + if(LAZYACCESS(mods, RIGHT_CLICK)) + if(grabbed_atom) + clear_grab() + return TRUE + return FALSE - // Only middle click does anything for now - if(!LAZYACCESS(modifiers, MIDDLE_CLICK)) + if(INCAPACITATED_IGNORING(clicker, INCAPABLE_GRAB)) + owner.balloon_alert(clicker, span_warning("Cannot grab target!")) + return FALSE + + // Middle click: grab if empty, punt if holding + if(LAZYACCESS(mods, MIDDLE_CLICK)) + if(!grabbed_atom) + if(!target) + owner.balloon_alert(clicker, span_warning("No target!")) + return TRUE + + if(!range_check(clicker, target)) + owner.balloon_alert(clicker, span_warning("Too far!")) + return TRUE + + if(!can_grab(clicker, target)) + owner.balloon_alert(clicker, span_warning("Cannot grab target!")) + return TRUE + + grab_atom(target) + return TRUE + + // Holding something: punt + punt_held(clicker, target, params) return TRUE - // Middle click: drop if already holding something - if(grabbed_atom) +// You shouldn't get as stressed from picking up a pen as a closet. +/datum/action/cooldown/spell/pointed/telekinesis/proc/get_stress_cost_for_atom(atom/target) + var/cost = 10 + + if(isitem(target)) + var/obj/item/I = target + switch(I.w_class) + if(WEIGHT_CLASS_TINY) + cost = 1 + if(WEIGHT_CLASS_SMALL) + cost = 2 + if(WEIGHT_CLASS_NORMAL) + cost = 4 + if(WEIGHT_CLASS_BULKY) + cost = 8 + else + cost = 15 // structures, superheavy things, basically anything that goes beyond w_class. + + return cost + +/datum/action/cooldown/spell/pointed/telekinesis/process(seconds_per_tick) + var/mob/living/user = owner + if(!grabbed_atom || !user?.client) + STOP_PROCESSING(SSfastprocess, src) + return + + if(INCAPACITATED_IGNORING(user, INCAPABLE_GRAB)) clear_grab() - return TRUE + return - // Otherwise: attempt grab - if(!target) - return TRUE + if(!range_check(user, grabbed_atom)) + to_chat(user, span_warning("Out of range!")) + clear_grab() + return - if(!range_check(clicker, target)) - to_chat(clicker, span_warning("Too far!")) - return TRUE + if(kinesis_catcher?.mouse_params) + kinesis_catcher.calculate_params() + if(!kinesis_catcher?.given_turf) + return - if(!can_grab(clicker, target)) - to_chat(clicker, span_warning("Can't grab that!")) - return TRUE + var/turf/target_turf = kinesis_catcher.given_turf + if(!target_turf) + return - add_stress(stress_cost_grab) - grab_atom(clicker, target) - return TRUE + // Dragging along hte floor + + if(grabbed_atom.loc != target_turf) + var/turf/next_turf = get_step_towards(grabbed_atom, target_turf) -// TODO: Move this to the more universal one in _psyker_power.dm -/datum/action/cooldown/spell/pointed/telekinesis/proc/add_stress(amount) - if(!amount) + if(grabbed_atom.Move(next_turf, get_dir(grabbed_atom, next_turf), 8)) + // If the item is in our space, do we scoop it up? + if(isitem(grabbed_atom) && (user in next_turf)) + var/obj/item/grabbed_item = grabbed_atom + clear_grab(playsound = FALSE) + grabbed_item.pickup(user) + user.put_in_hands(grabbed_item) + return + + + psyker_power.add_stress(1 * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever. + +// The fun part, punting shit. +/datum/action/cooldown/spell/pointed/telekinesis/proc/punt_held(mob/living/user, atom/target, params) + if(!grabbed_atom) return - var/mob/living/user = owner - if(!psyker_organ && user) - psyker_organ = user.get_organ_slot(ORGAN_SLOT_PSYKER) - if(psyker_organ) - psyker_organ.stress += amount + // Where are we throwing it? + var/turf/throw_turf = target ? get_turf(target) : null + + // If target didn't resolve (common on middle click), derive turf from click params via catcher + if(!throw_turf && kinesis_catcher) + kinesis_catcher.mouse_params = params + kinesis_catcher.calculate_params() + throw_turf = kinesis_catcher.given_turf + + if(!throw_turf) + owner.balloon_alert(user, span_warning("No target!")) + return + + var/atom/movable/launched = grabbed_atom + + // Basically the same stress cost for picking it up. + psyker_power.add_stress(get_stress_cost_for_atom(launched)) + + clear_grab(playsound = FALSE) + playsound(launched, 'sound/effects/magic/repulse.ogg', 75, TRUE) + + launched.throw_at( + throw_turf, + range = grab_range, + speed = (launched.density ? 3 : 4), + thrower = user, + spin = isitem(launched) + ) + +// The proverbial leash. /datum/action/cooldown/spell/pointed/telekinesis/proc/range_check(mob/living/user, atom/target) - if(!isturf(user.loc)) + if(!user || !isturf(user.loc)) return FALSE if(ismovable(target) && !isturf(target.loc)) return FALSE @@ -102,6 +213,7 @@ return FALSE return TRUE +// Can we ACTUALLY grab it or will it just fizz out? /datum/action/cooldown/spell/pointed/telekinesis/proc/can_grab(mob/living/user, atom/target) if(user == target) return FALSE @@ -135,9 +247,13 @@ return TRUE -/datum/action/cooldown/spell/pointed/telekinesis/proc/grab_atom(mob/living/user, atom/movable/target) +/datum/action/cooldown/spell/pointed/telekinesis/proc/grab_atom(atom/movable/target) + // If anything was already held, clear it first + if(grabbed_atom) + clear_grab(playsound = FALSE) grabbed_atom = target + // Mob handling like module_kinesis if(isliving(grabbed_atom)) grabbed_atom.add_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), REF(src)) RegisterSignal(grabbed_atom, COMSIG_MOB_STATCHANGE, PROC_REF(on_statchange)) @@ -145,75 +261,72 @@ ADD_TRAIT(grabbed_atom, TRAIT_NO_FLOATING_ANIM, REF(src)) RegisterSignal(grabbed_atom, COMSIG_MOVABLE_SET_ANCHORED, PROC_REF(on_setanchored)) - playsound(grabbed_atom, 'sound/items/weapons/contractor_baton/contractorbatonhit.ogg', 75, TRUE) + playsound(grabbed_atom, 'sound/effects/magic/magic_missile.ogg', 75, TRUE) - kinesis_icon = mutable_appearance(icon = 'icons/effects/effects.dmi', icon_state = "cursehand0", layer = grabbed_atom.layer - 0.1, appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART) - kinesis_icon.overlays += emissive_appearance(icon = 'icons/effects/effects.dmi', icon_state = "cursehand0", offset_spokesman = grabbed_atom) + kinesis_icon = mutable_appearance( + icon = 'icons/effects/effects.dmi', + icon_state = "psychic", + layer = grabbed_atom.layer - 0.1, + appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART + ) + kinesis_icon.color = "#8A2BE2" //mutable appearance doesn't support color? + player_icon = mutable_appearance( + icon = 'icons/effects/effects.dmi', + icon_state = "purplesparkles", + layer = owner.layer - 0.1, + appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART + ) grabbed_atom.add_overlay(kinesis_icon) + owner.add_overlay(player_icon) - kinesis_beam = user.Beam(grabbed_atom, "curse0") + // Even though the modsuit catcher is global, we want our own so we can tweak the visuals. if(!kinesis_catcher) - kinesis_catcher = user.overlay_fullscreen( - "curse0", - /atom/movable/screen/fullscreen/cursor_catcher/kinesis, - 0 - ) - kinesis_catcher.assign_to_mob(user) - - START_PROCESSING(SSfastprocess, src) - -/datum/action/cooldown/spell/pointed/telekinesis/process(seconds_per_tick) - var/mob/living/user = owner - if(!grabbed_atom || !user?.client) - STOP_PROCESSING(SSfastprocess, src) - return - - if(!kinesis_catcher?.mouse_params) - return - - kinesis_catcher.calculate_params() - if(!kinesis_catcher.given_turf) - return + kinesis_catcher = owner.overlay_fullscreen("psyker_tk", /atom/movable/screen/fullscreen/cursor_catcher/kinesis/psyker_tk, 0) + kinesis_catcher.assign_to_mob(owner) - var/turf/target_turf = kinesis_catcher.given_turf - if(grabbed_atom.loc == target_turf) - return + // Amounts are in the get_stress_cost_for_atom + psyker_power.add_stress(get_stress_cost_for_atom(target)) - grabbed_atom.Move( - get_step_towards(grabbed_atom, target_turf), - get_dir(grabbed_atom, target_turf), - 8 - ) + START_PROCESSING(SSfastprocess, src) /datum/action/cooldown/spell/pointed/telekinesis/proc/clear_grab(playsound = TRUE) if(!grabbed_atom) + // Still ensure the fullscreen overlay is gone if we somehow desynced + if(owner) + owner.clear_fullscreen("psyker_tk") + kinesis_catcher = null + kinesis_icon = null + STOP_PROCESSING(SSfastprocess, src) return - if(playsound) - playsound(grabbed_atom, 'sound/effects/empulse.ogg', 75, TRUE) - - UnregisterSignal(grabbed_atom, list(COMSIG_MOB_STATCHANGE, COMSIG_MOVABLE_SET_ANCHORED)) - - grabbed_atom.cut_overlay(kinesis_icon) - QDEL_NULL(kinesis_beam) + // Hold a stable ref so we can safely null grabbed_atom early + var/atom/movable/held = grabbed_atom + grabbed_atom = null - if(isliving(grabbed_atom)) - grabbed_atom.remove_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), REF(src)) + if(playsound) + playsound(held, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE) - REMOVE_TRAIT(grabbed_atom, TRAIT_NO_FLOATING_ANIM, REF(src)) + STOP_PROCESSING(SSfastprocess, src) - if(!isitem(grabbed_atom)) - animate(grabbed_atom, 0.2 SECONDS, pixel_x = grabbed_atom.base_pixel_x, pixel_y = grabbed_atom.base_pixel_y) + UnregisterSignal(held, list(COMSIG_MOB_STATCHANGE, COMSIG_MOVABLE_SET_ANCHORED)) - grabbed_atom = null + // Remove overlay BEFORE deleting vars + if(kinesis_icon) + held.cut_overlay(kinesis_icon) + kinesis_icon = null + if(player_icon) + owner.cut_overlay(player_icon) + player_icon = null - STOP_PROCESSING(SSfastprocess, src) + if(isliving(held)) + held.remove_traits(list(TRAIT_IMMOBILIZED, TRAIT_HANDS_BLOCKED), REF(src)) - if(kinesis_catcher) - var/mob/living/user = owner - user?.clear_fullscreen("kinesis") - kinesis_catcher = null + REMOVE_TRAIT(held, TRAIT_NO_FLOATING_ANIM, REF(src)) + // Clear our telekinesis-specific screen overlay + if(owner) + owner.clear_fullscreen("psyker_tk") + kinesis_catcher = null /datum/action/cooldown/spell/pointed/telekinesis/proc/on_statchange(mob/grabbed_mob, new_stat) SIGNAL_HANDLER @@ -224,3 +337,14 @@ SIGNAL_HANDLER if(grabbed_atom_ref.anchored) clear_grab() + + +/* ------------------------------------------------------------ +// Telekinesis-only screen edge +// We do this so we can tweak the actual looks of the overlay. + ------------------------------------------------------------ */ +/atom/movable/screen/fullscreen/cursor_catcher/kinesis/psyker_tk + icon_state = "kinesis" + alpha = 180 + color = "#8A2BE2" + mouse_opacity = MOUSE_OPACITY_OPAQUE From 8d7e0fb1830a6cb07707eb3035592f9ef40a9c4d Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 24 Jan 2026 13:42:17 +0100 Subject: [PATCH 013/212] Meditate added. Got started on the stress backlash system so this may error, woopsies. --- .../code/powers/resonant/meditate.dm | 69 +++++++++++++++++++ .../powers/resonant/psyker/_psyker_power.dm | 7 -- .../powers/resonant/psyker/_psyker_root.dm | 3 + .../psyker/psyker_events/_psyker_event | 35 ++++++++++ .../powers/resonant/psyker/psyker_organ.dm | 30 ++++++++ .../powers/resonant/psyker/telekinesis.dm | 15 +--- tgstation.dme | 1 + 7 files changed, 141 insertions(+), 19 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/meditate.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm new file mode 100644 index 00000000000000..47a14131464003 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -0,0 +1,69 @@ +/datum/action/cooldown/spell/resonant_meditate + name = "Resonant Meditation" + desc = "Restores the full potential of your resonant powers." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "chuuni" + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + antimagic_flags = MAGIC_RESISTANCE_MIND + cooldown_time = 6 SECONDS // Just don't let people spam it on or off. + + + // Both Cultivator and Psyker can benefit from meditate. + + // The components responsible for meditation. + var/obj/item/organ/resonant/psyker/psyker_organ + var/cultivator_organ //TODO: Cultivator Organ + +/datum/action/cooldown/spell/resonant_meditate/cast(mob/on_who) + . = ..() + var/keep_going = TRUE + var/mob/living/spotlighttarget = owner // cause we need to call it on a mob/living + + to_chat(owner, "You start meditating.") + update_organs() + // Adds visual effects + var/datum/status_effect/spotlight_light/light = get_spotlight_color() + spotlighttarget.apply_status_effect(light, 300) + do + if(do_after(owner, 25, target = owner)) + if(!psyker_organ) + to_chat(owner, "I have nothing to meditate on!") + keep_going = FALSE + if(psyker_organ) + psyker_organ.remove_stress(8) + if(psyker_organ.stress <= 0) + to_chat(owner, "I no longer feel any stress") + keep_going = FALSE + else + keep_going = FALSE + while (keep_going) + + to_chat(owner, "You stop meditating.") + spotlighttarget.remove_status_effect(light) + return + +/datum/action/cooldown/spell/resonant_meditate/proc/get_spotlight_color() + + if(psyker_organ && cultivator_organ) + return /datum/status_effect/spotlight_light/resonant + else if(psyker_organ) + return /datum/status_effect/spotlight_light/psyker + else if(cultivator_organ) + return /datum/status_effect/spotlight_light/cultivator + else + return /datum/status_effect/spotlight_light + +/datum/action/cooldown/spell/resonant_meditate/proc/update_organs() + psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) + //TODO: Cultivator Organ + +// I wish I could just change the color on spotlights but no we have to make it special. +/datum/status_effect/spotlight_light/psyker + id = "psyker_spotlight" + spotlight_color = "#ba2cc9" +/datum/status_effect/spotlight_light/cultivator + id = "cultivator_spotlight" + spotlight_color = "#f5c612" +/datum/status_effect/spotlight_light/resonant // if you somehow have both + id = "resonant_spotlight" + spotlight_color = "#cf2525" 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 e17b82811f6109..8807697222e014 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,10 +8,3 @@ archetype = POWER_ARCHETYPE_RESONANT path = POWER_PATH_PSYKER required_powers = list(/datum/power/psyker_root) - - -/datum/power/psyker_power/proc/add_stress(amount) - var/obj/item/organ/resonant/psyker/psyker_organ = power_holder.get_organ_slot(ORGAN_SLOT_PSYKER) - if(psyker_organ) - psyker_organ.stress += amount - 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 67949f34fdfded..b01978dd8cec20 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 @@ -11,8 +11,11 @@ path = POWER_PATH_PSYKER priority = POWER_PRIORITY_ROOT + action_path = /datum/action/cooldown/spell/resonant_meditate // todo; deal with duplicates + var/obj/item/organ/resonant/psyker/psyker_organ /datum/power/psyker_root/add(client/client_source) psyker_organ = new /obj/item/organ/resonant/psyker psyker_organ.Insert(power_holder, special = TRUE) + diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event new file mode 100644 index 00000000000000..9432819b15992b --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event @@ -0,0 +1,35 @@ +/datum/psyker_event + var/abstract = TRUE + var/weight = 1 + + proc/can_execute(mob/living/carbon/human/H, obj/item/organ/resonant/psyker/gland) + return TRUE + + proc/execute(mob/living/carbon/human/H, obj/item/organ/resonant/psyker/gland) + return + +/datum/psyker_event/mild + abstract = TRUE + +/datum/psyker_event/severe + abstract = TRUE + +/datum/psyker_event/catastrophic + abstract = TRUE + + +// Example mild event +/datum/psyker_event/mild/eye_twitch + abstract = FALSE + weight = 3 + + execute(mob/living/carbon/human/H, obj/item/organ/resonant/psyker/gland) + H.balloon_alert(H, span_warning("Your eye won't stop twitching.")) + +// Example severe event +/datum/psyker_event/severe/nosebleed + abstract = FALSE + weight = 1 + + execute(mob/living/carbon/human/H, obj/item/organ/resonant/psyker/gland) + H.balloon_alert(H, span_danger("Blood drips from your nose.")) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm index 129cd586346671..27f906fb2face2 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm @@ -16,6 +16,18 @@ // Base recovery per second var/recovery_per_second = 1.1 + //Cooldowns for the stress events + var/CDstressMild = 0 + var/CDstressSevere = 0 + +// Don't modify stress directly. In the future affinity has a bearing on how much stress you gain. +/obj/item/organ/resonant/psyker/proc/add_stress(amount) + // TODO; Add clothing affinity. Wearing psychic nicknacks makes you gain less stress. + stress += amount + +/obj/item/organ/resonant/psyker/proc/remove_stress(amount) + // TODO: Ditto on above. + stress -= amount /obj/item/organ/resonant/psyker/on_life(seconds_per_tick, times_fired) . = ..() @@ -38,9 +50,27 @@ // Apply recovery, don't let it send stress into the negatives. stress = max(stress - stress_to_recover * seconds_per_tick, 0) + // Check if we do stress backlash after stress reduction. + if(stress >= 200) // Catastrophic event. + stress_backlash(3) + else if(stress >= 150 && && CDstressMild <= 0) // Severe Event + CDStresssevere = 100 // reset CD + stress_backlash(2) + else if (stress >= 100 && CDstressMild <= 0) // Mild Event + CDstressMild = 100 // reset CD + stress_backlash(1) + + if(!CDstressMild >= 0) + CDStressMild-- + if(!CDstressSevere >= 0) + CDStressSevere-- + // In the event that you implant this into someone else. // Currently placeholder til we settle on what it do on people that don't have it. else damage += 1 owner.apply_damage(damage * 0.1, TOX) +// The psyker is exploding and probably about to summon extradimensional demons. +/obj/item/organ/resonant/psyker/proc/stress_backlash(degree) + if(degree = 3) 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 d6b5691503ee89..e51192d2e880e6 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -41,15 +41,6 @@ // Mouse tracker overlay (telekinesis-specific) var/atom/movable/screen/fullscreen/cursor_catcher/kinesis/psyker_tk/kinesis_catcher - // Reference for the base psyker power, so we can call add_stress. - var/datum/power/psyker_power/psyker_power - -/datum/action/cooldown/spell/pointed/telekinesis/New(Target) - . = ..() - // We do this so we can call add_stress from the spell itself. - if(istype(Target, /datum/power/psyker_power)) - psyker_power = Target - /datum/action/cooldown/spell/pointed/telekinesis/on_activation(mob/on_who) // I am to commit a most heinous crime. // If I do not call parent, we'll get compile warnings. If I don't do this, there'll be misleading messages that we cannot suppress (we don't use left click because it mimmicks the MODsuit controls) @@ -167,7 +158,7 @@ return - psyker_power.add_stress(1 * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever. + psyker_organ.add_stress(1 * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever. // The fun part, punting shit. /datum/action/cooldown/spell/pointed/telekinesis/proc/punt_held(mob/living/user, atom/target, params) @@ -190,7 +181,7 @@ var/atom/movable/launched = grabbed_atom // Basically the same stress cost for picking it up. - psyker_power.add_stress(get_stress_cost_for_atom(launched)) + psyker_organ.add_stress(get_stress_cost_for_atom(launched)) clear_grab(playsound = FALSE) playsound(launched, 'sound/effects/magic/repulse.ogg', 75, TRUE) @@ -285,7 +276,7 @@ kinesis_catcher.assign_to_mob(owner) // Amounts are in the get_stress_cost_for_atom - psyker_power.add_stress(get_stress_cost_for_atom(target)) + psyker_organ.add_stress(get_stress_cost_for_atom(target)) START_PROCESSING(SSfastprocess, src) diff --git a/tgstation.dme b/tgstation.dme index 7c22d90399f34b..677efb719bf1ff 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7434,6 +7434,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\lodestone_legends.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_organ.dm" From 7399bc2740de57349bd50d1fcf71acf7a0981125 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 24 Jan 2026 17:47:22 +0100 Subject: [PATCH 014/212] Psyker Event system, triggering bad events if you overdo the psychic stuff. --- .../{psyker_organ.dm => _psyker_organ.dm} | 91 ++++++++++++++++--- .../psyker/psyker_events/_psyker_event | 35 ------- .../psyker/psyker_events/_psyker_event.dm | 30 ++++++ .../catastrophic/cardiac_arrest.dm | 11 +++ .../catastrophic/telekinetic_backlash.dm | 89 ++++++++++++++++++ .../psyker/psyker_events/mild/headache.dm | 11 +++ .../psyker/psyker_events/mild/nosebleed.dm | 11 +++ .../psyker/psyker_events/severe/vomit.dm | 7 ++ .../psyker_events/special/magic_trauma.dm | 16 ++++ tgstation.dme | 9 +- 10 files changed, 262 insertions(+), 48 deletions(-) rename modular_doppler/modular_powers/code/powers/resonant/psyker/{psyker_organ.dm => _psyker_organ.dm} (53%) delete mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm similarity index 53% rename from modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm rename to modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index 27f906fb2face2..ac150daeac4d38 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -3,6 +3,7 @@ desc = "An intrusive organ that should not even be able to function in most bodies. Commonly found in the bodies of Psykers. Though many would try to implement these into themselves to try and awaken psychic powers, its presence in those without such powers is often life-threatening." icon = 'icons/obj/medical/organs/organs.dmi' icon_state = "demon_heart-on" + healing_factor = STANDARD_ORGAN_HEALING decay_factor = 5 * STANDARD_ORGAN_DECAY //about 12mins to fully decay. slot = ORGAN_SLOT_PSYKER zone = BODY_ZONE_CHEST @@ -24,15 +25,21 @@ /obj/item/organ/resonant/psyker/proc/add_stress(amount) // TODO; Add clothing affinity. Wearing psychic nicknacks makes you gain less stress. stress += amount + if(stress <= 0) + stress = 0 + return /obj/item/organ/resonant/psyker/proc/remove_stress(amount) // TODO: Ditto on above. stress -= amount + if(stress <= 0) + stress = 0 + return /obj/item/organ/resonant/psyker/on_life(seconds_per_tick, times_fired) . = ..() - // If you have the associated power read; you are a psyker. + // If you have the associated power. read; you are a psyker. if(owner.has_power(/datum/power/psyker_root)) if(stress <= 0) stress = 0 @@ -51,26 +58,86 @@ stress = max(stress - stress_to_recover * seconds_per_tick, 0) // Check if we do stress backlash after stress reduction. - if(stress >= 200) // Catastrophic event. + if(stress >= (stress_threshold * 2)) // Catastrophic event. stress_backlash(3) - else if(stress >= 150 && && CDstressMild <= 0) // Severe Event - CDStresssevere = 100 // reset CD + stress = 0 // No CD, just a hard reset and the consequences of your actions. + CDstressMild = 0 + CDstressSevere = 0 + else if(stress >= (stress_threshold * 1.5) && CDstressSevere <= 0) // Severe Event + CDstressSevere = 60 // reset CD stress_backlash(2) - else if (stress >= 100 && CDstressMild <= 0) // Mild Event - CDstressMild = 100 // reset CD + else if (stress >= stress_threshold && CDstressMild <= 0) // Mild Event + CDstressMild = 60 // reset CD stress_backlash(1) - if(!CDstressMild >= 0) - CDStressMild-- - if(!CDstressSevere >= 0) - CDStressSevere-- + if(CDstressMild > 0) + CDstressMild = max(CDstressMild - seconds_per_tick, 0) + if(CDstressSevere > 0) + CDstressSevere = max(CDstressSevere - seconds_per_tick, 0) // In the event that you implant this into someone else. // Currently placeholder til we settle on what it do on people that don't have it. + // TODO: Appear on med scanners. else damage += 1 owner.apply_damage(damage * 0.1, TOX) -// The psyker is exploding and probably about to summon extradimensional demons. +// "The psyker is exploding and probably about to summon extradimensional demons." +// When psyker stress gets too high, it triggers bad events, this chooses said bad events. /obj/item/organ/resonant/psyker/proc/stress_backlash(degree) - if(degree = 3) + var/mob/living/carbon/human/human = owner + if(!istype(human)) + return FALSE + + var/base_type + switch(degree) + if(1) + base_type = /datum/psyker_event/mild + if(2) + base_type = /datum/psyker_event/severe + if(3) + if(prob(20)) // If you get 'lucky' you get FUN + base_type = /datum/psyker_event/special + else + base_type = /datum/psyker_event/catastrophic + else + return FALSE + + pick_psyker_event(base_type, human) + return + +// Picks the backlash event +/obj/item/organ/resonant/psyker/proc/pick_psyker_event(base_type, mob/living/carbon/human/human) + var/list/candidate_types = list() + + for(var/subtype in subtypesof(base_type)) + var/datum/psyker_event/event_type = subtype + + // Skip the abstract root itself + if(initial(event_type.abstract_type) == subtype) + continue + + candidate_types += subtype + + + while(length(candidate_types)) + var/subtype = pick(candidate_types) + candidate_types -= subtype + + var/datum/psyker_event/event = new subtype + + if(!event.can_execute(human, src)) + qdel(event) + continue + + // We check if it actually succesfully executed. Qdel it under normal circumstances; if it lingers we don't. + if(event.execute(human)) + if(!event.lingering) + qdel(event) + return + + // Execution failed? We retry + qdel(event) + + return + diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event deleted file mode 100644 index 9432819b15992b..00000000000000 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event +++ /dev/null @@ -1,35 +0,0 @@ -/datum/psyker_event - var/abstract = TRUE - var/weight = 1 - - proc/can_execute(mob/living/carbon/human/H, obj/item/organ/resonant/psyker/gland) - return TRUE - - proc/execute(mob/living/carbon/human/H, obj/item/organ/resonant/psyker/gland) - return - -/datum/psyker_event/mild - abstract = TRUE - -/datum/psyker_event/severe - abstract = TRUE - -/datum/psyker_event/catastrophic - abstract = TRUE - - -// Example mild event -/datum/psyker_event/mild/eye_twitch - abstract = FALSE - weight = 3 - - execute(mob/living/carbon/human/H, obj/item/organ/resonant/psyker/gland) - H.balloon_alert(H, span_warning("Your eye won't stop twitching.")) - -// Example severe event -/datum/psyker_event/severe/nosebleed - abstract = FALSE - weight = 1 - - execute(mob/living/carbon/human/H, obj/item/organ/resonant/psyker/gland) - H.balloon_alert(H, span_danger("Blood drips from your nose.")) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm new file mode 100644 index 00000000000000..f3196fa0883dfb --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm @@ -0,0 +1,30 @@ +// Psyker Events happen when your stress reaches the threshold. Specifically, 1x the stress_threshold, 1.5x for severe and 2x for catastrohpic. +// There is a 20% of substituting a catastrophic event for a special event. These aren't necessarily always better, just a lot weirder. +// Any psyker_event you define is added to the lists unless it is abstract. + +/datum/psyker_event + // Remember to set abstracts to this. + var/abstract_type = /datum/psyker_event + var/weight = 1 + // For events that continue for a while, this skips the qdel step, under the condition you qdel it later. + var/lingering = FALSE + +// Are there any special prerequisites? +/datum/psyker_event/proc/can_execute(mob/living/carbon/human/psyker) + return TRUE + +// Return TRUE if the event actually happens, FALSE if it should be skipped +/datum/psyker_event/proc/execute(mob/living/carbon/human/psyker) + return FALSE + +/datum/psyker_event/mild + abstract_type = /datum/psyker_event/mild + +/datum/psyker_event/severe + abstract_type = /datum/psyker_event/severe + +/datum/psyker_event/catastrophic + abstract_type = /datum/psyker_event/catastrophic + +/datum/psyker_event/special + abstract_type = /datum/psyker_event/special diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm new file mode 100644 index 00000000000000..232ad0cfe56581 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm @@ -0,0 +1,11 @@ +/datum/psyker_event/catastrophic/heart_attack + +/datum/psyker_event/catastrophic/heart_attack/execute(mob/living/carbon/human/psyker) + if(!psyker.can_heartattack() && !psyker.undergoing_cardiac_arrest()) // Can the target have a heartattack? And if so, are they already undergoing a heartattack? + return FALSE + psyker.apply_status_effect(/datum/status_effect/heart_attack) + //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them. + to_chat(psyker, span_purple("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity...")) + + return TRUE + diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm new file mode 100644 index 00000000000000..41e72fd74de659 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm @@ -0,0 +1,89 @@ +// Resonant forces batter and wound your body. This one will always return TRUE, and is probably the deadliest. +/datum/psyker_event/catastrophic/telekinetic_backlash + lingering = TRUE + // I guess we'll have a pity system. + var/max_ticks = 6 + + // Brute damage per severity + var/moderate_brute = 5 + var/severe_brute = 10 + var/critical_brute = 20 + +/datum/psyker_event/catastrophic/telekinetic_backlash/execute(mob/living/carbon/human/psyker) + to_chat(psyker, span_purple("As you strain your psychic powers past the breaking point, you feel yourself wracked by pain, as your skin, bones and flesh are pulled in all manner of directions!")) + + // Start the chain after ~1 second + addtimer(CALLBACK(src, PROC_REF(_backlash_tick), psyker, 0), 1 SECONDS) + + return TRUE + +/datum/psyker_event/catastrophic/telekinetic_backlash/proc/_backlash_tick(mob/living/carbon/human/psyker, tick_count) + if(!psyker || QDELETED(psyker)) + qdel(src) + return + + if(tick_count >= max_ticks) + qdel(src) + return + + var/obj/item/bodypart/target_limb = pick_wound_bodypart(psyker) + if(!target_limb) + qdel(src) + return + + //What wound type we apply for this instance. + var/wound_type = pick(WOUND_SLASH, WOUND_PIERCE, WOUND_BLUNT) + + // Roll which effect happens this tick (65/20/10/5) + var/roll = rand(1, 100) + + if(roll <= 65) + to_chat(psyker, span_warning("Your body lurches as invisible forces wrench at your flesh!")) + psyker.apply_damage(moderate_brute, BRUTE, def_zone = target_limb.body_zone) + psyker.cause_wound_of_type_and_severity(wound_type, target_limb, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_MODERATE) + else if(roll <= 85) + to_chat(psyker, span_danger("You feel something tear inside you as the force twists harder!")) + psyker.apply_damage(severe_brute, BRUTE, def_zone = target_limb.body_zone) + psyker.cause_wound_of_type_and_severity(wound_type, target_limb, WOUND_SEVERITY_SEVERE, WOUND_SEVERITY_CRITICAL) + else if(roll <= 95) + to_chat(psyker, span_userdanger("Agony spikes through you as feel your body being ripped apart!")) + psyker.apply_damage(critical_brute, BRUTE, def_zone = target_limb.body_zone) + psyker.cause_wound_of_type_and_severity(wound_type, target_limb, WOUND_SEVERITY_CRITICAL, WOUND_SEVERITY_CRITICAL) + psyker.emote("scream") + else + // MY LEG! + var/obj/item/bodypart/part = pick_wound_bodypart(psyker, FALSE) + if(part) + part.dismember() + to_chat(psyker, span_userdanger("Something gives way—your body can't hold together!")) + psyker.emote("scream") + + // 75% chance to continue applying effects + if(!prob(75)) + qdel(src) + return + + // Schedule next tick in ~1 second + addtimer(CALLBACK(src, PROC_REF(_backlash_tick), psyker, tick_count + 1), 1 SECONDS) + +/datum/psyker_event/catastrophic/telekinetic_backlash/proc/pick_wound_bodypart(mob/living/carbon/human/psyker, allow_vital = TRUE) + if(!psyker || !length(psyker.bodyparts)) + return null + + var/list/candidates = list() + for(var/obj/item/bodypart/bodypart as anything in psyker.bodyparts) + // Skip missing/destroyed parts if your fork tracks those (optional safety) + if(QDELETED(bodypart)) + continue + + // Avoid vital zones unless explicitly allowed + if(!allow_vital) + if(bodypart.body_zone == BODY_ZONE_HEAD || bodypart.body_zone == BODY_ZONE_CHEST) + continue + + candidates += bodypart + + if(!length(candidates)) + return null + + return pick(candidates) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm new file mode 100644 index 00000000000000..42373ee21a3496 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm @@ -0,0 +1,11 @@ +/datum/psyker_event/mild/headache + +/datum/psyker_event/mild/headache/execute(mob/living/carbon/human/psyker) + psyker.add_mood_event("headache", /datum/mood_event/psyker_headache) + to_chat(psyker, span_purple("Overusing your powers has given you a splitting headache!")) + return TRUE + +/datum/mood_event/psyker_headache + description = "Overusing my powers has given me a splitting headache!" + mood_change = -15 + timeout = 1 MINUTES // I wish my headaches went away that fast. diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm new file mode 100644 index 00000000000000..394bfa24c7b157 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm @@ -0,0 +1,11 @@ +/datum/psyker_event/mild/nosebleed + +/datum/psyker_event/mild/nosebleed/execute(mob/living/carbon/human/psyker) + var/obj/item/bodypart/head = psyker.get_bodypart(BODY_ZONE_HEAD) + if(isnull(head)) + return FALSE + if(!psyker.can_bleed()) + return FALSE + head.adjustBleedStacks(5) + psyker.visible_message(span_notice("[psyker] gets a nosebleed."), span_purple("Overusing your powers has given you a nosebleed!")) + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm new file mode 100644 index 00000000000000..abd9f3e8af3c41 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm @@ -0,0 +1,7 @@ +/datum/psyker_event/severe/vomit + +/datum/psyker_event/severe/vomit/execute(mob/living/carbon/human/psyker) + to_chat(psyker, span_purple("A wave of psychic energy overwhelms you, making you vomit!")) + psyker.vomit(VOMIT_CATEGORY_DEFAULT, lost_nutrition = 10) + // Even though they may dryheave, the feedback is there from vomit(), so mission accomplished. + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm new file mode 100644 index 00000000000000..c722d27584b2fe --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm @@ -0,0 +1,16 @@ +/datum/psyker_event/special/magic_trauma + +/datum/psyker_event/special/magic_trauma/execute(mob/living/carbon/human/psyker) + var/datum/brain_trauma/magic/trauma + if(prob(65)) // Poltergeists are a bit more thematic so they're a tad more common. + trauma = new /datum/brain_trauma/magic/poltergeist + else // Gets you the stalker, which is even spookier (and bothersome) + trauma = new /datum/brain_trauma/magic/stalker + // We are also not going to tell them they got a trauma. + trauma.gain_text = null + if(!psyker.gain_trauma(trauma)) + QDEL_NULL(trauma) + return FALSE + //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them. + to_chat(psyker, span_purple("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity...")) + return TRUE diff --git a/tgstation.dme b/tgstation.dme index 677efb719bf1ff..c0001ba951b42f 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7437,8 +7437,15 @@ #include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_organ.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_organ.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\_psyker_event.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\nosebleed.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\headache.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\vomit.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\telekinetic_backlash.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\cardiac_arrest.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\special\magic_trauma.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" #include "modular_doppler\modular_quirks\bouncy\bouncy.dm" From d08afd9ead5eaadd4ec0426e0a3bb37d64f9da79 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 24 Jan 2026 17:51:10 +0100 Subject: [PATCH 015/212] span types for most psychic events. --- .../psyker/psyker_events/catastrophic/cardiac_arrest.dm | 2 +- .../psyker/psyker_events/catastrophic/telekinetic_backlash.dm | 2 +- .../code/powers/resonant/psyker/psyker_events/mild/headache.dm | 2 +- .../code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm | 2 +- .../code/powers/resonant/psyker/psyker_events/severe/vomit.dm | 2 +- .../resonant/psyker/psyker_events/special/magic_trauma.dm | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm index 232ad0cfe56581..599cdbaf1ecd55 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm @@ -5,7 +5,7 @@ return FALSE psyker.apply_status_effect(/datum/status_effect/heart_attack) //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them. - to_chat(psyker, span_purple("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity...")) + to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity...")) return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm index 41e72fd74de659..3d621d02e767e9 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm @@ -10,7 +10,7 @@ var/critical_brute = 20 /datum/psyker_event/catastrophic/telekinetic_backlash/execute(mob/living/carbon/human/psyker) - to_chat(psyker, span_purple("As you strain your psychic powers past the breaking point, you feel yourself wracked by pain, as your skin, bones and flesh are pulled in all manner of directions!")) + to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you feel yourself wracked by pain, as your skin, bones and flesh are pulled in all manner of directions!")) // Start the chain after ~1 second addtimer(CALLBACK(src, PROC_REF(_backlash_tick), psyker, 0), 1 SECONDS) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm index 42373ee21a3496..140cb11f005773 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/headache.dm @@ -2,7 +2,7 @@ /datum/psyker_event/mild/headache/execute(mob/living/carbon/human/psyker) psyker.add_mood_event("headache", /datum/mood_event/psyker_headache) - to_chat(psyker, span_purple("Overusing your powers has given you a splitting headache!")) + to_chat(psyker, span_danger("Overusing your powers has given you a splitting headache!")) return TRUE /datum/mood_event/psyker_headache diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm index 394bfa24c7b157..28cd8e49d027a9 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/nosebleed.dm @@ -7,5 +7,5 @@ if(!psyker.can_bleed()) return FALSE head.adjustBleedStacks(5) - psyker.visible_message(span_notice("[psyker] gets a nosebleed."), span_purple("Overusing your powers has given you a nosebleed!")) + psyker.visible_message(span_notice("[psyker] gets a nosebleed."), span_danger("Overusing your powers has given you a nosebleed!")) return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm index abd9f3e8af3c41..88b2bd06a46d54 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm @@ -1,7 +1,7 @@ /datum/psyker_event/severe/vomit /datum/psyker_event/severe/vomit/execute(mob/living/carbon/human/psyker) - to_chat(psyker, span_purple("A wave of psychic energy overwhelms you, making you vomit!")) + to_chat(psyker, span_danger("A wave of psychic energy overwhelms you, making you vomit!")) psyker.vomit(VOMIT_CATEGORY_DEFAULT, lost_nutrition = 10) // Even though they may dryheave, the feedback is there from vomit(), so mission accomplished. return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm index c722d27584b2fe..04da5046f3a3dc 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm @@ -12,5 +12,5 @@ QDEL_NULL(trauma) return FALSE //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them. - to_chat(psyker, span_purple("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity...")) + to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity...")) return TRUE From 6a22a3de461f17b329a00acdfb132820ee7a3590 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 24 Jan 2026 20:02:57 +0100 Subject: [PATCH 016/212] Defines based numbers for stress. Hallucinations for psyker_events. --- code/__DEFINES/~doppler_defines/powers.dm | 21 +++++++++++++++++++ .../code/powers/resonant/levitate.dm | 0 .../code/powers/resonant/meditate.dm | 2 +- .../powers/resonant/psyker/_psyker_organ.dm | 4 ++-- .../psyker_events/severe/hallucinate.dm | 20 ++++++++++++++++++ .../powers/resonant/psyker/telekinesis.dm | 16 +++++++------- tgstation.dme | 1 + 7 files changed, 53 insertions(+), 11 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/levitate.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 13215f2d6940a0..c451ffb7a62d89 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -89,3 +89,24 @@ /// Trait held by all under the resonant archetype. #define TRAIT_ARCHETYPE_RESONANT "archetype_resonant" + +/** + * RESONANT: PSYKER + * All defines related to the enigmatist powers. + */ + +// Standard stress threshold value for the Psyker's organ. +#define PSYKER_STRESS_STANDARD_THRESHOLD 100 + +// Standard stress recovery per second before modifiers. +#define PSYKER_STRESS_RECOVERY 1.1 + +// How much meditate recovers. +#define PSYKER_STRESS_MEDITATION_POWER 10 + +// Standard stress for Psykers. This all goes off of the base organ being 100. +#define PSYKER_STRESS_TRIVIAL (PSYKER_STRESS_STANDARD_THRESHOLD / 100) +#define PSYKER_STRESS_MINOR (PSYKER_STRESS_STANDARD_THRESHOLD / 10) +#define PSYKER_STRESS_MODERATE (PSYKER_STRESS_STANDARD_THRESHOLD / 5) +#define PSYKER_STRESS_MAJOR (PSYKER_STRESS_STANDARD_THRESHOLD / 2) +#define PSYKER_STRESS_CRUSHING (PSYKER_STRESS_STANDARD_THRESHOLD) diff --git a/modular_doppler/modular_powers/code/powers/resonant/levitate.dm b/modular_doppler/modular_powers/code/powers/resonant/levitate.dm new file mode 100644 index 00000000000000..e69de29bb2d1d6 diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index 47a14131464003..19870bdfa45fc9 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -30,7 +30,7 @@ to_chat(owner, "I have nothing to meditate on!") keep_going = FALSE if(psyker_organ) - psyker_organ.remove_stress(8) + psyker_organ.remove_stress(PSYKER_STRESS_MEDITATION_POWER) if(psyker_organ.stress <= 0) to_chat(owner, "I no longer feel any stress") keep_going = FALSE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index ac150daeac4d38..e9b9f98d0d4eaa 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -13,9 +13,9 @@ var/stress = 0 // Stress threshold is how much the psyker organ can handle before the bad events start befalling the user. // Usually, 1x is the minor events, 1.5x are the major events, and 2x are the catastrophic events. - var/stress_threshold = 100 + var/stress_threshold = PSYKER_STRESS_STANDARD_THRESHOLD // Base recovery per second - var/recovery_per_second = 1.1 + var/recovery_per_second = PSYKER_STRESS_RECOVERY //Cooldowns for the stress events var/CDstressMild = 0 diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm new file mode 100644 index 00000000000000..8cc3b494f8fed4 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm @@ -0,0 +1,20 @@ +/datum/psyker_event/severe/hallucinate + // Mostly instant shock factor stuff. + var/static/list/initial_hallucinations = list( + /datum/hallucination/delusion, + /datum/hallucination/xeno_attack, + /datum/hallucination/oh_yeah, + /datum/hallucination/death, + /datum/hallucination/fire, + /datum/hallucination/ice, + /datum/hallucination/shock + ) + +/datum/psyker_event/severe/hallucinate/execute(mob/living/carbon/human/psyker) + to_chat(psyker, span_danger("You begin to lose your grip on reality!")) + // Generaly speaking we don't want these to last too long. + psyker.adjust_hallucinations(60 SECONDS) + // We do also want immediate hallucinations as feedback, as the psyker_events double as stress warnings. + psyker.cause_hallucination(pick(initial_hallucinations), src) + return TRUE + diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm index e51192d2e880e6..f91c97fe77b3cd 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -101,21 +101,21 @@ // You shouldn't get as stressed from picking up a pen as a closet. /datum/action/cooldown/spell/pointed/telekinesis/proc/get_stress_cost_for_atom(atom/target) - var/cost = 10 + var/cost if(isitem(target)) var/obj/item/I = target switch(I.w_class) if(WEIGHT_CLASS_TINY) - cost = 1 + cost = PSYKER_STRESS_TRIVIAL if(WEIGHT_CLASS_SMALL) - cost = 2 + cost = PSYKER_STRESS_TRIVIAL * 2 if(WEIGHT_CLASS_NORMAL) - cost = 4 + cost = PSYKER_STRESS_TRIVIAL * 4 if(WEIGHT_CLASS_BULKY) - cost = 8 - else - cost = 15 // structures, superheavy things, basically anything that goes beyond w_class. + cost = PSYKER_STRESS_MINOR * 0.8 + else + cost = PSYKER_STRESS_MINOR // structures, superheavy things, basically anything that goes beyond w_class. return cost @@ -158,7 +158,7 @@ return - psyker_organ.add_stress(1 * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever. + psyker_organ.add_stress(PSYKER_STRESS_TRIVIAL * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever. // The fun part, punting shit. /datum/action/cooldown/spell/pointed/telekinesis/proc/punt_held(mob/living/user, atom/target, params) diff --git a/tgstation.dme b/tgstation.dme index c0001ba951b42f..b3ad5666b13243 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7443,6 +7443,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\nosebleed.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\headache.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\vomit.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\hallucinate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\telekinetic_backlash.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\cardiac_arrest.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\special\magic_trauma.dm" From 47a19f3a3488ffe67cb7bd8dd0b5cf0503df7608 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 24 Jan 2026 21:26:29 +0100 Subject: [PATCH 017/212] Levitation Power. Minor tweaks to the other powers. --- .../code/powers/resonant/levitate.dm | 0 .../code/powers/resonant/meditate.dm | 2 +- .../code/powers/resonant/psyker/levitate.dm | 79 +++++++++++++++++++ .../powers/resonant/psyker/telekinesis.dm | 5 +- tgstation.dme | 1 + 5 files changed, 83 insertions(+), 4 deletions(-) delete mode 100644 modular_doppler/modular_powers/code/powers/resonant/levitate.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/levitate.dm b/modular_doppler/modular_powers/code/powers/resonant/levitate.dm deleted file mode 100644 index e69de29bb2d1d6..00000000000000 diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index 19870bdfa45fc9..66822ca0ed5dcc 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -14,7 +14,7 @@ var/obj/item/organ/resonant/psyker/psyker_organ var/cultivator_organ //TODO: Cultivator Organ -/datum/action/cooldown/spell/resonant_meditate/cast(mob/on_who) +/datum/action/cooldown/spell/resonant_meditate/cast() . = ..() var/keep_going = TRUE var/mob/living/spotlighttarget = owner // cause we need to call it on a mob/living diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm new file mode 100644 index 00000000000000..db67a4de85d334 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -0,0 +1,79 @@ +/datum/power/psyker_power/levitate + name = "Levitate" + desc = "Grants the ability to float yourself above surfaces and letting you propel yourself in zero-gravity. Passively drains stress while in use." + + value = 5 + priority = POWER_PRIORITY_BASIC + required_powers = list(/datum/power/psyker_root) + action_path = /datum/action/cooldown/spell/levitate + +/datum/action/cooldown/spell/levitate + name = "Levitate" + desc = "Toggles levitation, causing you to ignore the ground. Also allows for propulsion in zero-gravity. Passively drains stress while in use." + button_icon = 'icons/mob/actions/actions_minor_antag.dmi' + button_icon_state = "beam_up" + + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + antimagic_flags = MAGIC_RESISTANCE_MIND + cooldown_time = 2 SECONDS + + // Are we currently levitating? + var/levitating = FALSE + + // Psyker organ for stress. + var/obj/item/organ/resonant/psyker/psyker_organ + + // Overlay we add to the caster + var/mutable_appearance/caster_effect + +/datum/action/cooldown/spell/levitate/cast() + . = ..() + psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) + if(!levitating) + //ADD_TRAIT(owner, TRAIT_IGNORING_GRAVITY, MAGIC_TRAIT) + owner.AddElement(/datum/element/forced_gravity, 0) + owner.AddElement(/datum/element/simple_flying) + to_chat(owner, span_boldnotice("Your body gently floats in the air!")) + START_PROCESSING(SSfastprocess, src) + levitating = TRUE + //visual fx + caster_effect = mutable_appearance( + icon = 'icons/effects/effects.dmi', + icon_state = "psychic", + layer = owner.layer - 0.1, + appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART + ) + owner.add_overlay(caster_effect) + playsound(owner, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + else + //REMOVE_TRAIT(owner, TRAIT_IGNORING_GRAVITY, MAGIC_TRAIT) + owner.RemoveElement(/datum/element/forced_gravity, 0) + owner.RemoveElement(/datum/element/simple_flying) + to_chat(owner, span_boldnotice("You let yourself be affected by gravity once more.")) + STOP_PROCESSING(SSfastprocess, src) + levitating = FALSE + // visual fx + if(caster_effect) + owner.cut_overlay(caster_effect) + caster_effect = null + playsound(owner, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + + return + +/datum/action/cooldown/spell/levitate/process(seconds_per_tick) + // Calling the parent process() stops processing when the ability is off cooldown, which is not what we want. + if(!owner) + build_all_button_icons(UPDATE_BUTTON_STATUS) + STOP_PROCESSING(SSfastprocess, src) + return + build_all_button_icons(UPDATE_BUTTON_STATUS) + + // Passive stress cost + if(levitating) + var/mob/living/carbon/human/psyker = owner + var/cost = PSYKER_STRESS_TRIVIAL * 2 + if(psyker.get_quirk(/datum/quirk/paraplegic)) // There'll probably be several that'd like to do this. Effecively gives a 2x discount + cost = PSYKER_STRESS_TRIVIAL + psyker_organ.add_stress(cost * seconds_per_tick) + + 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 f91c97fe77b3cd..2f1714e89e5898 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -252,7 +252,7 @@ ADD_TRAIT(grabbed_atom, TRAIT_NO_FLOATING_ANIM, REF(src)) RegisterSignal(grabbed_atom, COMSIG_MOVABLE_SET_ANCHORED, PROC_REF(on_setanchored)) - playsound(grabbed_atom, 'sound/effects/magic/magic_missile.ogg', 75, TRUE) + playsound(grabbed_atom, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) kinesis_icon = mutable_appearance( icon = 'icons/effects/effects.dmi', @@ -260,7 +260,6 @@ layer = grabbed_atom.layer - 0.1, appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART ) - kinesis_icon.color = "#8A2BE2" //mutable appearance doesn't support color? player_icon = mutable_appearance( icon = 'icons/effects/effects.dmi', icon_state = "purplesparkles", @@ -295,7 +294,7 @@ grabbed_atom = null if(playsound) - playsound(held, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE) + playsound(held, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) STOP_PROCESSING(SSfastprocess, src) diff --git a/tgstation.dme b/tgstation.dme index b3ad5666b13243..cd0ec68388e9bf 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7439,6 +7439,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_organ.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\levitate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\_psyker_event.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\nosebleed.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\headache.dm" From e186112a507308f372291977af76e6a457cf9f41 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 24 Jan 2026 21:38:35 +0100 Subject: [PATCH 018/212] fixed the spotlight from medigate going away way too early --- modular_doppler/modular_powers/code/powers/resonant/meditate.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index 66822ca0ed5dcc..9f114a02d9137a 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -23,7 +23,7 @@ update_organs() // Adds visual effects var/datum/status_effect/spotlight_light/light = get_spotlight_color() - spotlighttarget.apply_status_effect(light, 300) + spotlighttarget.apply_status_effect(light, 3000) do if(do_after(owner, 25, target = owner)) if(!psyker_organ) From 6901cf180e37bd843b32fb56ead37244c5a676f2 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 25 Jan 2026 21:14:35 +0100 Subject: [PATCH 019/212] Addded 4 psyker_events, modified comments and span classes on other various fiels. --- .../code/powers/resonant/psyker/levitate.dm | 6 ++-- .../psyker/psyker_events/_psyker_event.dm | 5 ++- .../catastrophic/cardiac_arrest.dm | 2 +- .../psyker/psyker_events/mild/dizziness.dm | 7 ++++ .../psyker/psyker_events/mild/twitching.dm | 7 ++++ .../psyker/psyker_events/severe/exhaustion.dm | 11 ++++++ .../psyker/psyker_events/severe/eyes_bleed.dm | 36 +++++++++++++++++++ .../psyker_events/severe/hallucinate.dm | 4 +-- .../psyker/psyker_events/severe/vomit.dm | 2 +- .../psyker_events/special/magic_trauma.dm | 2 +- tgstation.dme | 4 +++ 11 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/dizziness.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/twitching.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/exhaustion.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/eyes_bleed.dm 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 db67a4de85d334..4639f08c57c792 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -1,6 +1,6 @@ /datum/power/psyker_power/levitate name = "Levitate" - desc = "Grants the ability to float yourself above surfaces and letting you propel yourself in zero-gravity. Passively drains stress while in use." + desc = "Grants the ability to levitate yourself above surfaces and letting you propel yourself in zero-gravity. Passively drains stress while in use." value = 5 priority = POWER_PRIORITY_BASIC @@ -30,7 +30,6 @@ . = ..() psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) if(!levitating) - //ADD_TRAIT(owner, TRAIT_IGNORING_GRAVITY, MAGIC_TRAIT) owner.AddElement(/datum/element/forced_gravity, 0) owner.AddElement(/datum/element/simple_flying) to_chat(owner, span_boldnotice("Your body gently floats in the air!")) @@ -46,7 +45,6 @@ owner.add_overlay(caster_effect) playsound(owner, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) else - //REMOVE_TRAIT(owner, TRAIT_IGNORING_GRAVITY, MAGIC_TRAIT) owner.RemoveElement(/datum/element/forced_gravity, 0) owner.RemoveElement(/datum/element/simple_flying) to_chat(owner, span_boldnotice("You let yourself be affected by gravity once more.")) @@ -72,7 +70,7 @@ if(levitating) var/mob/living/carbon/human/psyker = owner var/cost = PSYKER_STRESS_TRIVIAL * 2 - if(psyker.get_quirk(/datum/quirk/paraplegic)) // There'll probably be several that'd like to do this. Effecively gives a 2x discount + if(psyker.get_quirk(/datum/quirk/paraplegic)) // There'll probably be several that'd like to do this. Effecively puts you just below the rate at which regen will keep up. cost = PSYKER_STRESS_TRIVIAL psyker_organ.add_stress(cost * seconds_per_tick) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm index f3196fa0883dfb..8ee63ea02340d0 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm @@ -6,7 +6,7 @@ // Remember to set abstracts to this. var/abstract_type = /datum/psyker_event var/weight = 1 - // For events that continue for a while, this skips the qdel step, under the condition you qdel it later. + // For events that continue for a while, this skips the qdel step. MAKE SURE YOU QDEL IT YOURSELF LATER INSIDE THE CODE. var/lingering = FALSE // Are there any special prerequisites? @@ -17,12 +17,15 @@ /datum/psyker_event/proc/execute(mob/living/carbon/human/psyker) return FALSE +// Milds generally want to not take you out of the flow but be noticeable enough that someone paying attention will notice they're pushing the line. /datum/psyker_event/mild abstract_type = /datum/psyker_event/mild +// Severe are the very clear warning to stop. These should be obvious and detrimental, with a clear goal of making it so that you stop and meditate or face the consequences. /datum/psyker_event/severe abstract_type = /datum/psyker_event/severe +// The consequences of your actions. Usually things that demand an immediate medbay visit or leave lingering consequences for the Psyker. /datum/psyker_event/catastrophic abstract_type = /datum/psyker_event/catastrophic diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm index 599cdbaf1ecd55..14e6691882cf90 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm @@ -5,7 +5,7 @@ return FALSE psyker.apply_status_effect(/datum/status_effect/heart_attack) //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them. - to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity...")) + to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity; as well as a feeling that something is very wrong.")) return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/dizziness.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/dizziness.dm new file mode 100644 index 00000000000000..576c0f9784feb7 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/dizziness.dm @@ -0,0 +1,7 @@ +// A mild dizzy, but enough to be noticed. +/datum/psyker_event/mild/dizziness + +/datum/psyker_event/mild/dizziness/execute(mob/living/carbon/human/psyker) + psyker.set_dizzy_if_lower(15 SECONDS) + to_chat(psyker, span_danger("Overusing your powers has made you dizzy!")) + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/twitching.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/twitching.dm new file mode 100644 index 00000000000000..8846dc42e94f9c --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/mild/twitching.dm @@ -0,0 +1,7 @@ +// Twitching, pretty mild. +/datum/psyker_event/mild/twitching + +/datum/psyker_event/mild/twitching/execute(mob/living/carbon/human/psyker) + psyker.set_jitter_if_lower(15 SECONDS) + to_chat(psyker, span_danger("Overusing your powers has made you twitchy!")) + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/exhaustion.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/exhaustion.dm new file mode 100644 index 00000000000000..f65c3a5471242f --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/exhaustion.dm @@ -0,0 +1,11 @@ +// Head rings, myriad of minor effects and a big chunk of stamina damage. +/datum/psyker_event/severe/exhaustion + +/datum/psyker_event/severe/exhaustion/execute(mob/living/carbon/human/psyker) + to_chat(psyker, span_userdanger("A loud ringing plays in your head, and you feel a wave of lethargy creep up on you.")) + psyker.apply_damage(70, STAMINA) + psyker.adjustOrganLoss(ORGAN_SLOT_BRAIN, BRAIN_DAMAGE_MILD, BRAIN_DAMAGE_MILD) + psyker.set_jitter_if_lower(5 SECONDS) + psyker.playsound_local(psyker, 'sound/effects/screech.ogg', 50, FALSE) + psyker.flash_act(visual = TRUE, length = 1 SECONDS) + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/eyes_bleed.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/eyes_bleed.dm new file mode 100644 index 00000000000000..839498e481d6e3 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/eyes_bleed.dm @@ -0,0 +1,36 @@ +// Bleeding from the eyes and all that. Classic trope. +// Deals a bunch of eye damage, makes you bleed and gives a temporary red overlay. +/datum/psyker_event/severe/eyes_bleed + lingering = TRUE // Needs to linger to apply the blind remove. + +/datum/psyker_event/severe/eyes_bleed/execute(mob/living/carbon/human/psyker) + var/obj/item/organ/eyes/eyes = psyker.get_organ_slot(ORGAN_SLOT_EYES) + var/obj/item/bodypart/head/head = psyker.get_bodypart(BODY_ZONE_HEAD) + if(isnull(eyes)) + return FALSE + if(!psyker.can_bleed()) + return FALSE + psyker.visible_message(span_notice("[psyker] begins to bleed from the eyes!"), span_userdanger("You feel blood begin to seep out from your eyes!")) + eyes.apply_organ_damage(15) // not enough to do anything bad unless it's already damaged. + head.adjustBleedStacks(10) + psyker.playsound_local(psyker, 'sound/effects/meatslap.ogg', 50, FALSE) + + // visual effects + psyker.become_nearsighted(src) + psyker.add_client_colour(/datum/client_colour/psyker_eyes_bleed, REF(src)) + addtimer(CALLBACK(src, PROC_REF(_remove_blind), psyker), 6 SECONDS) + return TRUE + +/datum/psyker_event/severe/eyes_bleed/proc/_remove_blind(mob/living/carbon/human/psyker) + // remove visual effects + psyker.cure_nearsighted(src) + psyker.remove_client_colour(REF(src)) + // lingering event so have to qdel self + qdel(src) + return + +/datum/client_colour/psyker_eyes_bleed + priority = CLIENT_COLOR_IMPORTANT_PRIORITY + color = COLOR_RED + fade_in = 1 + fade_out = 1 diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm index 8cc3b494f8fed4..550aa2ee871aa1 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/hallucinate.dm @@ -11,9 +11,9 @@ ) /datum/psyker_event/severe/hallucinate/execute(mob/living/carbon/human/psyker) - to_chat(psyker, span_danger("You begin to lose your grip on reality!")) + to_chat(psyker, span_userdanger("You begin to lose your grip on reality!")) // Generaly speaking we don't want these to last too long. - psyker.adjust_hallucinations(60 SECONDS) + psyker.set_hallucinations_if_lower(60 SECONDS) // We do also want immediate hallucinations as feedback, as the psyker_events double as stress warnings. psyker.cause_hallucination(pick(initial_hallucinations), src) return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm index 88b2bd06a46d54..f5f3db60431a79 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/severe/vomit.dm @@ -1,7 +1,7 @@ /datum/psyker_event/severe/vomit /datum/psyker_event/severe/vomit/execute(mob/living/carbon/human/psyker) - to_chat(psyker, span_danger("A wave of psychic energy overwhelms you, making you vomit!")) + to_chat(psyker, span_userdanger("A wave of psychic energy overwhelms you, making you vomit!")) psyker.vomit(VOMIT_CATEGORY_DEFAULT, lost_nutrition = 10) // Even though they may dryheave, the feedback is there from vomit(), so mission accomplished. return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm index 04da5046f3a3dc..ca552ce4646bd1 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm @@ -12,5 +12,5 @@ QDEL_NULL(trauma) return FALSE //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them. - to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity...")) + to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity; as well as a feeling that something is very wrong.")) return TRUE diff --git a/tgstation.dme b/tgstation.dme index cd0ec68388e9bf..26b52654bad8d9 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7443,8 +7443,12 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\_psyker_event.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\nosebleed.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\headache.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\dizziness.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\twitching.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\vomit.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\hallucinate.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\exhaustion.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\eyes_bleed.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\telekinetic_backlash.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\cardiac_arrest.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\special\magic_trauma.dm" From 16c10f2b2b080a44b1e7ff964ef5fbd314dab2a5 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 26 Jan 2026 11:51:43 +0100 Subject: [PATCH 020/212] Adds weighting to psyker powers., adds normal traumas as a catastrophic. --- code/__DEFINES/~doppler_defines/powers.dm | 14 ++++++++ .../powers/resonant/psyker/_psyker_organ.dm | 35 +++++++++---------- .../code/powers/resonant/psyker/levitate.dm | 2 +- .../psyker/psyker_events/_psyker_event.dm | 6 ++-- .../catastrophic/brain_trauma.dm | 11 ++++++ .../catastrophic/cardiac_arrest.dm | 2 +- .../{special => catastrophic}/magic_trauma.dm | 10 ++++-- tgstation.dme | 2 +- 8 files changed, 54 insertions(+), 28 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm rename modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/{special => catastrophic}/magic_trauma.dm (62%) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index c451ffb7a62d89..f216a3c11235dc 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -110,3 +110,17 @@ #define PSYKER_STRESS_MODERATE (PSYKER_STRESS_STANDARD_THRESHOLD / 5) #define PSYKER_STRESS_MAJOR (PSYKER_STRESS_STANDARD_THRESHOLD / 2) #define PSYKER_STRESS_CRUSHING (PSYKER_STRESS_STANDARD_THRESHOLD) + +// Psyker event tiers. +#define PSYKER_EVENT_TIER_MILD 1 +#define PSYKER_EVENT_TIER_SEVERE 2 +#define PSYKER_EVENT_TIER_CATASTROPHIC 3 + +// Psyker event rarities +#define PSYKER_EVENT_RARITY_COMMON 100 +#define PSYKER_EVENT_RARITY_UNCOMMON 50 +#define PSYKER_EVENT_RARITY_RARE 25 +#define PSYKER_EVENT_RARITY_VERYRARE 10 + +// Standard messages for Psyker Events +#define PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE "As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity; as well as a feeling that something is very wrong." diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index e9b9f98d0d4eaa..e23688bf394464 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -59,16 +59,16 @@ // Check if we do stress backlash after stress reduction. if(stress >= (stress_threshold * 2)) // Catastrophic event. - stress_backlash(3) + stress_backlash(PSYKER_EVENT_TIER_CATASTROPHIC) stress = 0 // No CD, just a hard reset and the consequences of your actions. CDstressMild = 0 CDstressSevere = 0 else if(stress >= (stress_threshold * 1.5) && CDstressSevere <= 0) // Severe Event CDstressSevere = 60 // reset CD - stress_backlash(2) + stress_backlash(PSYKER_EVENT_TIER_SEVERE) else if (stress >= stress_threshold && CDstressMild <= 0) // Mild Event CDstressMild = 60 // reset CD - stress_backlash(1) + stress_backlash(PSYKER_EVENT_TIER_MILD) if(CDstressMild > 0) CDstressMild = max(CDstressMild - seconds_per_tick, 0) @@ -91,38 +91,37 @@ var/base_type switch(degree) - if(1) + if(PSYKER_EVENT_TIER_MILD) base_type = /datum/psyker_event/mild - if(2) + if(PSYKER_EVENT_TIER_SEVERE) base_type = /datum/psyker_event/severe - if(3) - if(prob(20)) // If you get 'lucky' you get FUN - base_type = /datum/psyker_event/special - else - base_type = /datum/psyker_event/catastrophic + if(PSYKER_EVENT_TIER_CATASTROPHIC) + base_type = /datum/psyker_event/catastrophic else return FALSE pick_psyker_event(base_type, human) - return + return TRUE // Picks the backlash event /obj/item/organ/resonant/psyker/proc/pick_psyker_event(base_type, mob/living/carbon/human/human) - var/list/candidate_types = list() + var/list/candidates = list() + // We check for abstract types and assign the weights for(var/subtype in subtypesof(base_type)) var/datum/psyker_event/event_type = subtype - // Skip the abstract root itself if(initial(event_type.abstract_type) == subtype) continue - candidate_types += subtype - + var/weight = initial(event_type.weight) + candidates[subtype] = weight - while(length(candidate_types)) - var/subtype = pick(candidate_types) - candidate_types -= subtype + // We check the canidates, pick one, try it. If it returns true, we ened. If it returns false, we try another. + // In principle this should never fail because each category has one that will always return true. + while(length(candidates)) + var/subtype = pick_weight(candidate) + candidates -= subtype var/datum/psyker_event/event = new subtype 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 4639f08c57c792..1a4046337d29f5 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -47,7 +47,7 @@ else owner.RemoveElement(/datum/element/forced_gravity, 0) owner.RemoveElement(/datum/element/simple_flying) - to_chat(owner, span_boldnotice("You let yourself be affected by gravity once more.")) + to_chat(owner, span_boldnotice("You let yourself gently drop the ground.")) STOP_PROCESSING(SSfastprocess, src) levitating = FALSE // visual fx diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm index 8ee63ea02340d0..0895406e0da8da 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm @@ -5,7 +5,8 @@ /datum/psyker_event // Remember to set abstracts to this. var/abstract_type = /datum/psyker_event - var/weight = 1 + // check defines for weights. + var/weight = PSYKER_EVENT_RARITY_COMMON // For events that continue for a while, this skips the qdel step. MAKE SURE YOU QDEL IT YOURSELF LATER INSIDE THE CODE. var/lingering = FALSE @@ -28,6 +29,3 @@ // The consequences of your actions. Usually things that demand an immediate medbay visit or leave lingering consequences for the Psyker. /datum/psyker_event/catastrophic abstract_type = /datum/psyker_event/catastrophic - -/datum/psyker_event/special - abstract_type = /datum/psyker_event/special diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm new file mode 100644 index 00000000000000..a674702008f008 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm @@ -0,0 +1,11 @@ +// Gives deep-rooted normal traumas. +/datum/psyker_event/catastrophic/brain_taruma + weight = PSYKER_EVENT_RARITY_UNCOMMON + +/datum/psyker_event/catastrophic/brain_trauma/execute(mob/living/carbon/human/psyker) + if(psyker.gain_trauma_type(BRAIN_TRAUMA_SEVERE, TRAUMA_RESILIENCE_LOBOTOMY)) + // If we somehow fail to give them the trauma + return FALSE + //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them. + to_chat(psyker, span_userdanger(PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE)) + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm index 14e6691882cf90..db01856688d720 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/cardiac_arrest.dm @@ -5,7 +5,7 @@ return FALSE psyker.apply_status_effect(/datum/status_effect/heart_attack) //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them. - to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity; as well as a feeling that something is very wrong.")) + to_chat(psyker, span_userdanger(PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE)) return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/magic_trauma.dm similarity index 62% rename from modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm rename to modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/magic_trauma.dm index ca552ce4646bd1..f97d6eda38f96e 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/special/magic_trauma.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/magic_trauma.dm @@ -1,6 +1,8 @@ -/datum/psyker_event/special/magic_trauma +// Gives one of the wizard's magical traumas. +/datum/psyker_event/catastrophic/magic_trauma + weight = PSYKER_EVENT_RARITY_RARE -/datum/psyker_event/special/magic_trauma/execute(mob/living/carbon/human/psyker) +/datum/psyker_event/catastrophic/magic_trauma/execute(mob/living/carbon/human/psyker) var/datum/brain_trauma/magic/trauma if(prob(65)) // Poltergeists are a bit more thematic so they're a tad more common. trauma = new /datum/brain_trauma/magic/poltergeist @@ -8,9 +10,11 @@ trauma = new /datum/brain_trauma/magic/stalker // We are also not going to tell them they got a trauma. trauma.gain_text = null + if(!psyker.gain_trauma(trauma)) + // If we somehow fail to give them the trauma QDEL_NULL(trauma) return FALSE //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them. - to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity; as well as a feeling that something is very wrong.")) + to_chat(psyker, span_userdanger(PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE)) return TRUE diff --git a/tgstation.dme b/tgstation.dme index 26b52654bad8d9..8f1c69f53234ab 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7451,7 +7451,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\eyes_bleed.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\telekinetic_backlash.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\cardiac_arrest.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\special\magic_trauma.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\magic_trauma.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" #include "modular_doppler\modular_quirks\bouncy\bouncy.dm" From 059d809b8989270df6687b230ee5bcba648b7423 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 26 Jan 2026 13:35:14 +0100 Subject: [PATCH 021/212] Added the baseline dispel() system tho ngl it's kinda shit and I need help with this. --- code/__DEFINES/~doppler_defines/powers.dm | 3 ++ modular_doppler/modular_powers/code/_power.dm | 15 ++++++--- .../powers/resonant/psyker/_psyker_organ.dm | 2 +- .../code/powers/resonant/psyker/levitate.dm | 31 +++++++++++++++++++ .../catastrophic/silence_trauma.dm | 15 +++++++++ .../catastrophic/telekinetic_backlash.dm | 2 ++ .../code/powers/resonant/silence_trauma.dm | 23 ++++++++++++++ tgstation.dme | 3 ++ 8 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index f216a3c11235dc..4598c05b65b39d 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -34,6 +34,9 @@ /// This power is has a visual aspect in that it changes how the player looks. Used in generating dummies. #define POWER_CHANGES_APPEARANCE (1<<2) +// Trait for when you are unable to use resonant powers +#define TRAIT_RESONANCE_SILENCED + /** * SORCEROUS * All defines related to the sorcerous archetype. diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index e1945d1a9b6e09..b6e31e4bb127b7 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -156,20 +156,27 @@ /datum/power/proc/remove() return +/// Dispels the current power, basically a forced-off switch. Normally only used by resonant powers. +/// TRUE = There was a power, and whatever it was, its off now. FALSE = There was nothing to turn off +/datum/power/proc/dispel() + return FALSE + /// Any special effects or chat messages which should be applied. /// This proc is guaranteed to run if the mob has a client when the power is added. /// Otherwise, it runs once on the next COMSIG_MOB_LOGIN. /datum/power/proc/post_add() // Grants appropriate actions in the UI - grant_action() + if(action_path) + var/new_action_path = grant_action(action_path) + action_path = new_action_path return // Adds activateable power buttons. -/datum/power/proc/grant_action() - if(!ispath(action_path) || !power_holder) +/datum/power/proc/grant_action(datum/action/cooldown/power_path) + if(!ispath(power_path) || !power_holder) return FALSE - var/datum/action/cooldown/new_power = new action_path(src) + var/datum/action/cooldown/new_power = new power_path(src) new_power.background_icon_state = "bg_tech_blue" new_power.base_background_icon_state = new_power.background_icon_state new_power.active_background_icon_state = "[new_power.base_background_icon_state]_active" diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index e23688bf394464..664e871eb6c8f3 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -120,7 +120,7 @@ // We check the canidates, pick one, try it. If it returns true, we ened. If it returns false, we try another. // In principle this should never fail because each category has one that will always return true. while(length(candidates)) - var/subtype = pick_weight(candidate) + var/subtype = pick_weight(candidates) candidates -= subtype var/datum/psyker_event/event = new subtype 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 1a4046337d29f5..08c543031ee599 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -7,6 +7,13 @@ required_powers = list(/datum/power/psyker_root) action_path = /datum/action/cooldown/spell/levitate +/datum/power/psyker_power/levitate/dispel() + // TODO: Ask Ephe on how to do this better. + var/datum/action/cooldown/spell/levitate/to_be_dispelled = action_path + if(to_be_dispelled.dispel()) + return TRUE + return FALSE + /datum/action/cooldown/spell/levitate name = "Levitate" desc = "Toggles levitation, causing you to ignore the ground. Also allows for propulsion in zero-gravity. Passively drains stress while in use." @@ -74,4 +81,28 @@ cost = PSYKER_STRESS_TRIVIAL psyker_organ.add_stress(cost * seconds_per_tick) +// Dispel function; basically off-switch and possibly comedic faceplant +/datum/action/cooldown/spell/levitate/proc/dispel() + var/mob/living/carbon/human/victim = owner + if(levitating) + owner.RemoveElement(/datum/element/forced_gravity, 0) + owner.RemoveElement(/datum/element/simple_flying) + STOP_PROCESSING(SSfastprocess, src) + levitating = FALSE + // visual fx + if(caster_effect) + owner.cut_overlay(caster_effect) + caster_effect = null + playsound(owner, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + + // Do you have anything to brace your fall? Or do you possibly manage to get lucky? + var/obj/item/organ/wings/gliders = owner.get_organ_by_type(/obj/item/organ/wings) + if(HAS_TRAIT(owner, TRAIT_FREERUNNING) || gliders?.can_soften_fall() || prob(30)) + to_chat(owner, span_warning("You suddenly fall to the ground, but manage to catch yourself!")) + else + to_chat(owner, span_userdanger("You suddenly fall to the ground!")) + playsound(owner, 'sound/effects/desecration/desecration-02.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + victim.Knockdown(3 SECONDS) + return TRUE + return FALSE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm new file mode 100644 index 00000000000000..979877c9289dd9 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm @@ -0,0 +1,15 @@ +// Gives a special deep-rooted trauma that silences Resonance powers all-together. +/datum/psyker_event/catastrophic/silence_trauma + weight = PSYKER_EVENT_RARITY_UNCOMMON + +/datum/psyker_event/catastrophic/silence_trauma/execute(mob/living/carbon/human/psyker) + var/datum/brain_trauma/magic/trauma = new /datum/brain_trauma/magic/resonance_silenced + trauma.gain_text = null + if(!psyker.gain_trauma(trauma)) + // If we somehow fail to give them the trauma + QDEL_NULL(trauma) + return FALSE + // We replicate the trauma message just in a different span. + to_chat(psyker, span_userdanger("You feel like you're no longer in touch with your own Resonant powers.")) + return TRUE + diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm index 3d621d02e767e9..2d2ba4967e5f19 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/telekinetic_backlash.dm @@ -9,6 +9,8 @@ var/severe_brute = 10 var/critical_brute = 20 + weight = PSYKER_EVENT_RARITY_UNCOMMON + /datum/psyker_event/catastrophic/telekinetic_backlash/execute(mob/living/carbon/human/psyker) to_chat(psyker, span_userdanger("As you strain your psychic powers past the breaking point, you feel yourself wracked by pain, as your skin, bones and flesh are pulled in all manner of directions!")) diff --git a/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm new file mode 100644 index 00000000000000..1607710619df96 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm @@ -0,0 +1,23 @@ +/datum/brain_trauma/magic/resonance_silenced + name = "Aresonaphasia" + desc = "Patient is unable to wield their own Resonant powers." + scan_desc = "resonance silenced" + gain_text = span_notice("You feel like you're no longer in touch with your own Resonant powers.") + lose_text = span_notice("You begin to feel your Resonant Powers returning.") + +/datum/brain_trauma/magic/resonance_silenced/on_gain() + // Dispel everything + var/list/powers_list = owner.powers + if(!length(powers_list)) + return + + for(var/datum/power/power_instance in powers_list) + power_instance.dispel() + + // TODO: actually make the silence work (the spell_requirements code scares me) + //ADD_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAUMA_TRAIT) + . = ..() + +/datum/brain_trauma/magic/resonance_silenced/on_lose() + //REMOVE_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAUMA_TRAIT) + ..() diff --git a/tgstation.dme b/tgstation.dme index 8f1c69f53234ab..3894d6ae6ca6f8 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7435,6 +7435,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_organ.dm" @@ -7452,6 +7453,8 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\telekinetic_backlash.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\cardiac_arrest.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\magic_trauma.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\silence_trauma.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\brain_trauma.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" #include "modular_doppler\modular_quirks\bouncy\bouncy.dm" From 718a8c21ab04218a5891ff218d883fe1fd836e41 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 26 Jan 2026 13:44:37 +0100 Subject: [PATCH 022/212] this week has sired doubts in my status as non-dyslexic --- .../resonant/psyker/psyker_events/catastrophic/brain_trauma.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm index a674702008f008..e0697394424593 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm @@ -1,5 +1,5 @@ // Gives deep-rooted normal traumas. -/datum/psyker_event/catastrophic/brain_taruma +/datum/psyker_event/catastrophic/brain_trauma weight = PSYKER_EVENT_RARITY_UNCOMMON /datum/psyker_event/catastrophic/brain_trauma/execute(mob/living/carbon/human/psyker) From 3c4d1e19380bae16451129a7b5c699781f365d4d Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 26 Jan 2026 15:05:43 +0100 Subject: [PATCH 023/212] Base Theolgist Roots. I need to finish these; but plans have changed. --- .../sorcerous/theologist/_theologist_root.dm | 34 +++++++++++++++++++ .../theologist/_theologist_root_revered.dm | 12 +++++++ .../theologist/_theologist_root_shared.dm | 11 ++++++ .../theologist/_theologist_root_twisted.dm | 11 ++++++ tgstation.dme | 4 +++ 5 files changed, 72 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm new file mode 100644 index 00000000000000..5da7e248653ae4 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm @@ -0,0 +1,34 @@ +/datum/power/theologist_root + name = "Abstract Theologist Root" + desc = "There's no 4th theologist root; this is just debug code. \ + Please report this!" + abstract_parent_type = /datum/power/theologist_root + + // Used as a resource. We permit decimal numbers, but the UI will always show non-decimals. + var/piety = 0 + + // The UI itself + var/atom/movable/screen/theologist_piety/theologist_ui + +/datum/power/theologist_root/New() + . = ..() + //Grants the UI element for Piety +/* +// UI Elements for Piety +/atom/movable/screen/theologoist_resource + name = "piety" + icon = 'icons/hud/blob.dmi' + icon_state = "corehealth" + screen_loc = "EAST+0:23,NORTH-1:5" // We really need to use a define for this but currently I'm lazy + +/atom/movable/screen/theologist_framework + name = "piety framework" + icon_state = "block" + screen_loc = ui_health + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + plane = ABOVE_HUD_PLANE + + var/piety_ui = new /atom/movable/screen/theologoist_resource + infodisplay += piety_ui + +*/ diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm new file mode 100644 index 00000000000000..eba1861bee6bc2 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -0,0 +1,12 @@ + +/datum/power/theologist_root/revered + name = "A Burden Revered" + desc = "Nullifies pain and slowly heals the creature over a prolonged period of time. \ + Grants piety based on healing done, ends prematurely if the target reaches full health. \ + This is mutually exclusive with the other 'A Burden...' powers." + + value = 5 + mob_trait = TRAIT_ARCHETYPE_SORCEROUS + 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_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm new file mode 100644 index 00000000000000..455fcf1db6c212 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -0,0 +1,11 @@ +/datum/power/theologist_root/shared + name = "A Burden Shared" + desc = "tbd \ + tbd \ + This is mutually exclusive with the other 'A Burden...' powers." + + value = 5 + mob_trait = TRAIT_ARCHETYPE_SORCEROUS + 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_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm new file mode 100644 index 00000000000000..76ca1c28dd4b73 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -0,0 +1,11 @@ +/datum/power/theologist_root/twisted + name = "A Burden Twisted" + desc = "tbd \ + tbd \ + This is mutually exclusive with the other 'A Burden...' powers." + + value = 5 + mob_trait = TRAIT_ARCHETYPE_SORCEROUS + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THEOLOGIST + priority = POWER_PRIORITY_ROOT diff --git a/tgstation.dme b/tgstation.dme index 3894d6ae6ca6f8..11186d1eb0990b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7434,6 +7434,10 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\lodestone_legends.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_shared.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_twisted.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_revered.dm" #include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" From a2de5c6f024bf131b7c5cf1afa204cf777c79dfa Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 26 Jan 2026 18:51:07 +0100 Subject: [PATCH 024/212] Central action system for powers --- code/__DEFINES/~doppler_defines/powers.dm | 2 +- modular_doppler/modular_powers/code/_power.dm | 16 ++--- .../code/powers/resonant/meditate.dm | 26 ++++---- .../powers/resonant/psyker/_psyker_action.dm | 45 +++++++++++++ .../powers/resonant/psyker/_psyker_organ.dm | 8 +-- .../powers/resonant/psyker/_psyker_root.dm | 2 +- .../code/powers/resonant/psyker/levitate.dm | 41 +++++------- .../powers/resonant/psyker/telekinesis.dm | 8 ++- .../modular_powers/code/powers_action.dm | 65 +++++++++++++++++++ tgstation.dme | 2 + 10 files changed, 157 insertions(+), 58 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_action.dm create mode 100644 modular_doppler/modular_powers/code/powers_action.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 4598c05b65b39d..a12f2eb1420226 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -35,7 +35,7 @@ #define POWER_CHANGES_APPEARANCE (1<<2) // Trait for when you are unable to use resonant powers -#define TRAIT_RESONANCE_SILENCED +#define TRAIT_RESONANCE_SILENCED "RESONANCE_SILENCED" /** * SORCEROUS diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index b6e31e4bb127b7..4e964349e3bd6f 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -172,20 +172,16 @@ return // Adds activateable power buttons. -/datum/power/proc/grant_action(datum/action/cooldown/power_path) +/datum/power/proc/grant_action(datum/action/power/power_path) if(!ispath(power_path) || !power_holder) return FALSE - var/datum/action/cooldown/new_power = new power_path(src) - new_power.background_icon_state = "bg_tech_blue" - new_power.base_background_icon_state = new_power.background_icon_state - new_power.active_background_icon_state = "[new_power.base_background_icon_state]_active" - new_power.overlay_icon_state = "bg_tech_blue_border" - new_power.active_overlay_icon_state = "bg_spell_border_active_blue" - new_power.panel = "Powers" - new_power.Grant(power_holder) + var/datum/action/power/new_action = new power_path(src) + // TODO: Browse this and see how much of this we can move to the action subtypes. + new_action.origin_power = src + new_action.Grant(power_holder) - return new_power + return new_action /// Returns if the power holder should process currently or not. /datum/power/proc/should_process() diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index 9f114a02d9137a..1285ec946462ea 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -1,49 +1,49 @@ -/datum/action/cooldown/spell/resonant_meditate +/* Since this is used by two different archetypes there will be a bit of snowflaking. +Reduces stress for psykers and restores Dantian for cultivators +*/ + +/datum/action/power/resonant_meditate name = "Resonant Meditation" desc = "Restores the full potential of your resonant powers." button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "chuuni" - spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC - antimagic_flags = MAGIC_RESISTANCE_MIND - cooldown_time = 6 SECONDS // Just don't let people spam it on or off. - - // Both Cultivator and Psyker can benefit from meditate. // The components responsible for meditation. var/obj/item/organ/resonant/psyker/psyker_organ var/cultivator_organ //TODO: Cultivator Organ -/datum/action/cooldown/spell/resonant_meditate/cast() +/datum/action/power/resonant_meditate/use_action() . = ..() var/keep_going = TRUE var/mob/living/spotlighttarget = owner // cause we need to call it on a mob/living - to_chat(owner, "You start meditating.") + to_chat(owner, span_notice("You start meditating.")) update_organs() // Adds visual effects var/datum/status_effect/spotlight_light/light = get_spotlight_color() spotlighttarget.apply_status_effect(light, 3000) do + active = TRUE if(do_after(owner, 25, target = owner)) if(!psyker_organ) - to_chat(owner, "I have nothing to meditate on!") + to_chat(owner, span_notice("I have nothing to meditate on!")) keep_going = FALSE if(psyker_organ) psyker_organ.remove_stress(PSYKER_STRESS_MEDITATION_POWER) if(psyker_organ.stress <= 0) - to_chat(owner, "I no longer feel any stress") + to_chat(owner, span_notice("I no longer feel any stress")) keep_going = FALSE else keep_going = FALSE while (keep_going) to_chat(owner, "You stop meditating.") + active = FALSE spotlighttarget.remove_status_effect(light) return -/datum/action/cooldown/spell/resonant_meditate/proc/get_spotlight_color() - +/datum/action/power/resonant_meditate/proc/get_spotlight_color() if(psyker_organ && cultivator_organ) return /datum/status_effect/spotlight_light/resonant else if(psyker_organ) @@ -53,7 +53,7 @@ else return /datum/status_effect/spotlight_light -/datum/action/cooldown/spell/resonant_meditate/proc/update_organs() +/datum/action/power/resonant_meditate/proc/update_organs() psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) //TODO: Cultivator Organ diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_action.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_action.dm new file mode 100644 index 00000000000000..ca76d0454f95b0 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_action.dm @@ -0,0 +1,45 @@ +/datum/action/power/psyker + name = "abstract psyker power action - ahelp this" + background_icon_state = "bg_hive" + overlay_icon_state = "bg_hive_border" + button_icon = 'icons/mob/actions/backgrounds.dmi' + + // The organ that processes most of the Psyker Powers. Mostly all functions here communicate with this. + var/obj/item/organ/resonant/psyker/psyker_organ + + // Disabled by the trait + var/disabled_by_mental_immunity = TRUE + +/datum/action/power/psyker/New() + . = ..() + ValidateOrgan() + +// Actually checks if our Psyker Organ is there. We really want to check this every use. +/datum/action/power/psyker/proc/ValidateOrgan() + if(owner) // Prevents runtiming on start + psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) + if(psyker_organ) + return TRUE + return FALSE + +// This doesn't actually add the stress itself; it merely tells the organ to add the stress. +// Why not handle it here? Because why give them an organ if we're not going to use it?! +// Validation is handled on the organ side. +/datum/action/power/psyker/proc/add_stress(amount) + psyker_organ.add_stress(amount) + +/datum/action/power/psyker/proc/remove_stress(amount) + psyker_organ.remove_stress(amount) + +// 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/power/psyker/try_use(mob/living/user, mob/living/target) + if(!ValidateOrgan()) + owner.balloon_alert(owner, "No paracausal gland!") + return FALSE + if(disabled_by_mental_immunity && owner.can_block_magic(MAGIC_RESISTANCE_MIND)) + add_stress(PSYKER_STRESS_MINOR) // This actively stresses you out. + owner.balloon_alert(owner, "Something interveres with your powers!") + return FALSE + . = .. () + + diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index 664e871eb6c8f3..7c4378115df27a 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -24,16 +24,12 @@ // Don't modify stress directly. In the future affinity has a bearing on how much stress you gain. /obj/item/organ/resonant/psyker/proc/add_stress(amount) // TODO; Add clothing affinity. Wearing psychic nicknacks makes you gain less stress. - stress += amount - if(stress <= 0) - stress = 0 + stress = max(0, stress + amount) return /obj/item/organ/resonant/psyker/proc/remove_stress(amount) // TODO: Ditto on above. - stress -= amount - if(stress <= 0) - stress = 0 + stress = max(0, stress - amount) return /obj/item/organ/resonant/psyker/on_life(seconds_per_tick, times_fired) 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 b01978dd8cec20..c3c98122186be9 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 @@ -11,7 +11,7 @@ path = POWER_PATH_PSYKER priority = POWER_PRIORITY_ROOT - action_path = /datum/action/cooldown/spell/resonant_meditate // todo; deal with duplicates + action_path = /datum/action/power/resonant_meditate // todo; deal with duplicates var/obj/item/organ/resonant/psyker/psyker_organ diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm index 08c543031ee599..14e7f458086dc0 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -5,43 +5,33 @@ value = 5 priority = POWER_PRIORITY_BASIC required_powers = list(/datum/power/psyker_root) - action_path = /datum/action/cooldown/spell/levitate + action_path = /datum/action/power/psyker/levitate /datum/power/psyker_power/levitate/dispel() // TODO: Ask Ephe on how to do this better. - var/datum/action/cooldown/spell/levitate/to_be_dispelled = action_path + var/datum/action/power/psyker/levitate/to_be_dispelled = action_path if(to_be_dispelled.dispel()) return TRUE return FALSE -/datum/action/cooldown/spell/levitate +/datum/action/power/psyker/levitate name = "Levitate" desc = "Toggles levitation, causing you to ignore the ground. Also allows for propulsion in zero-gravity. Passively drains stress while in use." button_icon = 'icons/mob/actions/actions_minor_antag.dmi' button_icon_state = "beam_up" - spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC - antimagic_flags = MAGIC_RESISTANCE_MIND - cooldown_time = 2 SECONDS - - // Are we currently levitating? - var/levitating = FALSE - - // Psyker organ for stress. - var/obj/item/organ/resonant/psyker/psyker_organ - // Overlay we add to the caster var/mutable_appearance/caster_effect -/datum/action/cooldown/spell/levitate/cast() +/datum/action/power/psyker/levitate/use_action() . = ..() psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) - if(!levitating) + if(!active) owner.AddElement(/datum/element/forced_gravity, 0) owner.AddElement(/datum/element/simple_flying) to_chat(owner, span_boldnotice("Your body gently floats in the air!")) START_PROCESSING(SSfastprocess, src) - levitating = TRUE + active = TRUE //visual fx caster_effect = mutable_appearance( icon = 'icons/effects/effects.dmi', @@ -56,7 +46,7 @@ owner.RemoveElement(/datum/element/simple_flying) to_chat(owner, span_boldnotice("You let yourself gently drop the ground.")) STOP_PROCESSING(SSfastprocess, src) - levitating = FALSE + active = FALSE // visual fx if(caster_effect) owner.cut_overlay(caster_effect) @@ -65,30 +55,29 @@ return -/datum/action/cooldown/spell/levitate/process(seconds_per_tick) - // Calling the parent process() stops processing when the ability is off cooldown, which is not what we want. +/datum/action/power/psyker/levitate/process(seconds_per_tick) if(!owner) - build_all_button_icons(UPDATE_BUTTON_STATUS) STOP_PROCESSING(SSfastprocess, src) return - build_all_button_icons(UPDATE_BUTTON_STATUS) // Passive stress cost - if(levitating) + if(active) var/mob/living/carbon/human/psyker = owner var/cost = PSYKER_STRESS_TRIVIAL * 2 if(psyker.get_quirk(/datum/quirk/paraplegic)) // There'll probably be several that'd like to do this. Effecively puts you just below the rate at which regen will keep up. cost = PSYKER_STRESS_TRIVIAL - psyker_organ.add_stress(cost * seconds_per_tick) + add_stress(cost * seconds_per_tick) // Dispel function; basically off-switch and possibly comedic faceplant -/datum/action/cooldown/spell/levitate/proc/dispel() + +// TODO: TURN THIS INTO A LISTENER +/datum/action/power/psyker/levitate/proc/dispel() var/mob/living/carbon/human/victim = owner - if(levitating) + if(active) owner.RemoveElement(/datum/element/forced_gravity, 0) owner.RemoveElement(/datum/element/simple_flying) STOP_PROCESSING(SSfastprocess, src) - levitating = FALSE + active = FALSE // visual fx if(caster_effect) owner.cut_overlay(caster_effect) 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 2f1714e89e5898..6f89e021daf5c5 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -1,4 +1,8 @@ - +/* This leviathan of spaghetti is based off of the MODsuit modules. +It is currently still using spells as a system rather than powers. +BIG TODO: FIX THAT +*/ +/* /datum/power/psyker_power/telekinesis name = "Telekinesis" desc = "Grants the ability to manipulate and move various objects. Generates stress based upon weight on pick-up and throw, as well as passively while holding an object." @@ -338,3 +342,5 @@ alpha = 180 color = "#8A2BE2" mouse_opacity = MOUSE_OPACITY_OPAQUE + +*/ diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm new file mode 100644 index 00000000000000..0cacf4d0f20bf0 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -0,0 +1,65 @@ +/* + Custom action system for supporting the powers system. Use this anytime you add actions to a power. + Almost all archetypes have their own subtype to handle their own resources and mechanics. + This one is largely responsible for the actions framework itself. + + Largely modeled after changeling_power.dm +*/ +/datum/action/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' + + /// Maximum stat before the ability is blocked. + /// For example, `UNCONSCIOUS` prevents it from being used when in hard crit or dead, + /// while `DEAD` allows the ability to be used on any stat values. + var/req_stat = CONSCIOUS + /// used by a few powers that toggle + var/active = FALSE + /// Does this ability stop working if you are silenced? + var/disabled_by_silence = TRUE + /// What power gave the origin? + var/origin_power + /// Can only humans use this power? + var/human_only = TRUE + +// When you press the button +/datum/action/power/Trigger(mob/clicker, trigger_flags) + var/mob/user = owner + if(!user) + return + try_use(user) + +// Attempts to actively use the action +/datum/action/power/proc/try_use(mob/living/user, mob/living/target) + SHOULD_CALL_PARENT(TRUE) + if(disabled_by_silence && HAS_TRAIT(owner, TRAIT_RESONANCE_SILENCED)) + owner.balloon_alert(user, "silenced!") + return FALSE + if(use_action(user, target)) + return TRUE + return FALSE + +// Validates the action can be used. +/datum/action/power/proc/can_use(mob/living/user, mob/living/target) + SHOULD_CALL_PARENT(TRUE) + if(!can_be_used_by(user)) // Runs can_be_used_by below + return FALSE + if(req_stat < user.stat) // Are we conscious? + owner.balloon_alert(user, "incapacitated!") + return FALSE + return TRUE + +// Checks if we exist (wow) and are human. +/datum/action/power/proc/can_be_used_by(mob/living/user) + SHOULD_CALL_PARENT(TRUE) + if(QDELETED(user)) + return FALSE + if(!ishuman(owner) && human_only) + return FALSE + return TRUE + +// Now we do THINGS! +/datum/action/power/proc/use_action(mob/living/user, mob/living/target) + return TRUE diff --git a/tgstation.dme b/tgstation.dme index 11186d1eb0990b..9e5974b1c6a680 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7421,6 +7421,7 @@ #include "modular_doppler\_savefile_migration\code\_preferences_savefile.dm" #include "modular_doppler\_savefile_migration\code\powers_migration.dm" #include "modular_doppler\modular_powers\code\_power.dm" +#include "modular_doppler\modular_powers\code\powers_action.dm" #include "modular_doppler\modular_powers\code\powers_living.dm" #include "modular_doppler\modular_powers\code\powers_helpers.dm" #include "modular_doppler\modular_powers\code\powers_prefs.dm" @@ -7440,6 +7441,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_revered.dm" #include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_organ.dm" From 3f7d1641a5d3e76c84db4c5d6aa381f1268e6844 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 26 Jan 2026 21:27:11 +0100 Subject: [PATCH 025/212] Targeting system added to powers. Expansion of theologist system. --- modular_doppler/modular_powers/code/_power.dm | 2 + .../powers/resonant/psyker/_psyker_action.dm | 8 +- .../theologist/_theologist_action.dm | 35 ++++++ .../sorcerous/theologist/_theologist_piety.dm | 48 ++++++++ .../sorcerous/theologist/_theologist_root.dm | 41 ++----- .../theologist/_theologist_root_revered.dm | 28 ++++- .../modular_powers/code/powers_action.dm | 110 +++++++++++++++++- tgstation.dme | 2 + 8 files changed, 233 insertions(+), 41 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_action.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_piety.dm diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index 4e964349e3bd6f..48d626d3e25689 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -28,6 +28,8 @@ /// A list of traits that should stop this power from processing. /// Signals for adding and removing this trait will automatically be added to `process_update_signals`. var/list/no_process_traits + // Is it not available in the preference menu? + var/available_in_prefs = TRUE /// The overarching archetype this belongs to. var/archetype 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 ca76d0454f95b0..28161d2a789db8 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 @@ -18,9 +18,9 @@ /datum/action/power/psyker/proc/ValidateOrgan() if(owner) // Prevents runtiming on start psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) - if(psyker_organ) - return TRUE - return FALSE + if(!psyker_organ) + return FALSE + return TRUE // This doesn't actually add the stress itself; it merely tells the organ to add the stress. // Why not handle it here? Because why give them an organ if we're not going to use it?! @@ -36,7 +36,7 @@ if(!ValidateOrgan()) owner.balloon_alert(owner, "No paracausal gland!") return FALSE - if(disabled_by_mental_immunity && owner.can_block_magic(MAGIC_RESISTANCE_MIND)) + if(disabled_by_mental_immunity && target.can_block_magic(MAGIC_RESISTANCE_MIND)) add_stress(PSYKER_STRESS_MINOR) // This actively stresses you out. owner.balloon_alert(owner, "Something interveres with your powers!") return FALSE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_action.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_action.dm new file mode 100644 index 00000000000000..18a4c0f73fbe87 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_action.dm @@ -0,0 +1,35 @@ +/datum/action/power/theologist + name = "abstract theologist power action - ahelp this" + background_icon_state = "bg_clock" + overlay_icon_state = "bg_clock_border" + button_icon = 'icons/mob/actions/backgrounds.dmi' + + // The organ that processes most of the Psyker Powers. Mostly all functions here communicate with this. + var/datum/power/theologist_piety/piety_power + + // The UI used for piety.alist + var/atom/movable/screen/theologist_piety/theologist_ui + +/datum/action/power/theologist/New() + . = ..() + ValidatePietyPower() + +// Since Theologist has both 3 roots and a persistent resource system, we use a hidden extra power for handling Piety. +/datum/action/power/theologist/proc/ValidatePietyPower() + if(owner) // Prevents runtiming on start + var/mob/living/carrier = owner + piety_power = carrier.get_power(/datum/power/theologist_piety) + if(!piety_power) + return FALSE + return TRUE + +// Validation handled in the piety power. +/datum/action/power/theologist/proc/adjust_piety(amount, override_cap) + piety_power.adjust_piety(amount, override_cap) + +// We check to see if our piety power is actually there, because usually things will go bad if they don't. +/datum/action/power/theologist/try_use(mob/living/user, mob/living/target) + if(!ValidatePietyPower()) + owner.balloon_alert(owner, "Yell at the coders; you're missing your piety system!") + return FALSE + . = .. () diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_piety.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_piety.dm new file mode 100644 index 00000000000000..71855f8723a14c --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_piety.dm @@ -0,0 +1,48 @@ +/// Helper to format the text that gets thrown onto the piety hud element. +#define FORMAT_PIETY_TEXT(charges) MAPTEXT("
[round(charges)]
") + +/datum/power/theologist_piety + name = "Piety" + desc = "Responsible for managing Piety." + abstract_parent_type = /datum/power + value = 0 + + mob_trait = TRAIT_ARCHETYPE_SORCEROUS + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THEOLOGIST + priority = POWER_PRIORITY_ROOT + + // Used as a resource. We permit decimal numbers, but the UI will always show non-decimals. + var/piety = 0 + + //At what point do we cap out piety? + var/max_piety = 20 + + // The UI itself + var/atom/movable/screen/theologist_piety/theologist_ui + +/datum/power/theologist_piety/New() + . = ..() + //Grants the UI element for Piety + if(power_holder) // Prevents runtiming at init + if(power_holder.hud_used) + var/datum/hud/hud_used = power_holder.hud_used + + theologist_ui = new /atom/movable/screen/theologist_piety(null, hud_used) + hud_used.infodisplay += theologist_ui + +// UI Elements for Piety +/atom/movable/screen/theologist_piety + name = "piety" + icon = 'icons/hud/blob.dmi' + icon_state = "corehealth" + screen_loc = "WEST,CENTER-1:15" // TODO: Define & Move this. + + +/datum/power/theologist_piety/proc/adjust_piety(amount, override_cap) + if(!isnum(amount)) + return + var/cap_to = isnum(override_cap) ? override_cap : max_piety + piety = clamp(piety + amount, 0, cap_to) + + theologist_ui?.maptext = FORMAT_PIETY_TEXT(piety) 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 5da7e248653ae4..ca23dac98c99dc 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 @@ -1,34 +1,17 @@ /datum/power/theologist_root - name = "Abstract Theologist Root" - desc = "There's no 4th theologist root; this is just debug code. \ - Please report this!" + name = "Abstract theologist root" + desc = "Oh noes, tell the coders!" abstract_parent_type = /datum/power/theologist_root - // Used as a resource. We permit decimal numbers, but the UI will always show non-decimals. - var/piety = 0 + mob_trait = TRAIT_ARCHETYPE_SORCEROUS + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THEOLOGIST + priority = POWER_PRIORITY_ROOT - // The UI itself - var/atom/movable/screen/theologist_piety/theologist_ui - -/datum/power/theologist_root/New() +/datum/power/theologist_root/revered/post_add() + if(!power_holder) // So it doesn't runtime at init + return + // We pass along the piety power to actually handle most of the piety stuff. + var/datum/power/theologist_piety/piety = new /datum/power/theologist_piety + piety.add_to_holder(new_holder = power_holder) . = ..() - //Grants the UI element for Piety -/* -// UI Elements for Piety -/atom/movable/screen/theologoist_resource - name = "piety" - icon = 'icons/hud/blob.dmi' - icon_state = "corehealth" - screen_loc = "EAST+0:23,NORTH-1:5" // We really need to use a define for this but currently I'm lazy - -/atom/movable/screen/theologist_framework - name = "piety framework" - icon_state = "block" - screen_loc = ui_health - mouse_opacity = MOUSE_OPACITY_TRANSPARENT - plane = ABOVE_HUD_PLANE - - var/piety_ui = new /atom/movable/screen/theologoist_resource - infodisplay += piety_ui - -*/ diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index eba1861bee6bc2..31825d8e2275bc 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -2,11 +2,29 @@ /datum/power/theologist_root/revered name = "A Burden Revered" desc = "Nullifies pain and slowly heals the creature over a prolonged period of time. \ - Grants piety based on healing done, ends prematurely if the target reaches full health. \ + Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again. \ This is mutually exclusive with the other 'A Burden...' powers." + action_path = /datum/action/power/theologist/theologist_root/revered value = 5 - mob_trait = TRAIT_ARCHETYPE_SORCEROUS - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THEOLOGIST - priority = POWER_PRIORITY_ROOT + +/datum/action/power/theologist/theologist_root/revered + name = "A Burden Revered" + desc = "Nullifies pain and slowly heals the creature over a prolonged period of time. \ + Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "transformslime" + target_range = 1 + click_to_activate = TRUE + +/datum/action/power/theologist/theologist_root/revered/use_action(mob/living/user, mob/living/target) + to_chat(owner, span_boldnotice("Your body gently floats in the air!")) + return TRUE + +/datum/action/power/theologist/theologist_root/revered/on_activation(mob/living/user) + to_chat(owner, span_notice("You ready yourself to relieve the burden of others!
Left-click a creature next to you to target them!")) + +/datum/action/power/theologist/theologist_root/revered/on_deactivation(mob/living/user) + to_chat(owner, span_notice("You withdraw your power.")) + +///datum/power/theologist_root/revered/process() diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 0cacf4d0f20bf0..311e7370f0fd93 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -24,16 +24,35 @@ /// Can only humans use this power? var/human_only = TRUE + // Is it an ability that requires us to click our mouse? + var/click_to_activate = FALSE + /// Maximum targeting range (in tiles) for click_to_activate powers. Set to 0 or null for no range limit. + var/target_range = 7 + /// The click cooldown added onto the user's next click (only for click_to_activate abilities) + var/click_cd_override = CLICK_CD_CLICK_ABILITY + /// If TRUE, we will unset after using our click intercept. + var/unset_after_click = TRUE + /// What icon to replace our mouse cursor with when active. + var/ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + // When you press the button -/datum/action/power/Trigger(mob/clicker, trigger_flags) - var/mob/user = owner +/datum/action/power/Trigger(mob/clicker, trigger_flags, atom/target) + var/mob/living/user = owner if(!user) return - try_use(user) + + // Click-to-activate powers set themselves as a click intercept, and then wait for a left click target. + if(click_to_activate) + return handle_click_to_activate(user, target) + + // Non-targeted powers just use immediately. + return try_use(user, target = null) // Attempts to actively use the action /datum/action/power/proc/try_use(mob/living/user, mob/living/target) SHOULD_CALL_PARENT(TRUE) + if(!can_use(user, target)) + return FALSE if(disabled_by_silence && HAS_TRAIT(owner, TRAIT_RESONANCE_SILENCED)) owner.balloon_alert(user, "silenced!") return FALSE @@ -63,3 +82,88 @@ // Now we do THINGS! /datum/action/power/proc/use_action(mob/living/user, mob/living/target) return TRUE + +/* +Handles all the logic involved in using a targeted, click-based action. +- First press: enables click intercept (targeting mode) +- Second press (while already active): disables click intercept +- While active: a left click calls InterceptClickOn() and passes the clicked atom as target +*/ +/datum/action/power/proc/handle_click_to_activate(mob/living/user, atom/target) + // If this was called with a direct target (ex: some automated caller), treat it like a click immediately. + if(target) + return InterceptClickOn(user, null, target) + + var/datum/action/power/already_set = user.click_intercept + if(already_set == src) + // If we clicked ourself and we're already set, unset and return + return unset_click_ability(user) + + else if(istype(already_set)) + // If we have an active one set already, unset it before we set ours + already_set.unset_click_ability(user) + + return set_click_ability(user) + +/// Intercepts client owner clicks to activate the ability. +/// Note: this is called by the click intercept system on left click. +/datum/action/power/proc/InterceptClickOn(mob/living/clicker, params, atom/target) + if(!target) + return FALSE + + // Range gate (only applies if target_range is non-zero). + if(target_range) + var/turf/clicker_turf = get_turf(clicker) + var/turf/target_turf = get_turf(target) + if(clicker_turf && target_turf && get_dist(clicker_turf, target_turf) > target_range) + to_chat(clicker, span_warning("Out of range!")) + return FALSE + + // If the power can't be used, refuse the click and keep intercept state as-is. + if(!try_use(clicker, target)) + return FALSE + + // Successful click. + if(unset_after_click) + unset_click_ability(clicker) + + clicker.next_click = world.time + click_cd_override + return TRUE + +/** + * Set our action as the click override on the passed mob. + */ +/datum/action/power/proc/set_click_ability(mob/on_who) + SHOULD_CALL_PARENT(TRUE) + + on_who.click_intercept = src + if(ranged_mousepointer) + on_who.client?.mouse_override_icon = ranged_mousepointer + on_who.update_mouse_pointer() + + build_all_button_icons(UPDATE_BUTTON_STATUS) + on_activation(on_who) + return TRUE + +/** + * Unset our action as the click override of the passed mob. + */ +/datum/action/power/proc/unset_click_ability(mob/on_who) + SHOULD_CALL_PARENT(TRUE) + + on_who.click_intercept = null + if(ranged_mousepointer) + on_who.client?.mouse_override_icon = initial(on_who.client?.mouse_override_icon) + on_who.update_mouse_pointer() + + build_all_button_icons(UPDATE_BUTTON_STATUS) + on_deactivation(on_who) + return TRUE + +/// These only call on pointed actions +/datum/action/power/proc/on_activation(mob/living/user) + return + +/// Same as above +/datum/action/power/proc/on_deactivation(mob/living/user) + return diff --git a/tgstation.dme b/tgstation.dme index 9e5974b1c6a680..1c7f0f69fa5f8a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7435,10 +7435,12 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\lodestone_legends.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_action.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_shared.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_twisted.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_revered.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_piety.dm" #include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" From cd28753e5357534b332f694119dcf69dceecc913 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 26 Jan 2026 22:16:33 +0100 Subject: [PATCH 026/212] Optimized targeting. --- .../theologist/_theologist_root_revered.dm | 4 +- .../modular_powers/code/powers_action.dm | 48 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index 31825d8e2275bc..07fd78bc5a5441 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -15,10 +15,12 @@ button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "transformslime" target_range = 1 + target_type = /mob/living click_to_activate = TRUE /datum/action/power/theologist/theologist_root/revered/use_action(mob/living/user, mob/living/target) - to_chat(owner, span_boldnotice("Your body gently floats in the air!")) + to_chat(owner, span_boldnotice("Placeholder")) + return TRUE /datum/action/power/theologist/theologist_root/revered/on_activation(mob/living/user) diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 311e7370f0fd93..0c41fa73a413aa 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -11,11 +11,11 @@ overlay_icon_state = "bg_revenant_border" button_icon = 'icons/mob/actions/backgrounds.dmi' - /// Maximum stat before the ability is blocked. + /// Maximum state of consciousness before the ability is blocked. /// For example, `UNCONSCIOUS` prevents it from being used when in hard crit or dead, /// while `DEAD` allows the ability to be used on any stat values. var/req_stat = CONSCIOUS - /// used by a few powers that toggle + /// If your power has an active state of any type, use this. var/active = FALSE /// Does this ability stop working if you are silenced? var/disabled_by_silence = TRUE @@ -28,6 +28,8 @@ var/click_to_activate = FALSE /// Maximum targeting range (in tiles) for click_to_activate powers. Set to 0 or null for no range limit. var/target_range = 7 + /// If set, clicked target MUST be of this type (or subtype). + var/target_type = null /// The click cooldown added onto the user's next click (only for click_to_activate abilities) var/click_cd_override = CLICK_CD_CLICK_ABILITY /// If TRUE, we will unset after using our click intercept. @@ -49,7 +51,7 @@ return try_use(user, target = null) // Attempts to actively use the action -/datum/action/power/proc/try_use(mob/living/user, mob/living/target) +/datum/action/power/proc/try_use(mob/living/user, atom/target) SHOULD_CALL_PARENT(TRUE) if(!can_use(user, target)) return FALSE @@ -61,7 +63,7 @@ return FALSE // Validates the action can be used. -/datum/action/power/proc/can_use(mob/living/user, mob/living/target) +/datum/action/power/proc/can_use(mob/living/user, atom/target) SHOULD_CALL_PARENT(TRUE) if(!can_be_used_by(user)) // Runs can_be_used_by below return FALSE @@ -80,7 +82,7 @@ return TRUE // Now we do THINGS! -/datum/action/power/proc/use_action(mob/living/user, mob/living/target) +/datum/action/power/proc/use_action(mob/living/user, atom/target) return TRUE /* @@ -97,13 +99,13 @@ Handles all the logic involved in using a targeted, click-based action. var/datum/action/power/already_set = user.click_intercept if(already_set == src) // If we clicked ourself and we're already set, unset and return - return unset_click_ability(user) + return unset_click_ability(TRUE) else if(istype(already_set)) // If we have an active one set already, unset it before we set ours - already_set.unset_click_ability(user) + already_set.unset_click_ability() - return set_click_ability(user) + return set_click_ability() /// Intercepts client owner clicks to activate the ability. /// Note: this is called by the click intercept system on left click. @@ -111,12 +113,17 @@ Handles all the logic involved in using a targeted, click-based action. if(!target) return FALSE + // Checks if we are allowed to actually target that type. + if(!istype(target, target_type)) + owner.balloon_alert(owner, "Invalid target!") + return FALSE + // Range gate (only applies if target_range is non-zero). if(target_range) var/turf/clicker_turf = get_turf(clicker) var/turf/target_turf = get_turf(target) if(clicker_turf && target_turf && get_dist(clicker_turf, target_turf) > target_range) - to_chat(clicker, span_warning("Out of range!")) + owner.balloon_alert("Out of range!")) return FALSE // If the power can't be used, refuse the click and keep intercept state as-is. @@ -125,7 +132,7 @@ Handles all the logic involved in using a targeted, click-based action. // Successful click. if(unset_after_click) - unset_click_ability(clicker) + unset_click_ability() clicker.next_click = world.time + click_cd_override return TRUE @@ -133,31 +140,32 @@ Handles all the logic involved in using a targeted, click-based action. /** * Set our action as the click override on the passed mob. */ -/datum/action/power/proc/set_click_ability(mob/on_who) +/datum/action/power/proc/set_click_ability() SHOULD_CALL_PARENT(TRUE) - on_who.click_intercept = src + owner.click_intercept = src if(ranged_mousepointer) - on_who.client?.mouse_override_icon = ranged_mousepointer - on_who.update_mouse_pointer() + owner.client?.mouse_override_icon = ranged_mousepointer + owner.update_mouse_pointer() build_all_button_icons(UPDATE_BUTTON_STATUS) - on_activation(on_who) + on_activation(owner) return TRUE /** * Unset our action as the click override of the passed mob. */ -/datum/action/power/proc/unset_click_ability(mob/on_who) +/datum/action/power/proc/unset_click_ability(manual = FALSE) SHOULD_CALL_PARENT(TRUE) - on_who.click_intercept = null + owner.click_intercept = null if(ranged_mousepointer) - on_who.client?.mouse_override_icon = initial(on_who.client?.mouse_override_icon) - on_who.update_mouse_pointer() + owner.client?.mouse_override_icon = initial(owner.client?.mouse_override_icon) + owner.update_mouse_pointer() build_all_button_icons(UPDATE_BUTTON_STATUS) - on_deactivation(on_who) + if(manual) + on_deactivation(owner) return TRUE /// These only call on pointed actions From e89204824ce9c0ac164ec28cac8378cb529a4907 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 26 Jan 2026 22:39:59 +0100 Subject: [PATCH 027/212] final bit of targeting tweaking; added support for forbidden self targeting. --- .../sorcerous/theologist/_theologist_root_revered.dm | 1 + modular_doppler/modular_powers/code/powers_action.dm | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index 07fd78bc5a5441..3a66373a2dc2ac 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -17,6 +17,7 @@ target_range = 1 target_type = /mob/living click_to_activate = TRUE + target_self = FALSE /datum/action/power/theologist/theologist_root/revered/use_action(mob/living/user, mob/living/target) to_chat(owner, span_boldnotice("Placeholder")) diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 0c41fa73a413aa..a38eea81f23388 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -23,6 +23,8 @@ var/origin_power /// Can only humans use this power? var/human_only = TRUE + /// Can we target ourselves? + var/target_self = TRUE // Is it an ability that requires us to click our mouse? var/click_to_activate = FALSE @@ -115,7 +117,10 @@ Handles all the logic involved in using a targeted, click-based action. // Checks if we are allowed to actually target that type. if(!istype(target, target_type)) - owner.balloon_alert(owner, "Invalid target!") + return FALSE + + // Check if we are allowed to target ourselves. + if(!target_self && target == owner) return FALSE // Range gate (only applies if target_range is non-zero). @@ -123,7 +128,7 @@ Handles all the logic involved in using a targeted, click-based action. var/turf/clicker_turf = get_turf(clicker) var/turf/target_turf = get_turf(target) if(clicker_turf && target_turf && get_dist(clicker_turf, target_turf) > target_range) - owner.balloon_alert("Out of range!")) + owner.balloon_alert("Out of range!") return FALSE // If the power can't be used, refuse the click and keep intercept state as-is. From 89c753494b7fff6b588aa9a2e7a36e45b1b68049 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 27 Jan 2026 13:43:54 +0100 Subject: [PATCH 028/212] Changes power system to be cooldown based, redid pointing code again, made Theologist + Revered work. --- modular_doppler/modular_powers/code/_power.dm | 4 +- .../code/powers/resonant/meditate.dm | 8 +- .../powers/resonant/psyker/_psyker_action.dm | 12 +- .../powers/resonant/psyker/_psyker_root.dm | 2 +- .../code/powers/resonant/psyker/levitate.dm | 12 +- .../theologist/_theologist_action.dm | 10 +- .../sorcerous/theologist/_theologist_piety.dm | 59 +++++++-- .../theologist/_theologist_root_revered.dm | 97 ++++++++++++-- .../modular_powers/code/powers_action.dm | 120 +++++------------- 9 files changed, 197 insertions(+), 127 deletions(-) diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index 48d626d3e25689..495b0bd1c0d030 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -174,11 +174,11 @@ return // Adds activateable power buttons. -/datum/power/proc/grant_action(datum/action/power/power_path) +/datum/power/proc/grant_action(datum/action/cooldown/power/power_path) if(!ispath(power_path) || !power_holder) return FALSE - var/datum/action/power/new_action = new power_path(src) + var/datum/action/cooldown/power/new_action = new power_path(src) // TODO: Browse this and see how much of this we can move to the action subtypes. new_action.origin_power = src new_action.Grant(power_holder) diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index 1285ec946462ea..b802eaf85c6bf0 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -2,7 +2,7 @@ Reduces stress for psykers and restores Dantian for cultivators */ -/datum/action/power/resonant_meditate +/datum/action/cooldown/power/resonant_meditate name = "Resonant Meditation" desc = "Restores the full potential of your resonant powers." button_icon = 'icons/mob/actions/actions_spells.dmi' @@ -13,7 +13,7 @@ Reduces stress for psykers and restores Dantian for cultivators var/obj/item/organ/resonant/psyker/psyker_organ var/cultivator_organ //TODO: Cultivator Organ -/datum/action/power/resonant_meditate/use_action() +/datum/action/cooldown/power/resonant_meditate/use_action() . = ..() var/keep_going = TRUE var/mob/living/spotlighttarget = owner // cause we need to call it on a mob/living @@ -43,7 +43,7 @@ Reduces stress for psykers and restores Dantian for cultivators spotlighttarget.remove_status_effect(light) return -/datum/action/power/resonant_meditate/proc/get_spotlight_color() +/datum/action/cooldown/power/resonant_meditate/proc/get_spotlight_color() if(psyker_organ && cultivator_organ) return /datum/status_effect/spotlight_light/resonant else if(psyker_organ) @@ -53,7 +53,7 @@ Reduces stress for psykers and restores Dantian for cultivators else return /datum/status_effect/spotlight_light -/datum/action/power/resonant_meditate/proc/update_organs() +/datum/action/cooldown/power/resonant_meditate/proc/update_organs() psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) //TODO: Cultivator Organ 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 28161d2a789db8..5d65233ba59f09 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,4 +1,4 @@ -/datum/action/power/psyker +/datum/action/cooldown/power/psyker name = "abstract psyker power action - ahelp this" background_icon_state = "bg_hive" overlay_icon_state = "bg_hive_border" @@ -10,12 +10,12 @@ // Disabled by the trait var/disabled_by_mental_immunity = TRUE -/datum/action/power/psyker/New() +/datum/action/cooldown/power/psyker/New() . = ..() ValidateOrgan() // Actually checks if our Psyker Organ is there. We really want to check this every use. -/datum/action/power/psyker/proc/ValidateOrgan() +/datum/action/cooldown/power/psyker/proc/ValidateOrgan() if(owner) // Prevents runtiming on start psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) if(!psyker_organ) @@ -25,14 +25,14 @@ // This doesn't actually add the stress itself; it merely tells the organ to add the stress. // Why not handle it here? Because why give them an organ if we're not going to use it?! // Validation is handled on the organ side. -/datum/action/power/psyker/proc/add_stress(amount) +/datum/action/cooldown/power/psyker/proc/add_stress(amount) psyker_organ.add_stress(amount) -/datum/action/power/psyker/proc/remove_stress(amount) +/datum/action/cooldown/power/psyker/proc/remove_stress(amount) psyker_organ.remove_stress(amount) // 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/power/psyker/try_use(mob/living/user, mob/living/target) +/datum/action/cooldown/power/psyker/try_use(mob/living/user, mob/living/target) if(!ValidateOrgan()) owner.balloon_alert(owner, "No paracausal gland!") return FALSE 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 c3c98122186be9..f410ead02fcdc5 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 @@ -11,7 +11,7 @@ path = POWER_PATH_PSYKER priority = POWER_PRIORITY_ROOT - action_path = /datum/action/power/resonant_meditate // todo; deal with duplicates + action_path = /datum/action/cooldown/power/resonant_meditate // todo; deal with duplicates var/obj/item/organ/resonant/psyker/psyker_organ diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm index 14e7f458086dc0..3b376075888059 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -5,16 +5,16 @@ value = 5 priority = POWER_PRIORITY_BASIC required_powers = list(/datum/power/psyker_root) - action_path = /datum/action/power/psyker/levitate + action_path = /datum/action/cooldown/power/psyker/levitate /datum/power/psyker_power/levitate/dispel() // TODO: Ask Ephe on how to do this better. - var/datum/action/power/psyker/levitate/to_be_dispelled = action_path + var/datum/action/cooldown/power/psyker/levitate/to_be_dispelled = action_path if(to_be_dispelled.dispel()) return TRUE return FALSE -/datum/action/power/psyker/levitate +/datum/action/cooldown/power/psyker/levitate name = "Levitate" desc = "Toggles levitation, causing you to ignore the ground. Also allows for propulsion in zero-gravity. Passively drains stress while in use." button_icon = 'icons/mob/actions/actions_minor_antag.dmi' @@ -23,7 +23,7 @@ // Overlay we add to the caster var/mutable_appearance/caster_effect -/datum/action/power/psyker/levitate/use_action() +/datum/action/cooldown/power/psyker/levitate/use_action() . = ..() psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) if(!active) @@ -55,7 +55,7 @@ return -/datum/action/power/psyker/levitate/process(seconds_per_tick) +/datum/action/cooldown/power/psyker/levitate/process(seconds_per_tick) if(!owner) STOP_PROCESSING(SSfastprocess, src) return @@ -71,7 +71,7 @@ // Dispel function; basically off-switch and possibly comedic faceplant // TODO: TURN THIS INTO A LISTENER -/datum/action/power/psyker/levitate/proc/dispel() +/datum/action/cooldown/power/psyker/levitate/proc/dispel() var/mob/living/carbon/human/victim = owner if(active) owner.RemoveElement(/datum/element/forced_gravity, 0) 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 18a4c0f73fbe87..f34a55ff9bee50 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,4 +1,4 @@ -/datum/action/power/theologist +/datum/action/cooldown/power/theologist name = "abstract theologist power action - ahelp this" background_icon_state = "bg_clock" overlay_icon_state = "bg_clock_border" @@ -10,12 +10,12 @@ // The UI used for piety.alist var/atom/movable/screen/theologist_piety/theologist_ui -/datum/action/power/theologist/New() +/datum/action/cooldown/power/theologist/New() . = ..() ValidatePietyPower() // Since Theologist has both 3 roots and a persistent resource system, we use a hidden extra power for handling Piety. -/datum/action/power/theologist/proc/ValidatePietyPower() +/datum/action/cooldown/power/theologist/proc/ValidatePietyPower() if(owner) // Prevents runtiming on start var/mob/living/carrier = owner piety_power = carrier.get_power(/datum/power/theologist_piety) @@ -24,11 +24,11 @@ return TRUE // Validation handled in the piety power. -/datum/action/power/theologist/proc/adjust_piety(amount, override_cap) +/datum/action/cooldown/power/theologist/proc/adjust_piety(amount, override_cap) piety_power.adjust_piety(amount, override_cap) // We check to see if our piety power is actually there, because usually things will go bad if they don't. -/datum/action/power/theologist/try_use(mob/living/user, mob/living/target) +/datum/action/cooldown/power/theologist/try_use(mob/living/user, mob/living/target) if(!ValidatePietyPower()) owner.balloon_alert(owner, "Yell at the coders; you're missing your piety system!") return FALSE 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 71855f8723a14c..05cf41a420e587 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 @@ -21,22 +21,61 @@ // The UI itself var/atom/movable/screen/theologist_piety/theologist_ui -/datum/power/theologist_piety/New() +/datum/power/theologist_piety/post_add() . = ..() - //Grants the UI element for Piety - if(power_holder) // Prevents runtiming at init - if(power_holder.hud_used) - var/datum/hud/hud_used = power_holder.hud_used - theologist_ui = new /atom/movable/screen/theologist_piety(null, hud_used) - hud_used.infodisplay += theologist_ui + if(!power_holder) + return + + var/mob/living/living_holder = power_holder + if(living_holder.hud_used) + install_piety_hud(living_holder) + else + RegisterSignal(living_holder, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created)) + +/datum/power/theologist_piety/remove() + . = ..() + + if(!power_holder) + return + + var/mob/living/living_holder = power_holder + UnregisterSignal(living_holder, COMSIG_MOB_HUD_CREATED) + + if(living_holder.hud_used && theologist_ui) + living_holder.hud_used.infodisplay -= theologist_ui + qdel(theologist_ui) + theologist_ui = null + +/datum/power/theologist_piety/proc/on_hud_created(datum/source) + SIGNAL_HANDLER + + var/mob/living/living_holder = power_holder + if(!living_holder || !living_holder.hud_used) + return + + install_piety_hud(living_holder) + +/datum/power/theologist_piety/proc/install_piety_hud(mob/living/living_holder) + if(theologist_ui) // already installed + return + + var/datum/hud/hud_used = living_holder.hud_used + theologist_ui = new /atom/movable/screen/theologist_piety(null, hud_used) + hud_used.infodisplay += theologist_ui + + // Set initial text so it isn't blank until first adjust. + theologist_ui.maptext = FORMAT_PIETY_TEXT(piety) + + // THIS is the missing “why it only appears after changeling” + hud_used.show_hud(hud_used.hud_version) // UI Elements for Piety /atom/movable/screen/theologist_piety name = "piety" - icon = 'icons/hud/blob.dmi' - icon_state = "corehealth" - screen_loc = "WEST,CENTER-1:15" // TODO: Define & Move this. + icon = 'icons/hud/blob.dmi' // TODO: Get sprites/UI for this. + icon_state = "block" + screen_loc = "WEST,CENTER-2:15" // TODO: Define & Move this. /datum/power/theologist_piety/proc/adjust_piety(amount, override_cap) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index 3a66373a2dc2ac..b7696afcf59891 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -4,30 +4,111 @@ desc = "Nullifies pain and slowly heals the creature over a prolonged period of time. \ Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again. \ This is mutually exclusive with the other 'A Burden...' powers." - action_path = /datum/action/power/theologist/theologist_root/revered + action_path = /datum/action/cooldown/power/theologist/theologist_root/revered value = 5 -/datum/action/power/theologist/theologist_root/revered +/datum/action/cooldown/power/theologist/theologist_root/revered name = "A Burden Revered" desc = "Nullifies pain and slowly heals the creature over a prolonged period of time. \ Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again." button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "transformslime" + cooldown_time = 50 target_range = 1 target_type = /mob/living click_to_activate = TRUE - target_self = FALSE + target_self = TRUE -/datum/action/power/theologist/theologist_root/revered/use_action(mob/living/user, mob/living/target) - to_chat(owner, span_boldnotice("Placeholder")) + // Current instance of the status effect + var/datum/status_effect/power/burden_revered/active_effect +/datum/action/cooldown/power/theologist/theologist_root/revered/use_action(mob/living/user, mob/living/target) + to_chat(owner, span_boldnotice("Placeholder")) + if(active_effect) + qdel(active_effect) + active_effect = target.apply_status_effect(/datum/status_effect/power/burden_revered, src) return TRUE -/datum/action/power/theologist/theologist_root/revered/on_activation(mob/living/user) +/datum/action/cooldown/power/theologist/theologist_root/revered/set_click_ability(mob/on_who) + . = ..() to_chat(owner, span_notice("You ready yourself to relieve the burden of others!
Left-click a creature next to you to target them!")) -/datum/action/power/theologist/theologist_root/revered/on_deactivation(mob/living/user) - to_chat(owner, span_notice("You withdraw your power.")) +/datum/action/cooldown/power/theologist/theologist_root/revered/proc/effect_expired(amount) + adjust_piety(amount) + if(amount >= 1) + to_chat(owner, span_notice("Your Burden Revered has expired, you gained [amount] piety!")) + owner.playsound_local(owner, 'sound/effects/pray.ogg', 50, FALSE) + else + to_chat(owner, span_notice("Your Burden Revered has expired!")) + return + ///datum/power/theologist_root/revered/process() + +// Status effect that Burden Revered applies +/datum/status_effect/power/burden_revered + id = "burden_revered" + duration = 2 MINUTES // If somehow it overestays its welcome + tick_interval = 1 SECONDS + alert_type = /atom/movable/screen/alert/status_effect/burden_revered + // The power responsible for this, so we can make sure it properly gives piety to the caster + var/datum/action/cooldown/power/theologist/theologist_root/revered/burden_power + // The maximum amount we will heal + var/healing_max = 30 + // How much we have healed already + var/healing_done = 0 + // How much we heal per tick. + var/base_healing_amount = 1 + // Has the thing already expired? + var/already_expired + +/datum/status_effect/power/burden_revered/on_apply() + ADD_TRAIT(owner, TRAIT_ANALGESIA, type) + return TRUE + +// Sets the link with the original action +/datum/status_effect/power/burden_revered/on_creation(mob/living/new_owner, datum/action/cooldown/power/theologist/theologist_root/revered/passed_power) + . = ..() + burden_power = passed_power + + +// You might wonder why we run Destroy as well as on_remove. The issue is that on_remove can trigger on qdel, which invalidates burden_power, which prevents us from efficiently passing on the piety back to the owner. +/datum/status_effect/power/burden_revered/Destroy() + if(!already_expired) + expire() + ..() + +/datum/status_effect/power/burden_revered/on_remove() + REMOVE_TRAIT(owner, TRAIT_ANALGESIA, type) + return + +// This is where the heal budgeting happens. +/datum/status_effect/power/burden_revered/tick(seconds_between_ticks) + var/healing_amount = (base_healing_amount * seconds_between_ticks) + new /obj/effect/temp_visual/heal(get_turf(owner), "#ddd166") + owner.heal_overall_damage(healing_amount) + healing_done += healing_amount + // Expire if at full health. + if(owner && owner.health >= owner.maxHealth) + expire() + return + // Expire if we've reached the max. + if(healing_done >= healing_max) + expire() + return + +// QDEL destroys burden_power +/datum/status_effect/power/burden_revered/proc/expire() + var/piety_gained = max(0, floor(healing_done * 0.2)) // TODO: defines + // Report back BEFORE deletion starts + if(burden_power) + burden_power.effect_expired(piety_gained) + already_expired = TRUE + src.Destroy() // There might be something better, but QDEL triggers the qdel loop warning. + +/atom/movable/screen/alert/status_effect/burden_revered + name = "A Burden Revered" + desc = "You passively heal damage, and are immune to pain for it's duration." + icon_state = "designated_target" // Placeholder + diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index a38eea81f23388..661426c64a0b33 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -5,11 +5,13 @@ Largely modeled after changeling_power.dm */ -/datum/action/power +/datum/action/cooldown/power name = "abstract power action - ahelp this" background_icon_state = "bg_revenant" overlay_icon_state = "bg_revenant_border" button_icon = 'icons/mob/actions/backgrounds.dmi' + active_overlay_icon_state = "bg_spell_border_active_red" + ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.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,45 +21,25 @@ var/active = FALSE /// Does this ability stop working if you are silenced? var/disabled_by_silence = TRUE - /// What power gave the origin? + /// What power is the origin? var/origin_power /// Can only humans use this power? var/human_only = TRUE /// Can we target ourselves? var/target_self = TRUE - // Is it an ability that requires us to click our mouse? - var/click_to_activate = FALSE /// Maximum targeting range (in tiles) for click_to_activate powers. Set to 0 or null for no range limit. var/target_range = 7 /// If set, clicked target MUST be of this type (or subtype). var/target_type = null - /// The click cooldown added onto the user's next click (only for click_to_activate abilities) - var/click_cd_override = CLICK_CD_CLICK_ABILITY - /// If TRUE, we will unset after using our click intercept. - var/unset_after_click = TRUE - /// What icon to replace our mouse cursor with when active. - var/ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' // When you press the button -/datum/action/power/Trigger(mob/clicker, trigger_flags, atom/target) - var/mob/living/user = owner - if(!user) - return - - // Click-to-activate powers set themselves as a click intercept, and then wait for a left click target. - if(click_to_activate) - return handle_click_to_activate(user, target) - - // Non-targeted powers just use immediately. - return try_use(user, target = null) - // Attempts to actively use the action -/datum/action/power/proc/try_use(mob/living/user, atom/target) +/datum/action/cooldown/power/proc/try_use(mob/living/user, atom/target) SHOULD_CALL_PARENT(TRUE) if(!can_use(user, target)) return FALSE - if(disabled_by_silence && HAS_TRAIT(owner, TRAIT_RESONANCE_SILENCED)) + if(disabled_by_silence && HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED)) owner.balloon_alert(user, "silenced!") return FALSE if(use_action(user, target)) @@ -65,7 +47,7 @@ return FALSE // Validates the action can be used. -/datum/action/power/proc/can_use(mob/living/user, atom/target) +/datum/action/cooldown/power/proc/can_use(mob/living/user, atom/target) SHOULD_CALL_PARENT(TRUE) if(!can_be_used_by(user)) // Runs can_be_used_by below return FALSE @@ -75,16 +57,16 @@ return TRUE // Checks if we exist (wow) and are human. -/datum/action/power/proc/can_be_used_by(mob/living/user) +/datum/action/cooldown/power/proc/can_be_used_by(mob/living/user) SHOULD_CALL_PARENT(TRUE) if(QDELETED(user)) return FALSE - if(!ishuman(owner) && human_only) + if(!ishuman(user) && human_only) return FALSE return TRUE // Now we do THINGS! -/datum/action/power/proc/use_action(mob/living/user, atom/target) +/datum/action/cooldown/power/proc/use_action(mob/living/user, atom/target) return TRUE /* @@ -93,34 +75,39 @@ Handles all the logic involved in using a targeted, click-based action. - Second press (while already active): disables click intercept - While active: a left click calls InterceptClickOn() and passes the clicked atom as target */ -/datum/action/power/proc/handle_click_to_activate(mob/living/user, atom/target) - // If this was called with a direct target (ex: some automated caller), treat it like a click immediately. - if(target) - return InterceptClickOn(user, null, target) - var/datum/action/power/already_set = user.click_intercept - if(already_set == src) - // If we clicked ourself and we're already set, unset and return - return unset_click_ability(TRUE) +/** + * Non-click_to_activate actions run through the cooldown framework: + * Trigger() -> PreActivate(owner) -> Activate(owner) + */ +/datum/action/cooldown/power/Activate(atom/target) + var/mob/living/user = owner + if(!user) + return FALSE - else if(istype(already_set)) - // If we have an active one set already, unset it before we set ours - already_set.unset_click_ability() + // Non-targeted powers just use immediately. + if(!try_use(user, target = null)) + return FALSE - return set_click_ability() + StartCooldown() + return TRUE /// Intercepts client owner clicks to activate the ability. -/// Note: this is called by the click intercept system on left click. -/datum/action/power/proc/InterceptClickOn(mob/living/clicker, params, atom/target) +/// Called by the base click intercept system on left click. +/// Whilst /datum/action/cooldown does have click support, it doesn't support range-detecting and target filtering, so we are overriding that with our own. +/datum/action/cooldown/power/InterceptClickOn(mob/living/clicker, params, atom/target) + if(!IsAvailable(feedback = TRUE)) + return FALSE if(!target) return FALSE // Checks if we are allowed to actually target that type. - if(!istype(target, target_type)) + if(target_type && !istype(target, target_type)) return FALSE // Check if we are allowed to target ourselves. - if(!target_self && target == owner) + if(!target_self && target == clicker) + owner.balloon_alert(clicker, "Can't target self!") return FALSE // Range gate (only applies if target_range is non-zero). @@ -128,55 +115,18 @@ Handles all the logic involved in using a targeted, click-based action. var/turf/clicker_turf = get_turf(clicker) var/turf/target_turf = get_turf(target) if(clicker_turf && target_turf && get_dist(clicker_turf, target_turf) > target_range) - owner.balloon_alert("Out of range!") + owner.balloon_alert(clicker, "Out of range!") return FALSE // If the power can't be used, refuse the click and keep intercept state as-is. if(!try_use(clicker, target)) return FALSE + StartCooldown() + // Successful click. if(unset_after_click) - unset_click_ability() + unset_click_ability(clicker, refund_cooldown = FALSE) clicker.next_click = world.time + click_cd_override return TRUE - -/** - * Set our action as the click override on the passed mob. - */ -/datum/action/power/proc/set_click_ability() - SHOULD_CALL_PARENT(TRUE) - - owner.click_intercept = src - if(ranged_mousepointer) - owner.client?.mouse_override_icon = ranged_mousepointer - owner.update_mouse_pointer() - - build_all_button_icons(UPDATE_BUTTON_STATUS) - on_activation(owner) - return TRUE - -/** - * Unset our action as the click override of the passed mob. - */ -/datum/action/power/proc/unset_click_ability(manual = FALSE) - SHOULD_CALL_PARENT(TRUE) - - owner.click_intercept = null - if(ranged_mousepointer) - owner.client?.mouse_override_icon = initial(owner.client?.mouse_override_icon) - owner.update_mouse_pointer() - - build_all_button_icons(UPDATE_BUTTON_STATUS) - if(manual) - on_deactivation(owner) - return TRUE - -/// These only call on pointed actions -/datum/action/power/proc/on_activation(mob/living/user) - return - -/// Same as above -/datum/action/power/proc/on_deactivation(mob/living/user) - return From 1092a42ac2c6952d15f96bbcef48d8befafc4832 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 27 Jan 2026 17:50:06 +0100 Subject: [PATCH 029/212] shared & revered burdens finished. Added stun check for powers. --- .../theologist/_theologist_root_revered.dm | 46 ++- .../theologist/_theologist_root_shared.dm | 314 +++++++++++++++++- .../modular_powers/code/powers_action.dm | 5 + 3 files changed, 355 insertions(+), 10 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index b7696afcf59891..1759c9ec98c37e 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -18,7 +18,7 @@ target_range = 1 target_type = /mob/living click_to_activate = TRUE - target_self = TRUE + target_self = FALSE // Current instance of the status effect var/datum/status_effect/power/burden_revered/active_effect @@ -37,7 +37,7 @@ /datum/action/cooldown/power/theologist/theologist_root/revered/proc/effect_expired(amount) adjust_piety(amount) if(amount >= 1) - to_chat(owner, span_notice("Your Burden Revered has expired, you gained [amount] piety!")) + to_chat(owner, span_notice("Your Burden Revered has expired! You gained [amount] piety!")) owner.playsound_local(owner, 'sound/effects/pray.ogg', 50, FALSE) else to_chat(owner, span_notice("Your Burden Revered has expired!")) @@ -87,8 +87,7 @@ /datum/status_effect/power/burden_revered/tick(seconds_between_ticks) var/healing_amount = (base_healing_amount * seconds_between_ticks) new /obj/effect/temp_visual/heal(get_turf(owner), "#ddd166") - owner.heal_overall_damage(healing_amount) - healing_done += healing_amount + // Expire if at full health. if(owner && owner.health >= owner.maxHealth) expire() @@ -98,6 +97,45 @@ expire() return + // Only include damage types that actually need healing + var/list/damage_choices = list() + var/brute_damage = owner.getBruteLoss() + var/burn_damage = owner.getFireLoss() + var/tox_damage = owner.getToxLoss() + var/oxy_damage = owner.getOxyLoss() + + if(brute_damage > 0) damage_choices += "brute" + if(burn_damage > 0) damage_choices += "burn" + if(tox_damage > 0) damage_choices += "tox" + if(oxy_damage > 0) damage_choices += "oxy" + + // Nothing to heal + if(!damage_choices.len) + return + + var/damage_choice = pick(damage_choices) + + switch(damage_choice) + if("brute") + var/heal_done = min(healing_amount, brute_damage) + owner.adjustBruteLoss(-heal_done) + healing_done += heal_done + + if("burn") + var/heal_done = min(healing_amount, burn_damage) + owner.adjustFireLoss(-heal_done) + healing_done += heal_done + + if("tox") + var/heal_done = min(healing_amount, tox_damage) + owner.adjustToxLoss(-heal_done) + healing_done += heal_done + + if("oxy") + var/heal_done = min(healing_amount, oxy_damage) + owner.adjustOxyLoss(-heal_done) + healing_done += heal_done + // QDEL destroys burden_power /datum/status_effect/power/burden_revered/proc/expire() var/piety_gained = max(0, floor(healing_done * 0.2)) // TODO: defines diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index 455fcf1db6c212..801dd0e0e53ff2 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -1,11 +1,313 @@ /datum/power/theologist_root/shared name = "A Burden Shared" - desc = "tbd \ - tbd \ + desc = "Channels a beam of energy between you and a target, equalizing damage over a period of time, scaling with severity. \ + The beam requires continous line of sight to function, and neither you or your target can be incapacitated. Generates Piety if you are transfering damage to yourself. \ This is mutually exclusive with the other 'A Burden...' powers." + action_path = /datum/action/cooldown/power/theologist/theologist_root/shared value = 5 - mob_trait = TRAIT_ARCHETYPE_SORCEROUS - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THEOLOGIST - priority = POWER_PRIORITY_ROOT + +/datum/action/cooldown/power/theologist/theologist_root/shared + name = "A Burden Shared" + desc = "Channels a beam of energy between you and a target, equalizing damage over a period of time, scaling with severity. \ + The beam requires continous line of sight to function, and neither you or your target can be incapacitated. Generates Piety if you are transfering damage to yourself." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "swap" + cooldown_time = 150 + click_to_activate = TRUE + + // Targeting rules: + // - Click a living target to start/retarget the beam between you and them. + // - While active, pressing the action again (manual deactivation) should end the beam. + target_range = 10 // 2 space beyond screen width if purely vertical/horizontal + target_type = /mob/living + target_self = FALSE + unset_after_click = TRUE + + // The piety build-up. Gets exchanged at exchange_build() if its either positive or negative. + var/piety_buildup + + /// Who we're currently linked to. + var/mob/living/carbon/current_target + + /// Visual beam datum we keep alive while the link is active. + var/datum/beam/current_beam + + // Visual for the glow on the target + var/mutable_appearance/target_glow + + /// How often (in deciseconds) we validate LoS + apply the equalization tick. + var/check_delay = 10 + var/last_check = 0 + + // Current instance of the status effect + var/datum/status_effect/power/burden_revered/active_effect + + +/datum/action/cooldown/power/theologist/theologist_root/shared/Destroy() + clear_link(manual = TRUE) + return ..() + +// We override trigger to be able to cancel the ability on clicking the button +/datum/action/cooldown/power/theologist/theologist_root/shared/Trigger(mob/clicker, trigger_flags, atom/target) + // If we're already actively beaming, pressing the button again should cancel immediately. + if(current_target) + clear_link(manual = TRUE) + // Also ensure click-intercept is not left enabled. + unset_click_ability(owner, refund_cooldown = FALSE) + return FALSE + + . = ..() + +// Currency exchange for piety. +/datum/action/cooldown/power/theologist/theologist_root/shared/proc/exchange_buildup() + // Have we been a good boy? + if(piety_buildup >= 5) + piety_buildup -= 5 + adjust_piety(1) + to_chat(owner, span_notice("Taking on the burdens of others has gained you piety!")) + // Have we been a bad boy? + else if (piety_buildup <= -5) + piety_buildup += 5 + // Have we been a VERY bad boy? Don't think you can get away with willynilly using this at 0 piety. + if(piety_power.piety <= 0 && prob(25)) + lightningbolt(owner) + if(ishuman(owner)) + var/mob/living/carbon/human/sinner = owner + sinner.Paralyze(100) + to_chat(owner, span_userdanger("Divine forces have punished you for your lack of piety!"), confidential = TRUE) + clear_link() + return + adjust_piety(-1) + to_chat(owner, span_warning("The transfer of your burdens onto others lost you piety!")) + + +/** + * Always-called cleanup. Use manual = TRUE when the user actively cancels the power. + */ +/datum/action/cooldown/power/theologist/theologist_root/shared/proc/clear_link(manual = FALSE) + // gets rid of the beam + if(current_beam) + UnregisterSignal(current_beam, COMSIG_QDELETING) + QDEL_NULL(current_beam) + // gets rid of the target's glow + if(target_glow) + current_target.cut_overlay(target_glow) + target_glow = null + // unflags active and tells the caster that the link :b:roke + if(active) + active = FALSE + if(!manual && owner && isliving(owner)) + owner.balloon_alert(owner, "link broken!") + // gets rid of the warning status message + if(active_effect) + qdel(active_effect) + + current_target = null + if(manual) + unset_click_ability(owner, refund_cooldown = FALSE) + +/** + * Called when the beam is deleted by something external (range/los/cleanup, etc). + */ +/datum/action/cooldown/power/theologist/theologist_root/shared/proc/beam_died() + SIGNAL_HANDLER + current_beam = null + active = FALSE // avoid re-qdel + clear_link() + +/** + * Starts (or re-targets) the link between the user and a clicked target. + * Returning TRUE means: the power was used successfully and should start cooldown (and unset targeting mode). + */ +/datum/action/cooldown/power/theologist/theologist_root/shared/use_action(mob/living/carbon/user, atom/target) + var/mob/living/new_target = target + + // If already active, cleanly drop the existing link before re-targeting. + if(active) + clear_link(manual = TRUE) + + current_target = new_target + last_check = 0 + + // Create a beam from user -> target. This mirrors medbeam.dm's Beam() lifecycle. + current_beam = user.Beam(current_target, icon_state = "light_beam", time = 10 MINUTES, maxdistance = target_range, beam_type = /obj/effect/ebeam/medical, beam_color = "#ddd166") + RegisterSignal(current_beam, COMSIG_QDELETING, PROC_REF(beam_died)) + + target_glow = mutable_appearance( + icon = 'icons/mob/effects/genetics.dmi', + icon_state = "servitude", + layer = current_target.layer - 0.1, + appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART + ) + current_target.add_overlay(target_glow) + + active = TRUE + active_effect = current_target.apply_status_effect(/datum/status_effect/power/burden_shared) + + return TRUE + +/datum/action/cooldown/power/theologist/theologist_root/shared/process() + // So we're kind-of parroting the original, but we don't want to stop proccessing so no . = ..() + build_all_button_icons(UPDATE_BUTTON_STATUS) + if(!active) + if(!owner || (next_use_time - world.time) <= 0) + STOP_PROCESSING(SSfastprocess, src) + return + + // If the owner vanishes or we no longer have a target, end it. + if(active) + // checks if we actually hve an owner or target + if(!owner || !isliving(owner) || !current_target) + clear_link() + return + // Checks if our owner or target are DEAD + if(current_target.stat == DEAD || owner.stat == DEAD) + clear_link() + return + + // checks if our owner or target got SNAPPED + if(QDELETED(owner) || QDELETED(current_target)) + clear_link() + return + + // checks if our owner or target are INCAPACITATED + if(HAS_TRAIT(owner, TRAIT_INCAPACITATED) || HAS_TRAIT(current_target, TRAIT_INCAPACITATED)) + clear_link() + return + + if(world.time <= last_check + check_delay) + return + last_check = world.time + + // LoS gate. If it fails, deleting the beam triggers beam_died() -> clear_link(). + if(!los_check(get_atom_on_turf(owner), current_target)) + QDEL_NULL(current_beam) + return + + on_beam_tick(owner, current_target) + exchange_buildup() + + + // Maths out who needs to receive the healing and who needs to receive the damage. +/datum/action/cooldown/power/theologist/theologist_root/shared/proc/on_beam_tick(mob/living/carbon/user, mob/living/target) + // Non carbons get their own equalization. + if(!iscarbon(target)) + equalize_simple(user, target) + return + + var/list/user_damage = get_damage_snapshot(user) + var/list/target_damage = get_damage_snapshot(target) + + for(var/damage_type in user_damage) + var/user_amount = user_damage[damage_type] + var/target_amount = target_damage[damage_type] + if(target_amount > user_amount) + equalize(target, user, damage_type) + if(target_amount < user_amount) + equalize(user, target, damage_type) + else + continue + return + +// Gets the damage of the affected creature. +/datum/action/cooldown/power/theologist/theologist_root/shared/proc/get_damage_snapshot(mob/living/carbon/subject) + return list( + "brute" = subject.getBruteLoss(), + "burn" = subject.getFireLoss(), + "tox" = subject.getToxLoss(), + "oxy" = subject.getOxyLoss(), + ) + +//Actually calls the proper health adjustments +/datum/action/cooldown/power/theologist/theologist_root/shared/proc/equalize(mob/living/carbon/giver, mob/living/carbon/taker, damage_type as text) +// Given we have already determined who has more and who has less in on_beam_tick, we can always assume that giver has more than taker, and thus make the comparison sum using that. + var/amount + // To summarize; heals the target by the amount (which is capped at 5) + switch(damage_type) + if("brute") + amount = clamp((giver.getBruteLoss() - taker.getBruteLoss()) / 20, 0.5, 3) + giver.adjustBruteLoss(-amount) + taker.adjustBruteLoss(amount) + + if("burn") + amount = clamp((giver.getFireLoss() - taker.getFireLoss()) / 20, 0.5 , 3) + giver.adjustFireLoss(-amount) + taker.adjustFireLoss(amount) + + if("tox") + amount = clamp((giver.getToxLoss() - taker.getToxLoss()) / 20, 0.5 , 3) + giver.adjustToxLoss(-amount) + taker.adjustToxLoss(amount) + + if("oxy") + amount = clamp((giver.getOxyLoss() - taker.getOxyLoss()) / 20, 0.5 , 3) + giver.adjustOxyLoss(-amount) + taker.adjustOxyLoss(amount) + + // Piety buildup increases/deductions + if(taker == owner) + piety_buildup += amount + else if(giver == owner) + piety_buildup -= amount + + return + +// Special version for when targeting non-carbon living creatures (usually simple_creatures) +/datum/action/cooldown/power/theologist/theologist_root/shared/proc/equalize_simple(mob/living/carbon/user, mob/living/target) + // Since we are comparing living vs carbon, we are doing health on our target and brute on our guy. + var/user_missingHP = user.maxHealth - user.health + var/target_missingHP = target.maxHealth - target.health + + // Boooo, hurting the animals! + // Way less effective on simple mobs + // TODO: Piety loss for animal murder? + + /* + This section is really ugly. Due for a do-over. + */ + if(user_missingHP > target_missingHP) + var/bruteloss = clamp((user.getBruteLoss() - target.bruteloss) / 20, 0.2, 1.5) + var/fireloss = clamp((user.getFireLoss() - target.fireloss) / 20, 0.2, 1.5) + var/toxloss = clamp((user.getToxLoss() - target.toxloss) / 20, 0.2, 1.5) + var/oxyloss = clamp((user.getOxyLoss() - target.oxyloss) / 20, 0.2, 1.5) + user.adjustBruteLoss(-bruteloss) + user.adjustFireLoss(-fireloss) + user.adjustToxLoss(-toxloss) + user.adjustOxyLoss(-oxyloss) + target.bruteloss -= bruteloss + target.fireloss -= fireloss + target.toxloss -= toxloss + target.oxyloss -= oxyloss + + return + + // Yaaay, healing the animals :) + if(user_missingHP < target_missingHP) + var/bruteloss = clamp((target.bruteloss - user.getBruteLoss()) / 20, 0.2, 1.5) + var/fireloss = clamp((target.fireloss - user.getFireLoss()) / 20, 0.2, 1.5) + var/toxloss = clamp((target.toxloss - user.getToxLoss()) / 20, 0.2, 1.5) + var/oxyloss = clamp((target.oxyloss - user.getOxyLoss()) / 20, 0.2, 1.5) + user.adjustBruteLoss(bruteloss) + user.adjustFireLoss(fireloss) + user.adjustToxLoss(toxloss) + user.adjustOxyLoss(oxyloss) + target.bruteloss += bruteloss + target.fireloss += fireloss + target.toxloss += toxloss + target.oxyloss += oxyloss + else + return + +// You know, if I was a smarter man I'd have made the status effect actually handle effects. +// Largely here for alerts so people know they are being damage transfered. +/datum/status_effect/power/burden_shared + id = "burden_shared" + duration = 5 MINUTES // If somehow it overestays its welcome + tick_interval = -1 SECONDS // This one's just cosmetic + alert_type = /atom/movable/screen/alert/status_effect/burden_shared + +/atom/movable/screen/alert/status_effect/burden_shared + name = "A Burden Shared" + desc = "Damage is being equalized between you and the caster!" + icon_state = "lightningorb" // Placeholder diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 661426c64a0b33..c08036ec87ff29 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -21,6 +21,8 @@ var/active = FALSE /// Does this ability stop working if you are silenced? var/disabled_by_silence = TRUE + /// Does this ability stop working if you are incapacitated? + var/disabled_by_incapacitate = TRUE /// What power is the origin? var/origin_power /// Can only humans use this power? @@ -39,6 +41,9 @@ SHOULD_CALL_PARENT(TRUE) if(!can_use(user, target)) return FALSE + if(disabled_by_incapacitate && HAS_TRAIT(user, TRAIT_INCAPACITATED)) + owner.balloon_alert(user, "incapacitated!") + return FALSE if(disabled_by_silence && HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED)) owner.balloon_alert(user, "silenced!") return FALSE From e05218a44af0cd84fa2861b45b5fabc469b8e4ed Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 27 Jan 2026 22:02:07 +0100 Subject: [PATCH 030/212] Added and finalzied all 3 types of theologist roots, readds telekinesis, added fun to levitate. --- .../powers/resonant/psyker/_psyker_action.dm | 9 +- .../powers/resonant/psyker/_psyker_organ.dm | 22 +++ .../code/powers/resonant/psyker/levitate.dm | 9 +- .../powers/resonant/psyker/telekinesis.dm | 73 ++++------ .../sorcerous/theologist/_theologist_piety.dm | 2 +- .../theologist/_theologist_root_revered.dm | 6 +- .../theologist/_theologist_root_shared.dm | 9 +- .../theologist/_theologist_root_twisted.dm | 136 +++++++++++++++++- 8 files changed, 206 insertions(+), 60 deletions(-) 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 5d65233ba59f09..d1552f08b97737 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 @@ -36,10 +36,11 @@ if(!ValidateOrgan()) owner.balloon_alert(owner, "No paracausal gland!") return FALSE - if(disabled_by_mental_immunity && target.can_block_magic(MAGIC_RESISTANCE_MIND)) - add_stress(PSYKER_STRESS_MINOR) // This actively stresses you out. - owner.balloon_alert(owner, "Something interveres with your powers!") - return FALSE + if(target) + if(disabled_by_mental_immunity && target.can_block_magic(MAGIC_RESISTANCE_MIND)) + add_stress(PSYKER_STRESS_MINOR) // This actively stresses you out. + owner.balloon_alert(owner, "Something interveres with your powers!") + return FALSE . = .. () diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index 7c4378115df27a..92d9b82e5a611f 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -21,6 +21,9 @@ var/CDstressMild = 0 var/CDstressSevere = 0 + //The stress warning message + var/datum/status_effect/power/stress_warning + // Don't modify stress directly. In the future affinity has a bearing on how much stress you gain. /obj/item/organ/resonant/psyker/proc/add_stress(amount) // TODO; Add clothing affinity. Wearing psychic nicknacks makes you gain less stress. @@ -71,6 +74,13 @@ if(CDstressSevere > 0) CDstressSevere = max(CDstressSevere - seconds_per_tick, 0) + //Handle the warning status effect + if(stress >= stress_threshold && !stress_warning) + stress_warning = owner.apply_status_effect(/datum/status_effect/power/stress_warning) + else if(stress < stress_threshold && stress_warning) + owner.remove_status_effect(/datum/status_effect/power/stress_warning) + stress_warning = null + // In the event that you implant this into someone else. // Currently placeholder til we settle on what it do on people that don't have it. // TODO: Appear on med scanners. @@ -136,3 +146,15 @@ return + +// Warning message for high stress +/datum/status_effect/power/stress_warning + id = "stress_warning" + tick_interval = -1 SECONDS // This one's just a warning + alert_type = /atom/movable/screen/alert/status_effect/stress_warning + +/atom/movable/screen/alert/status_effect/stress_warning + icon = 'icons/mob/actions/actions_ecult.dmi' + name = "Stress Warning!" + desc = "Your stress is at the backlash threshold! You will suffer periodic negative events until you meditate, and continued use of your powers will only make things worse!" + icon_state = "mansus_link" // Placeholder diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm index 3b376075888059..c3745892e6402b 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -59,7 +59,9 @@ if(!owner) STOP_PROCESSING(SSfastprocess, src) return - + //Faceplant if you get KO'd + if(HAS_TRAIT(owner, TRAIT_INCAPACITATED)) + dispel() // Passive stress cost if(active) var/mob/living/carbon/human/psyker = owner @@ -87,10 +89,11 @@ // Do you have anything to brace your fall? Or do you possibly manage to get lucky? var/obj/item/organ/wings/gliders = owner.get_organ_by_type(/obj/item/organ/wings) if(HAS_TRAIT(owner, TRAIT_FREERUNNING) || gliders?.can_soften_fall() || prob(30)) - to_chat(owner, span_warning("You suddenly fall to the ground, but manage to catch yourself!")) + to_chat(owner, span_warning("You drop to the ground, but manage to catch yourself!")) else - to_chat(owner, span_userdanger("You suddenly fall to the ground!")) + to_chat(owner, span_userdanger("You drop to the ground!")) playsound(owner, 'sound/effects/desecration/desecration-02.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + victim.adjustBruteLoss(5) victim.Knockdown(3 SECONDS) return TRUE 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 6f89e021daf5c5..f880f44c4512d3 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -1,8 +1,8 @@ /* This leviathan of spaghetti is based off of the MODsuit modules. -It is currently still using spells as a system rather than powers. +It is a lazy port to the current acitons powers system from the spells system and has a lot wonkiness as a consequence. BIG TODO: FIX THAT */ -/* + /datum/power/psyker_power/telekinesis name = "Telekinesis" desc = "Grants the ability to manipulate and move various objects. Generates stress based upon weight on pick-up and throw, as well as passively while holding an object." @@ -10,20 +10,18 @@ BIG TODO: FIX THAT value = 5 priority = POWER_PRIORITY_BASIC required_powers = list(/datum/power/psyker_root) - action_path = /datum/action/cooldown/spell/pointed/telekinesis + action_path = /datum/action/cooldown/power/psyker/telekinesis -/datum/action/cooldown/spell/pointed/telekinesis +/datum/action/cooldown/power/psyker/telekinesis name = "Telekinesis" desc = "Middle-click to grab an object, Right-Click to drop, Middle-Click again to punt!" button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "repulse" + click_to_activate = TRUE + target_self = FALSE - deactive_msg = "You unfocus your telekinetic powers." unset_after_click = FALSE - cast_range = 255 // this is just for show. - aim_assist = FALSE - spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC - antimagic_flags = MAGIC_RESISTANCE_MIND + target_range = 255 // this is just for show. // Range of the kinesis grab. var/grab_range = 8 @@ -39,32 +37,23 @@ BIG TODO: FIX THAT // Overlay we add to the player when using this power. var/mutable_appearance/player_icon - // Psyker organ for stress. - var/obj/item/organ/resonant/psyker/psyker_organ - // Mouse tracker overlay (telekinesis-specific) var/atom/movable/screen/fullscreen/cursor_catcher/kinesis/psyker_tk/kinesis_catcher -/datum/action/cooldown/spell/pointed/telekinesis/on_activation(mob/on_who) - // I am to commit a most heinous crime. - // If I do not call parent, we'll get compile warnings. If I don't do this, there'll be misleading messages that we cannot suppress (we don't use left click because it mimmicks the MODsuit controls) - // Maintainers forgive my sins. - var/mob/real_on_who = on_who - on_who = null - // Sins end. +/datum/action/cooldown/power/psyker/telekinesis/Trigger(mob/clicker, trigger_flags, atom/target) . = ..() - if(!.) - return - psyker_organ = real_on_who.get_organ_slot(ORGAN_SLOT_PSYKER) - to_chat(real_on_who, span_notice("You focus your telekinetic powers...
Middle-click: Grab/Punt | Right-click: Drop | Move mouse: to drag")) - return TRUE + // We run this here cause telekinesis doesn't use use_action for some awful reason and I cba to fix it. + ValidateOrgan() -/datum/action/cooldown/spell/pointed/telekinesis/on_deactivation(mob/on_who, refund_cooldown = TRUE) - clear_grab(playsound = FALSE) - psyker_organ = null - return ..() + if(grabbed_atom) + clear_grab(playsound = FALSE) + to_chat(owner, span_notice("You relax your telekinetic powers.")) + else + to_chat(owner, span_notice("You focus your telekinetic powers...
Middle-click: Grab/Punt | Right-click: Drop | Move mouse: to drag")) + return TRUE -/datum/action/cooldown/spell/pointed/telekinesis/InterceptClickOn(mob/living/clicker, params, atom/target) +/datum/action/cooldown/power/psyker/telekinesis/InterceptClickOn(mob/living/clicker, params, atom/target) + .=..() if(clicker != owner) return FALSE @@ -104,7 +93,7 @@ BIG TODO: FIX THAT return TRUE // You shouldn't get as stressed from picking up a pen as a closet. -/datum/action/cooldown/spell/pointed/telekinesis/proc/get_stress_cost_for_atom(atom/target) +/datum/action/cooldown/power/psyker/telekinesis/proc/get_stress_cost_for_atom(atom/target) var/cost if(isitem(target)) @@ -123,7 +112,7 @@ BIG TODO: FIX THAT return cost -/datum/action/cooldown/spell/pointed/telekinesis/process(seconds_per_tick) +/datum/action/cooldown/power/psyker/telekinesis/process(seconds_per_tick) var/mob/living/user = owner if(!grabbed_atom || !user?.client) STOP_PROCESSING(SSfastprocess, src) @@ -162,10 +151,10 @@ BIG TODO: FIX THAT return - psyker_organ.add_stress(PSYKER_STRESS_TRIVIAL * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever. + add_stress(PSYKER_STRESS_TRIVIAL * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever. // The fun part, punting shit. -/datum/action/cooldown/spell/pointed/telekinesis/proc/punt_held(mob/living/user, atom/target, params) +/datum/action/cooldown/power/psyker/telekinesis/proc/punt_held(mob/living/user, atom/target, params) if(!grabbed_atom) return @@ -185,7 +174,7 @@ BIG TODO: FIX THAT var/atom/movable/launched = grabbed_atom // Basically the same stress cost for picking it up. - psyker_organ.add_stress(get_stress_cost_for_atom(launched)) + add_stress(get_stress_cost_for_atom(launched)) clear_grab(playsound = FALSE) playsound(launched, 'sound/effects/magic/repulse.ogg', 75, TRUE) @@ -199,7 +188,7 @@ BIG TODO: FIX THAT ) // The proverbial leash. -/datum/action/cooldown/spell/pointed/telekinesis/proc/range_check(mob/living/user, atom/target) +/datum/action/cooldown/power/psyker/telekinesis/proc/range_check(mob/living/user, atom/target) if(!user || !isturf(user.loc)) return FALSE if(ismovable(target) && !isturf(target.loc)) @@ -209,7 +198,7 @@ BIG TODO: FIX THAT return TRUE // Can we ACTUALLY grab it or will it just fizz out? -/datum/action/cooldown/spell/pointed/telekinesis/proc/can_grab(mob/living/user, atom/target) +/datum/action/cooldown/power/psyker/telekinesis/proc/can_grab(mob/living/user, atom/target) if(user == target) return FALSE if(!ismovable(target)) @@ -242,7 +231,7 @@ BIG TODO: FIX THAT return TRUE -/datum/action/cooldown/spell/pointed/telekinesis/proc/grab_atom(atom/movable/target) +/datum/action/cooldown/power/psyker/telekinesis/proc/grab_atom(atom/movable/target) // If anything was already held, clear it first if(grabbed_atom) clear_grab(playsound = FALSE) @@ -279,11 +268,11 @@ BIG TODO: FIX THAT kinesis_catcher.assign_to_mob(owner) // Amounts are in the get_stress_cost_for_atom - psyker_organ.add_stress(get_stress_cost_for_atom(target)) + add_stress(get_stress_cost_for_atom(target)) START_PROCESSING(SSfastprocess, src) -/datum/action/cooldown/spell/pointed/telekinesis/proc/clear_grab(playsound = TRUE) +/datum/action/cooldown/power/psyker/telekinesis/proc/clear_grab(playsound = TRUE) if(!grabbed_atom) // Still ensure the fullscreen overlay is gone if we somehow desynced if(owner) @@ -322,12 +311,12 @@ BIG TODO: FIX THAT owner.clear_fullscreen("psyker_tk") kinesis_catcher = null -/datum/action/cooldown/spell/pointed/telekinesis/proc/on_statchange(mob/grabbed_mob, new_stat) +/datum/action/cooldown/power/psyker/telekinesis/proc/on_statchange(mob/grabbed_mob, new_stat) SIGNAL_HANDLER if(new_stat < stat_required) clear_grab() -/datum/action/cooldown/spell/pointed/telekinesis/proc/on_setanchored(atom/movable/grabbed_atom_ref, anchorvalue) +/datum/action/cooldown/power/psyker/telekinesis/proc/on_setanchored(atom/movable/grabbed_atom_ref, anchorvalue) SIGNAL_HANDLER if(grabbed_atom_ref.anchored) clear_grab() @@ -342,5 +331,3 @@ BIG TODO: FIX THAT alpha = 180 color = "#8A2BE2" mouse_opacity = MOUSE_OPACITY_OPAQUE - -*/ 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 05cf41a420e587..ef984ff01732ac 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 @@ -16,7 +16,7 @@ var/piety = 0 //At what point do we cap out piety? - var/max_piety = 20 + var/max_piety = 99 // The UI itself var/atom/movable/screen/theologist_piety/theologist_ui diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index 1759c9ec98c37e..8565c25358488f 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -1,7 +1,7 @@ /datum/power/theologist_root/revered name = "A Burden Revered" - desc = "Nullifies pain and slowly heals the creature over a prolonged period of time. \ + desc = "Nullifies pain and slowly heals the target over a prolonged period of time. \ Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again. \ This is mutually exclusive with the other 'A Burden...' powers." action_path = /datum/action/cooldown/power/theologist/theologist_root/revered @@ -10,7 +10,7 @@ /datum/action/cooldown/power/theologist/theologist_root/revered name = "A Burden Revered" - desc = "Nullifies pain and slowly heals the creature over a prolonged period of time. \ + desc = "Nullifies pain and slowly heals the target over a prolonged period of time. \ Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again." button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "transformslime" @@ -18,7 +18,7 @@ target_range = 1 target_type = /mob/living click_to_activate = TRUE - target_self = FALSE + target_self = TRUE // Current instance of the status effect var/datum/status_effect/power/burden_revered/active_effect diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index 801dd0e0e53ff2..0030baa785c6d2 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -135,8 +135,8 @@ RegisterSignal(current_beam, COMSIG_QDELETING, PROC_REF(beam_died)) target_glow = mutable_appearance( - icon = 'icons/mob/effects/genetics.dmi', - icon_state = "servitude", + icon = 'icons/effects/effects.dmi', + icon_state = "shield-yellow", layer = current_target.layer - 0.1, appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART ) @@ -163,6 +163,7 @@ return // Checks if our owner or target are DEAD if(current_target.stat == DEAD || owner.stat == DEAD) + to_chat(owner, span_warning("You cannot share burdens with dead people!")) clear_link() return @@ -171,8 +172,8 @@ clear_link() return - // checks if our owner or target are INCAPACITATED - if(HAS_TRAIT(owner, TRAIT_INCAPACITATED) || HAS_TRAIT(current_target, TRAIT_INCAPACITATED)) + // checks if our owner is INCAPACITATED + if(HAS_TRAIT(owner, TRAIT_INCAPACITATED)) clear_link() return diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index 76ca1c28dd4b73..70f6dbc9da227c 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -1,11 +1,143 @@ /datum/power/theologist_root/twisted name = "A Burden Twisted" - desc = "tbd \ - tbd \ + desc = "Twist the burdens of others into many lesser ones. The target is healed, then damaged for half that amount in random damage types. \ + Gives Piety proportional to the amount of damage twisted. \ This is mutually exclusive with the other 'A Burden...' powers." + action_path = /datum/action/cooldown/power/theologist/theologist_root/twisted value = 5 mob_trait = TRAIT_ARCHETYPE_SORCEROUS archetype = POWER_ARCHETYPE_SORCEROUS path = POWER_PATH_THEOLOGIST priority = POWER_PRIORITY_ROOT + +/datum/action/cooldown/power/theologist/theologist_root/twisted + name = "A Burden Twisted" + desc = "Twist the burdens of others into many lesser ones. The target is healed, then damaged for half that amount in random damage types. \ + Gives Piety proportional to the amount of damage twisted." + button_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "hand" + cooldown_time = 150 + target_range = 1 + target_type = /mob/living + click_to_activate = TRUE + target_self = FALSE + + //How much we can heal max with twisted per use. + var/healing_max = 30 + //Tracks how much healing we did throughout the proccess. + var/healing_done = 0 + + //Tracks how much damage we did throughout the process. + var/damage_done = 0 + + +/datum/action/cooldown/power/theologist/theologist_root/twisted/use_action(mob/living/user, mob/living/target) + owner.visible_message(span_warning("[owner] lays a hand on [target], twisting their wounds into other, smaller wounds!"), span_notice("You twist [target]'s wounds!")) + new /obj/effect/temp_visual/heal(get_turf(target), "#cf2525") + // I mean let's be real here if we are going to pawn off of other sound effects, chaos is basically the name of the game here. + playsound(owner, 'sound/effects/magic/cosmic_expansion.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + // I am going to shamelessly steal the red meditation spotlight for a moment. + target.apply_status_effect(/datum/status_effect/spotlight_light/resonant, 10) + // Does the healing and damage + heal_random_damage(owner, target) + deal_random_damage(owner, target, (healing_done / 2)) + + // Handles piety gain + var/piety_gained = max(0, floor(healing_done * 0.15)) + // resets for next time + healing_done = 0 + damage_done = 0 + adjust_piety(piety_gained) + if(piety_gained >= 1) + to_chat(owner, span_notice("You Burden Twisted yielded [piety_gained] piety!")) + else + to_chat(owner, span_notice("Your Burden Twisted yielded no piety!")) + + return TRUE + +/datum/action/cooldown/power/theologist/theologist_root/twisted/set_click_ability(mob/on_who) + . = ..() + to_chat(owner, span_notice("You ready yourself to twist the burden of others!
Left-click a creature next to you to target them!")) + +// Does the random 30 healing, entirely randomly. Very chaotic, very random. +/datum/action/cooldown/power/theologist/theologist_root/twisted/proc/heal_random_damage(mob/living/user, mob/living/target) + // Tells the while loop to stop + var/no_more_healing = FALSE + // Cap for how much our random healing can do. + var/rand_cap + //Used to save how much healing was done in that switch-case + var/heal_done + + while(!no_more_healing) + // Gets all damage types on target + var/list/damage_choices = list() + var/brute_damage = target.getBruteLoss() + var/burn_damage = target.getFireLoss() + var/tox_damage = target.getToxLoss() + var/oxy_damage = target.getOxyLoss() + // Checks if there's any injuries to heal b4 rolling the damage-type. + if(brute_damage > 0) damage_choices += "brute" + if(burn_damage > 0) damage_choices += "burn" + if(tox_damage > 0) damage_choices += "tox" + if(oxy_damage > 0) damage_choices += "oxy" + // Nothing to heal or healed the max already + if(!damage_choices.len || healing_done >= healing_max) + no_more_healing = TRUE + break + var/damage_choice = pick(damage_choices) + switch(damage_choice) + if("brute") + rand_cap = min(healing_max - healing_done, brute_damage) + heal_done = target.adjustBruteLoss(-rand(1, rand_cap)) + healing_done += heal_done + + if("burn") + rand_cap = min(healing_max - healing_done, burn_damage) + heal_done = target.adjustFireLoss(-rand(1, rand_cap)) + healing_done += heal_done + + if("tox") + rand_cap = min(healing_max - healing_done, tox_damage) + heal_done = target.adjustToxLoss(-rand(1, rand_cap)) + healing_done += heal_done + + if("oxy") + rand_cap = min(healing_max - healing_done, oxy_damage) + heal_done = target.adjustOxyLoss(-rand(1, rand_cap)) + healing_done += heal_done + no_more_healing = FALSE + return TRUE + +// Pretty similar to heal_random_damage but we're just hurting them. +/datum/action/cooldown/power/theologist/theologist_root/twisted/proc/deal_random_damage(mob/living/user, mob/living/target, damage_max) + // Tells the while loop to stop + var/no_more_damaging = FALSE + // Cap for how much our random damage we can do. + var/rand_cap + //Used to save how much damage was done in that switch-case + var/dam_done + + while(!no_more_damaging) + // Dealt max amount of damage already. + if(damage_done >= damage_max) + no_more_damaging = TRUE + break + var/list/damage_choices = list("brute", "burn", "tox", "oxy") + rand_cap = min(damage_max - damage_done) + dam_done = rand(1, rand_cap) + var/damage_choice = pick(damage_choices) + switch(damage_choice) + if("brute") + target.adjustBruteLoss(dam_done) + if("burn") + target.adjustFireLoss(dam_done) + if("tox") + target.adjustToxLoss(dam_done) + // The jackpot + if("oxy") + target.adjustOxyLoss(dam_done) + damage_done += dam_done + + no_more_damaging = FALSE + return TRUE From 09b7916875980859c376426bd4d8086ed132f733 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 28 Jan 2026 13:47:05 +0100 Subject: [PATCH 031/212] Adds powers as a subsystem, added defines to theologist, baseline entropic mending. --- code/__DEFINES/~doppler_defines/powers.dm | 15 ++ .../powers/resonant/psyker/_psyker_action.dm | 2 +- .../powers/resonant/psyker/_psyker_power.dm | 1 + .../theologist/_theologist_action.dm | 29 ++-- .../sorcerous/theologist/_theologist_piety.dm | 98 +++++++++++- .../sorcerous/theologist/_theologist_power.dm | 9 ++ .../sorcerous/theologist/_theologist_root.dm | 10 +- .../theologist/_theologist_root_revered.dm | 26 +-- .../theologist/_theologist_root_shared.dm | 20 +-- .../theologist/_theologist_root_twisted.dm | 148 ++++++++++------- .../sorcerous/theologist/entropic_mending.dm | 151 ++++++++++++++++++ .../modular_powers/code/powers_action.dm | 19 ++- .../modular_powers/code/powers_subsystem.dm | 3 +- tgstation.dme | 2 + 14 files changed, 430 insertions(+), 103 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_power.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index a12f2eb1420226..a14ede0d830b9f 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -76,6 +76,21 @@ /// Any Enigmatist lore whatsoever. #define ENIGMATIST_ANY_ALL (ENIGMATIST_RESONANT|ENIGMATIST_UNSEALED|ENIGMATIST_ILLUMINATED|ENIGMATIST_DIVIDED) +/** + * SORCEROUS: THEOLOGIST + * All defines related to the enigmatist powers. + */ + +// How much root abilities should heal (max), if they heal. +#define ROOT_HEALING 30 + +// Healing equates to this much piety. +#define PIETY_HEALING_COEFFICIENT 0.2 + +// Maximum amount of Piety +#define PIETY_MAX 99 + + /**MORTAL DEFINES * I'm literally just using this to define Breacher Knuckle right now * These things, they take time. 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 d1552f08b97737..dc7467fcaad3c5 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 @@ -36,7 +36,7 @@ if(!ValidateOrgan()) owner.balloon_alert(owner, "No paracausal gland!") return FALSE - if(target) + if(isliving(target)) if(disabled_by_mental_immunity && target.can_block_magic(MAGIC_RESISTANCE_MIND)) add_stress(PSYKER_STRESS_MINOR) // This actively stresses you out. owner.balloon_alert(owner, "Something interveres with your powers!") 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 8807697222e014..561c07e38d161f 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 @@ -7,4 +7,5 @@ archetype = POWER_ARCHETYPE_RESONANT path = POWER_PATH_PSYKER + priority = POWER_PRIORITY_BASIC required_powers = list(/datum/power/psyker_root) 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 f34a55ff9bee50..36129c1b2258e2 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 @@ -5,31 +5,40 @@ button_icon = 'icons/mob/actions/backgrounds.dmi' // The organ that processes most of the Psyker Powers. Mostly all functions here communicate with this. - var/datum/power/theologist_piety/piety_power + var/datum/component/theologist_piety/piety_component // The UI used for piety.alist var/atom/movable/screen/theologist_piety/theologist_ui + // Cost in Piety to use. + // THIS IS NOT IN EVERY POWER DATUM, ONLY ONES THAT HAVE RESOURCE SPENDING MECHANICS. + var/cost + /datum/action/cooldown/power/theologist/New() . = ..() - ValidatePietyPower() + ValidatePietyComponent() -// Since Theologist has both 3 roots and a persistent resource system, we use a hidden extra power for handling Piety. -/datum/action/cooldown/power/theologist/proc/ValidatePietyPower() +// Since Theologist has both 3 roots and a persistent resource system, we use a component for handling Piety +/datum/action/cooldown/power/theologist/proc/ValidatePietyComponent() if(owner) // Prevents runtiming on start var/mob/living/carrier = owner - piety_power = carrier.get_power(/datum/power/theologist_piety) - if(!piety_power) + piety_component = carrier.GetComponent(/datum/component/theologist_piety) + + if(!piety_component) return FALSE return TRUE -// Validation handled in the piety power. +// Validation handled in the piety component. /datum/action/cooldown/power/theologist/proc/adjust_piety(amount, override_cap) - piety_power.adjust_piety(amount, override_cap) + piety_component.adjust_piety(amount, override_cap) + +//Easy access to piety +/datum/action/cooldown/power/theologist/proc/get_piety() + return piety_component.piety -// We check to see if our piety power is actually there, because usually things will go bad if they don't. +// We check to see if our piety component is actually there, because usually things will go bad if they don't. /datum/action/cooldown/power/theologist/try_use(mob/living/user, mob/living/target) - if(!ValidatePietyPower()) + if(!ValidatePietyComponent()) owner.balloon_alert(owner, "Yell at the coders; you're missing your piety system!") return FALSE . = .. () 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 ef984ff01732ac..27dff6bf92466b 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 @@ -1,6 +1,101 @@ /// Helper to format the text that gets thrown onto the piety hud element. #define FORMAT_PIETY_TEXT(charges) MAPTEXT("
[round(charges)]
") +/datum/component/theologist_piety + dupe_mode = COMPONENT_DUPE_UNIQUE + + // The mob we’re attached to is always `parent`. + var/mob/living/attached_mob + + // Whatever state your old attached_theologist_piety tracked: + var/piety = 0 + var/max_piety = PIETY_MAX + + // The UI itself + var/atom/movable/screen/theologist_piety/theologist_ui + +/datum/component/theologist_piety/Initialize() + . = ..() + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + attached_mob = parent + + + // If your old system used signals, register them here. + RegisterWithParent() + + // If your old system processed over time, start that here. + // START_PROCESSING(SSprocessing, src) // only if you actually need processing + +/datum/component/theologist_piety/RegisterWithParent() + . = ..() + if(attached_mob.hud_used) + install_piety_hud(parent) + else + RegisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created)) + // Examples (swap for your real signals): + // RegisterSignal(attached_mob, COMSIG_MOB_LIFE, PROC_REF(on_life)) + // RegisterSignal(attached_mob, COMSIG_LIVING_DEATH, PROC_REF(on_death)) + +/datum/component/theologist_piety/UnregisterFromParent() + // UnregisterSignal(attached_mob, list(COMSIG_..., COMSIG_...)) + . = ..() + UnregisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED) + +/datum/component/theologist_piety/Destroy() + UnregisterFromParent() + + if(!attached_mob) + return + + if(attached_mob.hud_used && theologist_ui) + attached_mob.hud_used.infodisplay -= theologist_ui + qdel(theologist_ui) + theologist_ui = null + + attached_mob = null + return ..() + +/datum/component/theologist_piety/proc/on_hud_created(datum/source) + SIGNAL_HANDLER + + var/mob/living/living_holder = attached_mob + if(!living_holder || !living_holder.hud_used) + return + + install_piety_hud(living_holder) + +/datum/component/theologist_piety/proc/install_piety_hud(mob/living/living_holder) + if(theologist_ui) // already installed + return + + var/datum/hud/hud_used = living_holder.hud_used + theologist_ui = new /atom/movable/screen/theologist_piety(null, hud_used) + hud_used.infodisplay += theologist_ui + + // Set initial text so it isn't blank until first adjust. + theologist_ui.maptext = FORMAT_PIETY_TEXT(piety) + + // THIS is the missing “why it only appears after changeling” + hud_used.show_hud(hud_used.hud_version) + +/datum/component/theologist_piety/proc/adjust_piety(amount, override_cap) + if(!isnum(amount)) + return + var/cap_to = isnum(override_cap) ? override_cap : max_piety + piety = clamp(piety + amount, 0, cap_to) + + theologist_ui?.maptext = FORMAT_PIETY_TEXT(piety) + +// UI Elements for Piety +/atom/movable/screen/theologist_piety + name = "piety" + icon = 'icons/hud/blob.dmi' // TODO: Get sprites/UI for this. + icon_state = "block" + screen_loc = "WEST,CENTER-2:15" // TODO: Define & Move this. + +/* + /datum/power/theologist_piety name = "Piety" desc = "Responsible for managing Piety." @@ -16,7 +111,7 @@ var/piety = 0 //At what point do we cap out piety? - var/max_piety = 99 + var/max_piety = PIETY_MAX // The UI itself var/atom/movable/screen/theologist_piety/theologist_ui @@ -85,3 +180,4 @@ piety = clamp(piety + amount, 0, cap_to) theologist_ui?.maptext = FORMAT_PIETY_TEXT(piety) +*/ diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_power.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_power.dm new file mode 100644 index 00000000000000..e2a00c4c07ad3d --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_power.dm @@ -0,0 +1,9 @@ +/datum/power/theologist + name = "Theologist Power" + desc = "We used to call it spells but lets be real here we ditched the spell code for our own snowflake action-code a while ago. The spells system hasn't called in years; its time to let her go. \ + Also tell a developer you're seeing this when you read this; this isn't meant for your eyes. Shoo!" + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THEOLOGIST + priority = POWER_PRIORITY_BASIC + abstract_parent_type = /datum/power/theologist diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root.dm index ca23dac98c99dc..5cde484550ac55 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 @@ -1,6 +1,7 @@ /datum/power/theologist_root name = "Abstract theologist root" - desc = "Oh noes, tell the coders!" + desc = "Some spend most of their life looking for the holy grail, the root of life, yggdrasil and all those things. This is the root. Of the healer powers. So you're getting close? \ + Present this to the developers for the next hint in your quest. Because you're not actually meant to have this." abstract_parent_type = /datum/power/theologist_root mob_trait = TRAIT_ARCHETYPE_SORCEROUS @@ -8,10 +9,9 @@ path = POWER_PATH_THEOLOGIST priority = POWER_PRIORITY_ROOT -/datum/power/theologist_root/revered/post_add() +/datum/power/theologist_root/post_add() if(!power_holder) // So it doesn't runtime at init return - // We pass along the piety power to actually handle most of the piety stuff. - var/datum/power/theologist_piety/piety = new /datum/power/theologist_piety - piety.add_to_holder(new_holder = power_holder) + // We pass along the piety component to actually handle most of the piety stuff. + power_holder.AddComponent(/datum/component/theologist_piety, power_holder) . = ..() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index 8565c25358488f..33d606e03d3162 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -1,7 +1,7 @@ /datum/power/theologist_root/revered name = "A Burden Revered" - desc = "Nullifies pain and slowly heals the target over a prolonged period of time. \ + desc = "Nullifies pain and slowly heals the targeted creature over a prolonged period of time. This may be yourself. \ Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again. \ This is mutually exclusive with the other 'A Burden...' powers." action_path = /datum/action/cooldown/power/theologist/theologist_root/revered @@ -10,7 +10,7 @@ /datum/action/cooldown/power/theologist/theologist_root/revered name = "A Burden Revered" - desc = "Nullifies pain and slowly heals the target over a prolonged period of time. \ + desc = "Nullifies pain and slowly heals the targeted creature over a prolonged period of time. This may be yourself. \ Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again." button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "transformslime" @@ -23,11 +23,17 @@ // Current instance of the status effect var/datum/status_effect/power/burden_revered/active_effect + // Keeps track if we are targeting ourselves, as to ensure we don't give ourselves piety by repeatedly healing ourselves, which isn't very pious (according to MOST religions). + var/healing_self = FALSE + /datum/action/cooldown/power/theologist/theologist_root/revered/use_action(mob/living/user, mob/living/target) to_chat(owner, span_boldnotice("Placeholder")) if(active_effect) qdel(active_effect) active_effect = target.apply_status_effect(/datum/status_effect/power/burden_revered, src) + active = TRUE + if(active_effect && target == owner) + healing_self = TRUE return TRUE /datum/action/cooldown/power/theologist/theologist_root/revered/set_click_ability(mob/on_who) @@ -36,15 +42,17 @@ /datum/action/cooldown/power/theologist/theologist_root/revered/proc/effect_expired(amount) adjust_piety(amount) - if(amount >= 1) - to_chat(owner, span_notice("Your Burden Revered has expired! You gained [amount] piety!")) + if(amount >= 1 && !healing_self) + to_chat(owner, span_notice("Your previous Burden Revered has expired! You gained [amount] piety!")) owner.playsound_local(owner, 'sound/effects/pray.ogg', 50, FALSE) else - to_chat(owner, span_notice("Your Burden Revered has expired!")) - return + to_chat(owner, span_notice("Your previous Burden Revered has expired!")) + //Always reset this after use. + active = FALSE + healing_self = FALSE -///datum/power/theologist_root/revered/process() + return // Status effect that Burden Revered applies /datum/status_effect/power/burden_revered @@ -55,7 +63,7 @@ // The power responsible for this, so we can make sure it properly gives piety to the caster var/datum/action/cooldown/power/theologist/theologist_root/revered/burden_power // The maximum amount we will heal - var/healing_max = 30 + var/healing_max = ROOT_HEALING // How much we have healed already var/healing_done = 0 // How much we heal per tick. @@ -138,7 +146,7 @@ // QDEL destroys burden_power /datum/status_effect/power/burden_revered/proc/expire() - var/piety_gained = max(0, floor(healing_done * 0.2)) // TODO: defines + var/piety_gained = max(0, floor(healing_done * PIETY_HEALING_COEFFICIENT)) // TODO: defines // Report back BEFORE deletion starts if(burden_power) burden_power.effect_expired(piety_gained) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index 0030baa785c6d2..198be82d6b3a08 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -62,15 +62,15 @@ // Currency exchange for piety. /datum/action/cooldown/power/theologist/theologist_root/shared/proc/exchange_buildup() // Have we been a good boy? - if(piety_buildup >= 5) - piety_buildup -= 5 + if(piety_buildup >= 1) + piety_buildup -= 1 adjust_piety(1) to_chat(owner, span_notice("Taking on the burdens of others has gained you piety!")) // Have we been a bad boy? - else if (piety_buildup <= -5) - piety_buildup += 5 + else if (piety_buildup <= -1) + piety_buildup += 1 // Have we been a VERY bad boy? Don't think you can get away with willynilly using this at 0 piety. - if(piety_power.piety <= 0 && prob(25)) + if(get_piety() <= 0 && prob(25)) lightningbolt(owner) if(ishuman(owner)) var/mob/living/carbon/human/sinner = owner @@ -172,8 +172,10 @@ clear_link() return - // checks if our owner is INCAPACITATED - if(HAS_TRAIT(owner, TRAIT_INCAPACITATED)) + // checks if our owner is INCAPACITATED or KNOCKED DOWN + // Honestly more of a balance concern the latter, sorry paraplegic people. + if(HAS_TRAIT(owner, TRAIT_INCAPACITATED) || HAS_TRAIT(owner, TRAIT_FLOORED)) + to_chat(owner, span_warning("You need to be standing!")) clear_link() return @@ -248,9 +250,9 @@ // Piety buildup increases/deductions if(taker == owner) - piety_buildup += amount + piety_buildup += amount * PIETY_HEALING_COEFFICIENT else if(giver == owner) - piety_buildup -= amount + piety_buildup -= amount * PIETY_HEALING_COEFFICIENT return diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index 70f6dbc9da227c..359546e1b899c7 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -1,6 +1,6 @@ /datum/power/theologist_root/twisted name = "A Burden Twisted" - desc = "Twist the burdens of others into many lesser ones. The target is healed, then damaged for half that amount in random damage types. \ + desc = "Channel chaotic energies into another creature next to you. The target is healed over time in random amounts up to the maximum, then damaged for half that amount in random damage types. \ Gives Piety proportional to the amount of damage twisted. \ This is mutually exclusive with the other 'A Burden...' powers." action_path = /datum/action/cooldown/power/theologist/theologist_root/twisted @@ -13,38 +13,71 @@ /datum/action/cooldown/power/theologist/theologist_root/twisted name = "A Burden Twisted" - desc = "Twist the burdens of others into many lesser ones. The target is healed, then damaged for half that amount in random damage types. \ + desc = "Channel chaotic energies into another creature next to you. The target is healed over time in random amounts up to the maximum, then damaged for half that amount in random damage types. \ Gives Piety proportional to the amount of damage twisted." button_icon = 'icons/mob/actions/actions_cult.dmi' button_icon_state = "hand" - cooldown_time = 150 + cooldown_time = 600 target_range = 1 target_type = /mob/living click_to_activate = TRUE target_self = FALSE + unset_after_click = TRUE //How much we can heal max with twisted per use. - var/healing_max = 30 + var/healing_max = ROOT_HEALING //Tracks how much healing we did throughout the proccess. var/healing_done = 0 //Tracks how much damage we did throughout the process. var/damage_done = 0 + //The beam effect when channeling + var/datum/beam/current_beam + /datum/action/cooldown/power/theologist/theologist_root/twisted/use_action(mob/living/user, mob/living/target) - owner.visible_message(span_warning("[owner] lays a hand on [target], twisting their wounds into other, smaller wounds!"), span_notice("You twist [target]'s wounds!")) - new /obj/effect/temp_visual/heal(get_turf(target), "#cf2525") - // I mean let's be real here if we are going to pawn off of other sound effects, chaos is basically the name of the game here. - playsound(owner, 'sound/effects/magic/cosmic_expansion.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) - // I am going to shamelessly steal the red meditation spotlight for a moment. - target.apply_status_effect(/datum/status_effect/spotlight_light/resonant, 10) + // Because we have a do_while, it won't get to the usual unset_click_ability() until after the efffect resolves, so we have to run it here. + unset_click_ability(owner, FALSE) + //Tells the do_while loop to keep_going + var/keep_going = TRUE + owner.visible_message(span_warning("[owner] lays a hand on [target.get_visible_name()], twisting their injurioes into other, smaller injuries!"), span_notice("You twist [target.get_visible_name()]'s injuries!")) // Does the healing and damage - heal_random_damage(owner, target) - deal_random_damage(owner, target, (healing_done / 2)) + + do + active = TRUE + // I am going to shamelessly steal the red meditation spotlight for a moment. + target.apply_status_effect(/datum/status_effect/spotlight_light/resonant, 1200) + current_beam = owner.Beam(target, icon_state = "light_beam", time = 120 SECONDS, maxdistance = target_range, beam_type = /obj/effect/ebeam/medical, beam_color = "#cf2525") + if(do_after(owner, 25, target = target)) + if(target_range) + var/turf/owner_turf = get_turf(owner) + var/turf/target_turf = get_turf(target) + if(owner_turf && target_turf && get_dist(owner_turf, target_turf) > target_range) + owner.balloon_alert(owner, "Out of range!") + break // we use break here instead cuase we don't want to heal them anymore. + if(target.health >= target.maxHealth) + to_chat(owner, span_notice("Your target's health is full!")) + keep_going = FALSE + if(target.health < target.maxHealth) + new /obj/effect/temp_visual/heal(get_turf(target), "#cf2525") + playsound(owner, 'sound/effects/magic/cosmic_expansion.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + var/healtodmgcap = heal_random_damage(target) + deal_random_damage(target, (healtodmgcap / 2)) + if(healing_done >= healing_max) + to_chat(owner, span_notice("You have channeled the full effect of [name]!")) + keep_going = FALSE + else + keep_going = FALSE + while (keep_going) + + // cleanup + active = FALSE + target.remove_status_effect(/datum/status_effect/spotlight_light/resonant) + QDEL_NULL(current_beam) // Handles piety gain - var/piety_gained = max(0, floor(healing_done * 0.15)) + var/piety_gained = max(0, floor(healing_done * PIETY_HEALING_COEFFICIENT)) // resets for next time healing_done = 0 damage_done = 0 @@ -60,57 +93,52 @@ . = ..() to_chat(owner, span_notice("You ready yourself to twist the burden of others!
Left-click a creature next to you to target them!")) -// Does the random 30 healing, entirely randomly. Very chaotic, very random. -/datum/action/cooldown/power/theologist/theologist_root/twisted/proc/heal_random_damage(mob/living/user, mob/living/target) - // Tells the while loop to stop - var/no_more_healing = FALSE +// Does the given amount of healing, entirely randomly. Very chaotic, very random. +/datum/action/cooldown/power/theologist/theologist_root/twisted/proc/heal_random_damage(mob/living/target) // Cap for how much our random healing can do. var/rand_cap - //Used to save how much healing was done in that switch-case - var/heal_done - - while(!no_more_healing) - // Gets all damage types on target - var/list/damage_choices = list() - var/brute_damage = target.getBruteLoss() - var/burn_damage = target.getFireLoss() - var/tox_damage = target.getToxLoss() - var/oxy_damage = target.getOxyLoss() - // Checks if there's any injuries to heal b4 rolling the damage-type. - if(brute_damage > 0) damage_choices += "brute" - if(burn_damage > 0) damage_choices += "burn" - if(tox_damage > 0) damage_choices += "tox" - if(oxy_damage > 0) damage_choices += "oxy" - // Nothing to heal or healed the max already - if(!damage_choices.len || healing_done >= healing_max) - no_more_healing = TRUE - break - var/damage_choice = pick(damage_choices) - switch(damage_choice) - if("brute") - rand_cap = min(healing_max - healing_done, brute_damage) - heal_done = target.adjustBruteLoss(-rand(1, rand_cap)) - healing_done += heal_done - - if("burn") - rand_cap = min(healing_max - healing_done, burn_damage) - heal_done = target.adjustFireLoss(-rand(1, rand_cap)) - healing_done += heal_done - - if("tox") - rand_cap = min(healing_max - healing_done, tox_damage) - heal_done = target.adjustToxLoss(-rand(1, rand_cap)) - healing_done += heal_done - - if("oxy") - rand_cap = min(healing_max - healing_done, oxy_damage) - heal_done = target.adjustOxyLoss(-rand(1, rand_cap)) - healing_done += heal_done - no_more_healing = FALSE - return TRUE + //Used to save how much healing was done in that switch-case. + var/heal_done = 0 + + // Gets all damage types on target + var/list/damage_choices = list() + var/brute_damage = target.getBruteLoss() + var/burn_damage = target.getFireLoss() + var/tox_damage = target.getToxLoss() + var/oxy_damage = target.getOxyLoss() + // Checks if there's any injuries to heal b4 rolling the damage-type. + if(brute_damage > 0) damage_choices += "brute" + if(burn_damage > 0) damage_choices += "burn" + if(tox_damage > 0) damage_choices += "tox" + if(oxy_damage > 0) damage_choices += "oxy" + // Hey we already healed you to the max! + if(healing_done >= healing_max) + return 0 + // Nothing to heal + if(!damage_choices.len) + return + var/damage_choice = pick(damage_choices) + switch(damage_choice) + if("brute") + rand_cap = min(healing_max - healing_done, brute_damage) + heal_done = target.adjustBruteLoss(-rand(1, rand_cap)) + healing_done += heal_done + if("burn") + rand_cap = min(healing_max - healing_done, burn_damage) + heal_done = target.adjustFireLoss(-rand(1, rand_cap)) + healing_done += heal_done + if("tox") + rand_cap = min(healing_max - healing_done, tox_damage) + heal_done = target.adjustToxLoss(-rand(1, rand_cap)) + healing_done += heal_done + if("oxy") + rand_cap = min(healing_max - healing_done, oxy_damage) + heal_done = target.adjustOxyLoss(-rand(1, rand_cap)) + healing_done += heal_done + return heal_done // Pretty similar to heal_random_damage but we're just hurting them. -/datum/action/cooldown/power/theologist/theologist_root/twisted/proc/deal_random_damage(mob/living/user, mob/living/target, damage_max) +/datum/action/cooldown/power/theologist/theologist_root/twisted/proc/deal_random_damage(mob/living/target, damage_max) // Tells the while loop to stop var/no_more_damaging = FALSE // Cap for how much our random damage we can do. diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm new file mode 100644 index 00000000000000..7130702ab43988 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm @@ -0,0 +1,151 @@ +/* This largely served as the first example power of Theologist outside the roots. +Biggest takeaway is just use status effects if it's any form of lingering effect; and to borrow cool mechanics from other code. +Entropic Mending removes wounds (sometimes) and speeds up the target's metabolism, hunger and blood regen by 3x. +*/ +/datum/power/theologist/entropic_mending + name = "Entropic Mending" + desc = "Entropy's a long road, a few steps further along it will do you more good than harm. Spend 5 Piety to touch another humanoid and attempt to restore it's lingering wounds. \ + Moderate wounds will be healed automatically; all other wounds have a random chance to depending on severity. \ + Invoking this power will cause temporary, lingering entropic effects; such as increased metabolism, hunger and blood replenishment, at triple pace." + action_path = /datum/action/cooldown/power/theologist/entropic_mending + value = 6 + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THEOLOGIST + required_powers = list(/datum/power/theologist_root/twisted) + +/datum/action/cooldown/power/theologist/entropic_mending + name = "Entropic Mending" + desc = "Entropy's a long road, a few steps further along it will do one more good than harm. Spend 5 Piety to touch another humanoid and attempt to restore it's lingering wounds. \ + Moderate wounds will be healed automatically; all other wounds have a random chance to depending on severity. \ + Invoking this power will cause temporary, lingering entropic effects; such as increased metabolism, hunger and blood replenishment, at triple pace." + button_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "manip" + cooldown_time = 150 + target_range = 1 + target_type = /mob/living/carbon/human + click_to_activate = TRUE + target_self = TRUE // Make false in final version, for testing. + unset_after_click = TRUE + cost = 5 // TODO PROGRAM IN PIETY COST LOL + + // Current instance of the status effect + var/datum/status_effect/power/entropic_mending/active_effect + +/datum/action/cooldown/power/theologist/entropic_mending/use_action(mob/living/user, mob/living/target) + to_chat(owner, span_boldnotice("You begin to mend [target.get_visible_name()]")) + if(active_effect) + qdel(active_effect) + active_effect = target.apply_status_effect(/datum/status_effect/power/entropic_mending, src) + active = TRUE + return TRUE + +/datum/action/cooldown/power/theologist/entropic_mending/set_click_ability(mob/on_who) + . = ..() + to_chat(owner, span_notice("You channel entropic energies into your hand!
Left-click a creature next to you to target them!")) + +/datum/action/cooldown/power/theologist/entropic_mending/proc/effect_expired(amount) + //Always reset this after use. + active = FALSE + return + +// Status effect that Burden Revered applies +/datum/status_effect/power/entropic_mending + id = "entropic_mending" + duration = 3 MINUTES + tick_interval = 1 SECONDS + alert_type = /atom/movable/screen/alert/status_effect/entropic_mending + + // The power responsible for this. + var/datum/action/cooldown/power/theologist/entropic_mending/entropic_mending + + // Because a lot of things here require static types. + var/mob/living/carbon/human/victim + + // How much we speed up blood regen with + var/blood_regen_rate = 3 + // How much we speed up metabolism with + var/metabolic_boost = 3 + // How much we speed up hunger gain with + var/hunger_rate = 3 + /// Tracks if we've modified the physiology of the owner + VAR_PRIVATE/physiology_modified = FALSE + // Tracks how many wounds were healed by this. + var/wounds_treated = 0 + + +/atom/movable/screen/alert/status_effect/entropic_mending + name = "Entropic Mending" + desc = "Your body's internal functions seem to be accelerated, for better or worse." + icon_state = "arrow8" // Placeholder + +// So given it is 'part of the effect', we actually handle the wound removal on here. +/datum/status_effect/power/entropic_mending/on_apply() + victim = owner // Whilst I would like to set it on_creation, it doesn't always pass it along 4somerasinss. + // Attemps to remove wounds + for(var/datum/wound/wound in victim.all_wounds) + switch(wound.severity) + if(WOUND_SEVERITY_TRIVIAL || WOUND_SEVERITY_MODERATE) + wound.remove_wound() + to_chat(entropic_mending.owner, span_notice("The restorative energies manage to treat the [wound.name]!")) + to_chat(victim, span_notice("Your [wound.name] got healed!")) + wounds_treated++ + if(WOUND_SEVERITY_SEVERE) + if(prob(60)) + wound.remove_wound() + to_chat(entropic_mending.owner, span_notice("The restorative energies manage to treat the [wound.name]!")) + to_chat(victim, span_notice("Your [wound.name] got healed!")) + wounds_treated++ + else + to_chat(entropic_mending.owner, span_warning("The restorative energies fail to treat the [wound.name]!")) + if(WOUND_SEVERITY_CRITICAL) + if(prob(30)) + wound.remove_wound() + to_chat(entropic_mending.owner, span_notice("The restorative energies manage to treat the [wound.name]!")) + to_chat(victim, span_notice("Your [wound.name] got healed!")) + wounds_treated++ + else + to_chat(entropic_mending.owner, span_warning("The restorative energies fail to treat the [wound.name]!")) + if(!LAZYLEN(victim.all_wounds)) // Not necessarily bad, you might use this for it's metabolize effect. + to_chat(entropic_mending.owner, span_notice("[victim.get_visible_name()] has no wounds to treat!")) + else if(wounds_treated <= 0) + to_chat(entropic_mending.owner, span_warning("[entropic_mending.name] failed to heal any of [victim.get_visible_name()]'s wounds!")) + else if(LAZYLEN(victim.all_wounds)) + to_chat(entropic_mending.owner, span_notice("[entropic_mending.name] managed to heal some of [victim.get_visible_name()]'s wounds!")) + else + to_chat(entropic_mending.owner, span_notice("[entropic_mending.name] managed to heal all of [victim.get_visible_name()]'s' wounds!")) + + // Makes our blood regenerate faster + if(!physiology_modified) + victim.physiology.blood_regen_mod *= blood_regen_rate + physiology_modified = TRUE + + return TRUE + +// Sets the link with the original action +/datum/status_effect/power/entropic_mending/on_creation(mob/living/new_owner, datum/action/cooldown/power/theologist/entropic_mending/passed_power) + entropic_mending = passed_power + . = ..() + +/datum/status_effect/power/entropic_mending/on_remove() + // Removes the blood regen mult + if(physiology_modified) + victim.physiology.blood_regen_mod /= blood_regen_rate + physiology_modified = FALSE + expire() + +// We're not spelling it out but basically all the vibes of age-based healing. +/datum/status_effect/power/entropic_mending/tick(seconds_between_ticks) + //Code that the metabolic boost virus symptom would shamelessly steal from us, 16 years in the past. + // Unlike metabolic boost we actually check if there's a liver + var/obj/item/organ/liver/liver = victim.get_organ_slot(ORGAN_SLOT_LIVER) + if(liver) + // Not totally accurate with the liver damage but WHO WILL NOTICE THIS DISCRAPANCY?! IS IT YOU, MR./MS. CODEDIVER?! ARE YOU GOING TO TRIVIA THIS LIKE VIGGO'S TOE?! + victim.reagents.metabolize(victim, (metabolic_boost - (liver.damage * 0.03)) * SSMOBS_DT, 0, can_overdose=TRUE) + victim.overeatduration = max(victim.overeatduration - 4 SECONDS, 0) + victim.adjust_nutrition(-hunger_rate * HUNGER_FACTOR) //Hunger depletes at 3x the normal speed + +/datum/status_effect/power/entropic_mending/proc/expire() + // Report back BEFORE deletion starts + if(entropic_mending) + entropic_mending.effect_expired() diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index c08036ec87ff29..bf0a7cac7f87bf 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -29,6 +29,8 @@ var/human_only = TRUE /// Can we target ourselves? var/target_self = TRUE + // Do we need our hands free? + var/need_hands_free = TRUE /// Maximum targeting range (in tiles) for click_to_activate powers. Set to 0 or null for no range limit. var/target_range = 7 @@ -41,12 +43,6 @@ SHOULD_CALL_PARENT(TRUE) if(!can_use(user, target)) return FALSE - if(disabled_by_incapacitate && HAS_TRAIT(user, TRAIT_INCAPACITATED)) - owner.balloon_alert(user, "incapacitated!") - return FALSE - if(disabled_by_silence && HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED)) - owner.balloon_alert(user, "silenced!") - return FALSE if(use_action(user, target)) return TRUE return FALSE @@ -56,7 +52,16 @@ SHOULD_CALL_PARENT(TRUE) if(!can_be_used_by(user)) // Runs can_be_used_by below return FALSE - if(req_stat < user.stat) // Are we conscious? + if(disabled_by_incapacitate && HAS_TRAIT(user, TRAIT_INCAPACITATED)) + owner.balloon_alert(user, "incapacitated!") + return FALSE + if(disabled_by_silence && HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED)) + owner.balloon_alert(user, "silenced!") + return FALSE + if(need_hands_free && HAS_TRAIT(user, TRAIT_HANDS_BLOCKED)) + owner.balloon_alert(user, "restrained!") + return FALSE + if(req_stat < user.stat) // Whilst this seems similiar to trait_incapacitated, it is also used to check if you're dead in the event that disable_by_incapacitate is false. No corpses using powers! owner.balloon_alert(user, "incapacitated!") return FALSE return TRUE diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index ee52845903b64e..a61e931ece12f2 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -1,7 +1,8 @@ // Both of these lists are shifted to glob so they are generated at world start instead of risking players doing preference stuff before the subsystem inits. GLOBAL_LIST_INIT_TYPED(powers_blacklist, /list/datum/power, list( - list(/datum/power/item_power/thaumaturge_root, /datum/power/enigmatist_root) + list(/datum/power/item_power/thaumaturge_root, /datum/power/enigmatist_root), + list(/datum/power/theologist_root/revered, /datum/power/theologist_root/shared, /datum/power/theologist_root/twisted) // The three Theologist Roots )) GLOBAL_LIST_INIT(powers_requirements_list, generate_powers_requirements_list()) diff --git a/tgstation.dme b/tgstation.dme index 1c7f0f69fa5f8a..63c7ae5fc6bbdc 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7436,11 +7436,13 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_action.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_power.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_shared.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_twisted.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_revered.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_piety.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\entropic_mending.dm" #include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" From 4cbcb14c2b90d20c3211c9a176447f557b68fe65 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 28 Jan 2026 17:21:21 +0100 Subject: [PATCH 032/212] Adds smiting abilities, adjusts a few costs and adjusts the action system for more flexibile. Added a cost system for Piety. --- code/__DEFINES/~doppler_defines/powers.dm | 2 +- .../powers/resonant/psyker/_psyker_root.dm | 2 +- .../theologist/_theologist_action.dm | 11 +- .../sorcerous/theologist/_theologist_piety.dm | 3 + .../sorcerous/theologist/entropic_mending.dm | 31 +-- .../sorcerous/theologist/smiting_strike.dm | 179 ++++++++++++++++++ .../modular_powers/code/powers_action.dm | 5 + tgstation.dme | 1 + 8 files changed, 217 insertions(+), 17 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index a14ede0d830b9f..e752e68f12c0cb 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -88,7 +88,7 @@ #define PIETY_HEALING_COEFFICIENT 0.2 // Maximum amount of Piety -#define PIETY_MAX 99 +#define PIETY_MAX 50 /**MORTAL DEFINES 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 f410ead02fcdc5..6d541815d4f4e7 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 @@ -5,7 +5,7 @@ grown by prolonged exposure to certain types of Resonance. \ The catalyst for psychic abilities; but beware overexerting it." - value = 5 + value = 3 mob_trait = TRAIT_ARCHETYPE_RESONANT archetype = POWER_ARCHETYPE_RESONANT path = POWER_PATH_PSYKER 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 36129c1b2258e2..1cb2c16d632ad7 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 @@ -23,7 +23,6 @@ if(owner) // Prevents runtiming on start var/mob/living/carrier = owner piety_component = carrier.GetComponent(/datum/component/theologist_piety) - if(!piety_component) return FALSE return TRUE @@ -41,4 +40,14 @@ if(!ValidatePietyComponent()) owner.balloon_alert(owner, "Yell at the coders; you're missing your piety system!") return FALSE + if(piety_component.piety < cost) + user.balloon_alert(user, "needs [cost] piety!") + return FALSE . = .. () + +// Make sure the cost gets deducted after using the power (we already checked if we can afford it) +/datum/action/cooldown/power/theologist/on_action_success(mob/living/user, atom/target) + if(cost) + adjust_piety(-cost) + return + 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 27dff6bf92466b..091e72f52b62f5 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 @@ -20,6 +20,9 @@ return COMPONENT_INCOMPATIBLE attached_mob = parent + // Clearly the Chaplain is VERY pious. + if(is_chaplain_job(attached_mob.mind?.assigned_role)) + max_piety *= 2 // If your old system used signals, register them here. RegisterWithParent() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm index 7130702ab43988..ab181ee3164acc 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm @@ -6,7 +6,7 @@ Entropic Mending removes wounds (sometimes) and speeds up the target's metabolis name = "Entropic Mending" desc = "Entropy's a long road, a few steps further along it will do you more good than harm. Spend 5 Piety to touch another humanoid and attempt to restore it's lingering wounds. \ Moderate wounds will be healed automatically; all other wounds have a random chance to depending on severity. \ - Invoking this power will cause temporary, lingering entropic effects; such as increased metabolism, hunger and blood replenishment, at triple pace." + Invoking this power will cause temporary, lingering entropic effects on the target; such as increased metabolism, hunger and blood replenishment, at triple pace." action_path = /datum/action/cooldown/power/theologist/entropic_mending value = 6 @@ -18,16 +18,16 @@ Entropic Mending removes wounds (sometimes) and speeds up the target's metabolis name = "Entropic Mending" desc = "Entropy's a long road, a few steps further along it will do one more good than harm. Spend 5 Piety to touch another humanoid and attempt to restore it's lingering wounds. \ Moderate wounds will be healed automatically; all other wounds have a random chance to depending on severity. \ - Invoking this power will cause temporary, lingering entropic effects; such as increased metabolism, hunger and blood replenishment, at triple pace." + Invoking this power will cause temporary, lingering entropic effects on the target; such as increased metabolism, hunger and blood replenishment, at triple pace." button_icon = 'icons/mob/actions/actions_cult.dmi' button_icon_state = "manip" cooldown_time = 150 target_range = 1 target_type = /mob/living/carbon/human click_to_activate = TRUE - target_self = TRUE // Make false in final version, for testing. + target_self = FALSE unset_after_click = TRUE - cost = 5 // TODO PROGRAM IN PIETY COST LOL + cost = 5 // Current instance of the status effect var/datum/status_effect/power/entropic_mending/active_effect @@ -82,30 +82,26 @@ Entropic Mending removes wounds (sometimes) and speeds up the target's metabolis // So given it is 'part of the effect', we actually handle the wound removal on here. /datum/status_effect/power/entropic_mending/on_apply() victim = owner // Whilst I would like to set it on_creation, it doesn't always pass it along 4somerasinss. + playsound(owner, 'sound/effects/magic/staff_healing.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) // Attemps to remove wounds for(var/datum/wound/wound in victim.all_wounds) switch(wound.severity) - if(WOUND_SEVERITY_TRIVIAL || WOUND_SEVERITY_MODERATE) - wound.remove_wound() - to_chat(entropic_mending.owner, span_notice("The restorative energies manage to treat the [wound.name]!")) - to_chat(victim, span_notice("Your [wound.name] got healed!")) + if(WOUND_SEVERITY_TRIVIAL, WOUND_SEVERITY_MODERATE) + handle_wound_heal_success(entropic_mending.owner, victim, wound) wounds_treated++ if(WOUND_SEVERITY_SEVERE) if(prob(60)) - wound.remove_wound() - to_chat(entropic_mending.owner, span_notice("The restorative energies manage to treat the [wound.name]!")) - to_chat(victim, span_notice("Your [wound.name] got healed!")) + handle_wound_heal_success(entropic_mending.owner, victim, wound) wounds_treated++ else to_chat(entropic_mending.owner, span_warning("The restorative energies fail to treat the [wound.name]!")) if(WOUND_SEVERITY_CRITICAL) if(prob(30)) - wound.remove_wound() - to_chat(entropic_mending.owner, span_notice("The restorative energies manage to treat the [wound.name]!")) - to_chat(victim, span_notice("Your [wound.name] got healed!")) + handle_wound_heal_success(entropic_mending.owner, victim, wound) wounds_treated++ else to_chat(entropic_mending.owner, span_warning("The restorative energies fail to treat the [wound.name]!")) + // Feedback to user if(!LAZYLEN(victim.all_wounds)) // Not necessarily bad, you might use this for it's metabolize effect. to_chat(entropic_mending.owner, span_notice("[victim.get_visible_name()] has no wounds to treat!")) else if(wounds_treated <= 0) @@ -122,6 +118,13 @@ Entropic Mending removes wounds (sometimes) and speeds up the target's metabolis return TRUE +// Just there to quickly handle wound-healing. +/datum/status_effect/power/entropic_mending/proc/handle_wound_heal_success(caster, mob/living/victim, datum/wound/wound) + new /obj/effect/temp_visual/heal(get_turf(victim), "#cf2525") + wound.remove_wound() + to_chat(entropic_mending.owner, span_notice("The restorative energies manage to treat the [wound.name]!")) + to_chat(victim, span_notice("Your [wound.name] got healed!")) + // Sets the link with the original action /datum/status_effect/power/entropic_mending/on_creation(mob/living/new_owner, datum/action/cooldown/power/theologist/entropic_mending/passed_power) entropic_mending = passed_power diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm new file mode 100644 index 00000000000000..7e7f322d4dc61c --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm @@ -0,0 +1,179 @@ +/datum/power/theologist/smiting_strike + name = "Smiting Strike" + desc = "Channel energy into the item you are currently holding. Your next attack that hits with it against a creature deals 15 additional burn damage and sends them flying backwards 4 spaces. \ + This knockback cannot stun or damage on impact. Costs 5 Piety to use; recast to cancel. This effect ends if the item leaves your hands." + action_path = /datum/action/cooldown/power/theologist/smiting_strike + value = 5 + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THEOLOGIST + required_powers = list(/datum/power/theologist_root/revered) + +/datum/action/cooldown/power/theologist/smiting_strike + name = "Smiting Strike" + desc = "Channel energy into the item you are currently holding. Your next attack that hits with it against a creature deals 15 additional burn damage and sends them flying backwards 4 spaces. \ + This knockback cannot stun or damage on impact. Costs 5 Piety to use; recast to cancel. This effect ends if the item leaves your hands." + button_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "sword_fling" + cooldown_time = 150 + cost = 5 + + // How much damage the smite element will do + var/smite_damage = 15 + // How much distance the smite element will knock back + var/smite_knockback = 4 + //If the upgrade to imbue multiple items is unlocked. + var/can_imbue_multiples + // If it singular, which one is the one currently imbued? + var/obj/currently_imbued + +/datum/action/cooldown/power/theologist/smiting_strike/use_action(mob/living/user, atom/target) + var/obj/item/potential_smite = owner.get_active_held_item() + if(!potential_smite) + if(owner.get_inactive_held_item()) + to_chat(owner, span_warning("You must hold the desired item in your hands to imbue it!")) + else + to_chat(owner, span_warning("You aren't holding anything that can be imbued!")) + return FALSE + + var/link_message = "" + if(potential_smite.item_flags & ABSTRACT) + return FALSE + if(SEND_SIGNAL(potential_smite, COMSIG_ITEM_MARK_RETRIEVAL, src, owner) & COMPONENT_BLOCK_MARK_RETRIEVAL) + return FALSE + + to_chat(owner, span_notice(link_message)) + if(!can_imbue_multiples) + imbue_singular(potential_smite) + else + imbue_global(potential_smite) + return TRUE + +/datum/action/cooldown/power/theologist/smiting_strike/proc/imbue_singular(obj/to_imbue) + if(currently_imbued) + currently_imbued.RemoveElement(/datum/element/theologist_smite) + var/thingholdingimbued = currently_imbued.loc + if(ismob(thingholdingimbued)) + to_chat(thingholdingimbued, span_warning("The smiting energies leave your [currently_imbued]")) + currently_imbued = null + currently_imbued = to_imbue + currently_imbued.AddElement(/datum/element/theologist_smite, smite_damage, smite_knockback, FALSE, TRUE, TRUE) + to_chat(owner, span_notice("You infuse smiting energies into [currently_imbued]")) + +/datum/action/cooldown/power/theologist/smiting_strike/proc/imbue_global(obj/to_imbue) + to_imbue.AddElement(/datum/element/theologist_smite, smite_damage, smite_knockback, FALSE, TRUE, FALSE) + to_chat(owner, span_notice("You infuse smiting energies into [currently_imbued]")) + +// Whilst I originally considered adding just the knockback element, we kind-of want more control over when the smite fades. +/datum/element/theologist_smite + /// extra damage the smite does + var/smite_damage + /// distance the atom will be thrown + var/throw_distance + /// whether this can throw anchored targets (tables, etc) + var/throw_anchored + /// whether this is a gentle throw (default false means people thrown into walls are stunned / take damage) + var/throw_gentle + /// whether dropping this item ends the element + var/self_terminate_on_drop + // The person assigned to be the holder of the object. + var/mob/living/holder + // the glowy effect + var/mutable_appearance/target_glow + // The attached item + var/obj/item/attached_item + + +// This is basically the knockback code but hybridized. Sue me. +/datum/element/theologist_smite/Attach(datum/target, smite_damage = 1, throw_distance = 1, throw_anchored = FALSE, throw_gentle = FALSE, self_terminate_on_drop = TRUE) +// While the balancer inside me suggests we restrict this to melee hits... I kind of want to see the fun of ranged smites. +// For the future person to balance this; really just remove projectile_hit() and the first if in this sequence if you want to axe ranged. + . = ..() + if(ismachinery(target) || isstructure(target) || isgun(target) || isprojectilespell(target)) // turrets, etc + RegisterSignal(target, COMSIG_PROJECTILE_ON_HIT, PROC_REF(projectile_hit)) + else if(isitem(target)) + RegisterSignal(target, COMSIG_ITEM_AFTERATTACK, PROC_REF(item_afterattack)) + else + return ELEMENT_INCOMPATIBLE + + src.smite_damage = smite_damage + src.throw_distance = throw_distance + src.throw_anchored = throw_anchored + src.throw_gentle = throw_gentle + src.self_terminate_on_drop = self_terminate_on_drop + + attached_item = target + if(isitem(target)) + attached_item = target + RegisterSignal(attached_item, COMSIG_ITEM_DROPPED, PROC_REF(on_item_dropped)) + + // Applies the glowing effect + target_glow = mutable_appearance( + icon = 'icons/effects/effects.dmi', + icon_state = "blessed", + layer = attached_item.layer - 0.1, + appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART + ) + attached_item.add_overlay(target_glow) + +// Checks if the item is no longer in our hands. If so, remove this element. +/datum/element/theologist_smite/proc/on_item_dropped(datum/source, mob/user) + SIGNAL_HANDLER + if(self_terminate_on_drop) + self_terminate() + return + +// Said removing this element. +/datum/element/theologist_smite/proc/self_terminate() + Detach(attached_item) //Have to do it like so to make sure we de-bless guns too. + +// Prevents signalers from loitering. +/datum/element/theologist_smite/Detach(datum/source) + UnregisterSignal(source, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_POST_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT)) + if(attached_item) + UnregisterSignal(attached_item, COMSIG_ITEM_DROPPED) + + if(target_glow) + attached_item.cut_overlay(target_glow) + target_glow = null + + attached_item = null + holder = null + return ..() + + +/// triggered after an item attacks something +/datum/element/theologist_smite/proc/item_afterattack(obj/item/source, atom/target, mob/user, list/modifiers) + SIGNAL_HANDLER + + if(!isliving(target)) + return + on_hit(target, user, get_dir(source, target)) + +/// triggered after a projectile hits something +/datum/element/theologist_smite/proc/projectile_hit(datum/fired_from, atom/movable/firer, atom/target, Angle) + SIGNAL_HANDLER + + if(!isliving(target)) + return + on_hit(target, null, angle2dir(Angle)) + +// The on hit effect +/datum/element/theologist_smite/proc/on_hit(mob/living/target, mob/thrower, throw_dir) + //Knockback code + if(!ismovable(target) || throw_dir == null) + return + if(target.anchored && !throw_anchored) + return + if(throw_distance < 0) + throw_dir = REVERSE_DIR(throw_dir) + throw_distance *= -1 + var/atom/throw_target = get_edge_target_turf(target, throw_dir) + target.safe_throw_at(throw_target, throw_distance, 1, thrower, gentle = throw_gentle) + new /obj/effect/temp_visual/kinetic_blast(get_turf(target), "#ddd166") + playsound(target, 'sound/effects/magic/magic_block_holy.ogg', 75, TRUE) + target.adjustFireLoss(smite_damage) + to_chat(target, span_userdanger("You are knocked back by a burning, resonant energy!")) + self_terminate() + +// Metorimpact.ogg in sounds/effects. diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index bf0a7cac7f87bf..54a2d667c09ff8 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -44,6 +44,7 @@ if(!can_use(user, target)) return FALSE if(use_action(user, target)) + on_action_success() return TRUE return FALSE @@ -79,6 +80,10 @@ /datum/action/cooldown/power/proc/use_action(mob/living/user, atom/target) return TRUE +// Anything that should happen as a result of use_action returning TRUE. +// Cost systems for archetypes to name an example. +/datum/action/cooldown/power/proc/on_action_success(mob/living/user, atom/target) + return /* Handles all the logic involved in using a targeted, click-based action. - First press: enables click intercept (targeting mode) diff --git a/tgstation.dme b/tgstation.dme index 63c7ae5fc6bbdc..7953ae7bcb2341 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7443,6 +7443,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_revered.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_piety.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\entropic_mending.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike.dm" #include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" From 24c9d4dc66d18acea4aae04de6e56755a3e9a612 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 28 Jan 2026 19:44:08 +0100 Subject: [PATCH 033/212] Fixed bugs surrounding smiting strike, updated defines. --- code/__DEFINES/~doppler_defines/powers.dm | 8 +++-- .../sorcerous/theologist/_theologist_piety.dm | 5 +-- .../theologist/_theologist_root_revered.dm | 4 +-- .../theologist/_theologist_root_shared.dm | 4 +-- .../theologist/_theologist_root_twisted.dm | 6 ++-- .../sorcerous/theologist/smiting_strike.dm | 31 +++++++++---------- .../theologist/smiting_strike_upgrades.dm | 18 +++++++++++ tgstation.dme | 1 + 8 files changed, 49 insertions(+), 28 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index e752e68f12c0cb..0ca08b3e2199d7 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -82,14 +82,16 @@ */ // How much root abilities should heal (max), if they heal. -#define ROOT_HEALING 30 +#define THEOLOGIAN_ROOT_HEALING 30 // Healing equates to this much piety. -#define PIETY_HEALING_COEFFICIENT 0.2 +#define THEOLOGIAN_PIETY_HEALING_COEFFICIENT 0.2 // Maximum amount of Piety -#define PIETY_MAX 50 +#define THEOLOGIAN_PIETY_MAX 50 +// Trait made as to prevent duplicate smites. +#define TRAIT_HAS_SMITING_STRIKE "has_smiting_strike" /**MORTAL DEFINES * I'm literally just using this to define Breacher Knuckle right now 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 091e72f52b62f5..0dbeaf54f7fd37 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 @@ -9,7 +9,7 @@ // Whatever state your old attached_theologist_piety tracked: var/piety = 0 - var/max_piety = PIETY_MAX + var/max_piety = THEOLOGIAN_PIETY_MAX // The UI itself var/atom/movable/screen/theologist_piety/theologist_ui @@ -43,7 +43,8 @@ /datum/component/theologist_piety/UnregisterFromParent() // UnregisterSignal(attached_mob, list(COMSIG_..., COMSIG_...)) . = ..() - UnregisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED) + if(attached_mob) // prevents runtiming when adding/removing duplicate components + UnregisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED) /datum/component/theologist_piety/Destroy() UnregisterFromParent() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index 33d606e03d3162..c72ee2682971d9 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -63,7 +63,7 @@ // The power responsible for this, so we can make sure it properly gives piety to the caster var/datum/action/cooldown/power/theologist/theologist_root/revered/burden_power // The maximum amount we will heal - var/healing_max = ROOT_HEALING + var/healing_max = THEOLOGIAN_ROOT_HEALING // How much we have healed already var/healing_done = 0 // How much we heal per tick. @@ -146,7 +146,7 @@ // QDEL destroys burden_power /datum/status_effect/power/burden_revered/proc/expire() - var/piety_gained = max(0, floor(healing_done * PIETY_HEALING_COEFFICIENT)) // TODO: defines + var/piety_gained = max(0, floor(healing_done * THEOLOGIAN_PIETY_HEALING_COEFFICIENT)) // TODO: defines // Report back BEFORE deletion starts if(burden_power) burden_power.effect_expired(piety_gained) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index 198be82d6b3a08..86e60dd5035d0c 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -250,9 +250,9 @@ // Piety buildup increases/deductions if(taker == owner) - piety_buildup += amount * PIETY_HEALING_COEFFICIENT + piety_buildup += amount * THEOLOGIAN_PIETY_HEALING_COEFFICIENT else if(giver == owner) - piety_buildup -= amount * PIETY_HEALING_COEFFICIENT + piety_buildup -= amount * THEOLOGIAN_PIETY_HEALING_COEFFICIENT return diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index 359546e1b899c7..f2b04b4b9fdd6a 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -17,7 +17,7 @@ Gives Piety proportional to the amount of damage twisted." button_icon = 'icons/mob/actions/actions_cult.dmi' button_icon_state = "hand" - cooldown_time = 600 + cooldown_time = 300 target_range = 1 target_type = /mob/living click_to_activate = TRUE @@ -25,7 +25,7 @@ unset_after_click = TRUE //How much we can heal max with twisted per use. - var/healing_max = ROOT_HEALING + var/healing_max = THEOLOGIAN_ROOT_HEALING //Tracks how much healing we did throughout the proccess. var/healing_done = 0 @@ -77,7 +77,7 @@ QDEL_NULL(current_beam) // Handles piety gain - var/piety_gained = max(0, floor(healing_done * PIETY_HEALING_COEFFICIENT)) + var/piety_gained = max(0, floor(healing_done * THEOLOGIAN_PIETY_HEALING_COEFFICIENT)) // resets for next time healing_done = 0 damage_done = 0 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm index 7e7f322d4dc61c..8616a8b8dcb51f 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm @@ -36,13 +36,15 @@ to_chat(owner, span_warning("You aren't holding anything that can be imbued!")) return FALSE - var/link_message = "" + // In order to detect our buff, we pass along a trait to the host item. + if(HAS_TRAIT(potential_smite, TRAIT_HAS_SMITING_STRIKE)) + to_chat(owner, span_warning("The item is already imbued!")) + return FALSE if(potential_smite.item_flags & ABSTRACT) return FALSE if(SEND_SIGNAL(potential_smite, COMSIG_ITEM_MARK_RETRIEVAL, src, owner) & COMPONENT_BLOCK_MARK_RETRIEVAL) return FALSE - to_chat(owner, span_notice(link_message)) if(!can_imbue_multiples) imbue_singular(potential_smite) else @@ -51,10 +53,9 @@ /datum/action/cooldown/power/theologist/smiting_strike/proc/imbue_singular(obj/to_imbue) if(currently_imbued) - currently_imbued.RemoveElement(/datum/element/theologist_smite) var/thingholdingimbued = currently_imbued.loc if(ismob(thingholdingimbued)) - to_chat(thingholdingimbued, span_warning("The smiting energies leave your [currently_imbued]")) + to_chat(thingholdingimbued, span_warning("The smiting energies leave [currently_imbued]")) currently_imbued = null currently_imbued = to_imbue currently_imbued.AddElement(/datum/element/theologist_smite, smite_damage, smite_knockback, FALSE, TRUE, TRUE) @@ -85,17 +86,18 @@ // This is basically the knockback code but hybridized. Sue me. -/datum/element/theologist_smite/Attach(datum/target, smite_damage = 1, throw_distance = 1, throw_anchored = FALSE, throw_gentle = FALSE, self_terminate_on_drop = TRUE) +/datum/element/theologist_smite/Attach(datum/target, smite_damage = 1, throw_distance = 1, throw_anchored = FALSE, throw_gentle = FALSE, self_terminate_on_drop = FALSE) // While the balancer inside me suggests we restrict this to melee hits... I kind of want to see the fun of ranged smites. // For the future person to balance this; really just remove projectile_hit() and the first if in this sequence if you want to axe ranged. . = ..() - if(ismachinery(target) || isstructure(target) || isgun(target) || isprojectilespell(target)) // turrets, etc + if(isgun(target) || isprojectilespell(target)) // turrets, etc RegisterSignal(target, COMSIG_PROJECTILE_ON_HIT, PROC_REF(projectile_hit)) else if(isitem(target)) RegisterSignal(target, COMSIG_ITEM_AFTERATTACK, PROC_REF(item_afterattack)) else return ELEMENT_INCOMPATIBLE + ADD_TRAIT(target, TRAIT_HAS_SMITING_STRIKE, src) src.smite_damage = smite_damage src.throw_distance = throw_distance src.throw_anchored = throw_anchored @@ -103,8 +105,7 @@ src.self_terminate_on_drop = self_terminate_on_drop attached_item = target - if(isitem(target)) - attached_item = target + if(isitem(target) && self_terminate_on_drop) // No point tracking this if we aren't going to self_terminate on drop RegisterSignal(attached_item, COMSIG_ITEM_DROPPED, PROC_REF(on_item_dropped)) // Applies the glowing effect @@ -120,23 +121,21 @@ /datum/element/theologist_smite/proc/on_item_dropped(datum/source, mob/user) SIGNAL_HANDLER if(self_terminate_on_drop) - self_terminate() + Detach(attached_item) return -// Said removing this element. -/datum/element/theologist_smite/proc/self_terminate() - Detach(attached_item) //Have to do it like so to make sure we de-bless guns too. // Prevents signalers from loitering. /datum/element/theologist_smite/Detach(datum/source) UnregisterSignal(source, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_POST_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT)) - if(attached_item) + if(attached_item && self_terminate_on_drop) UnregisterSignal(attached_item, COMSIG_ITEM_DROPPED) if(target_glow) attached_item.cut_overlay(target_glow) target_glow = null + REMOVE_TRAIT(source, TRAIT_HAS_SMITING_STRIKE, src) attached_item = null holder = null return ..() @@ -149,6 +148,8 @@ if(!isliving(target)) return on_hit(target, user, get_dir(source, target)) + Detach(source) + /// triggered after a projectile hits something /datum/element/theologist_smite/proc/projectile_hit(datum/fired_from, atom/movable/firer, atom/target, Angle) @@ -157,6 +158,7 @@ if(!isliving(target)) return on_hit(target, null, angle2dir(Angle)) + Detach(fired_from) // The on hit effect /datum/element/theologist_smite/proc/on_hit(mob/living/target, mob/thrower, throw_dir) @@ -174,6 +176,3 @@ playsound(target, 'sound/effects/magic/magic_block_holy.ogg', 75, TRUE) target.adjustFireLoss(smite_damage) to_chat(target, span_userdanger("You are knocked back by a burning, resonant energy!")) - self_terminate() - -// Metorimpact.ogg in sounds/effects. diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm new file mode 100644 index 00000000000000..4e7057a2063dec --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm @@ -0,0 +1,18 @@ +/* Simple example of an upgrade, though more like a sidegrade here. +Most of the effects are already baked into the existing power for convenience. +*/ +/datum/power/theologist/smiting_strike/imbue_armaments + name = "Imbue Armaments" + desc = "Changes Smiting Strike to no longer be removed when it passes hands, and allows you to have an unlimited amount of items blessed. Reduces the smite effect's knockback by 2 and damage by 5." + value = 5 + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THEOLOGIST + required_powers = list(/datum/power/theologist/smiting_strike) + +/datum/power/theologist/smiting_strike/imbue_armaments/post_add() + var/datum/power/theologist/smiting_strike/smiting_strike = power_holder.get_power(/datum/power/theologist/smiting_strike) + var/datum/action/cooldown/power/theologist/smiting_strike/smite_action = smiting_strike.action_path // I really should find a better way to get the variables of actions. + smite_action.smite_damage -= 5 + smite_action.smite_knockback -= 2 + smite_action.can_imbue_multiples = TRUE diff --git a/tgstation.dme b/tgstation.dme index 7953ae7bcb2341..dc1372710b9339 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7444,6 +7444,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_piety.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\entropic_mending.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike_upgrades.dm" #include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" From f342446c831a830d353dafd89148cf78a4b4b8a2 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 29 Jan 2026 13:10:28 +0100 Subject: [PATCH 034/212] Added a prayer ability for piety generation on quieter days. Changed middleware to allow for subtype requirements if indicated by power. Stray PDA Icons cause I kept runtiming on this branch? Remind me to fish these out during final commit. --- icons/map_icons/items/pda.dmi | Bin 9469 -> 11183 bytes modular_doppler/modular_powers/code/_power.dm | 2 + .../powers/resonant/psyker/_psyker_root.dm | 4 + .../sorcerous/theologist/_theologist_root.dm | 2 +- .../sorcerous/theologist/pious_prayer.dm | 94 ++++++++++++++++++ .../sorcerous/theologist/smiting_strike.dm | 56 +++++------ .../code/powers_prefs_middleware.dm | 27 ++++- tgstation.dme | 1 + 8 files changed, 153 insertions(+), 33 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm diff --git a/icons/map_icons/items/pda.dmi b/icons/map_icons/items/pda.dmi index 62f53f25d34e4dddc9f61045ea38bfcaec7ec4d6..29ba74b1ca34cceace8dc4e479cad20a55701279 100644 GIT binary patch literal 11183 zcmZX4bzD?U6gG&G(jbVGG$N&REl4*apyYx`cS|gbfFj-9-QBsAq|%+z4FbEwF1vi| z_rC9!f82X!=FXXW@BHRI&vVY5P%RBbLVOy03=9lHWhFUnwEg|}z{N(NX}E1SFfcGn zd~`mz%UQUYyV^Lr+c-I5V0_M~OzPBN<$wJ3D7E-|jxoPcPNt;?Y1^ZY-l6!E(C1*0 zvCg#SV(F#TkFhZsO!8rVbRz=H zQk5i@No!w@aSX*gakeV+;Y~_PnwrtOtAB8qFPnXJeKoN;6u64V5imlip-QK=F|C2`3dN zZjeE4bs!Vd2;X<+7i~3<8R24K^4&m69iEZz&_wYu#y~!uq~tHLX2&Hi1svb`8khs6 zpMT_i-Z`Odtv%IXr#ATJbLAc zSBCo&!IS=2j};WZu`A_J@_bLKE zVwo|V^_D}ZyM_Z<+AqtoPp>$uxRQdgF>YFKm#9vej-#W(dyFys1%Hrifdzkb+dy9W z(E0<$ZF-MZ@es^gA1COfNoBIvfoJ)eVnmSaJ-Mw$OwM2=sSE!;K{pwlhq?p=(xaH| zj&>qGxl-MT*X)X=1Xzr8zM_0k|4Vb~Hayi|eX4A$`wLdxreYK=C#OaOPDE7uRW!HY zn()&^63%z);%nEVCBe9<@U`=<;gW|-C%im7I2g2d8wC|UTLXlOE-s>EzNOOu0*v1^ z8~r+f|E7>ez4xSh31(xK$MM+57G=_?wCvCC*s_7G?9$`YL>&gNbS0#w z^ExTDu_JqmHu`pCq*VFJP9+rFOW;02;T)^+ESk=Iy@}=z`-ie=U3m{9BEnc4ao(Jk z+B5cc3iPoaj!GsH1V4Wf*g}WfT3q)fXVw3kr;>&=XDFNC(?rIda5mUz$-{qY((OpP z#K>ZL>E<#=45Hz(=HGyWl8TD{8AGf=Z?c#f*-~W=um>_TD$YN5)-U&N7Um_UaF{#8 zk^pH#X(-w>1M$qvxVJ>o((#)04Zmnjym!a9{mw_yY)H_VT$J_A|Feio`z(~RnZf$@ zLS9i33nN3rs+?WM?f{T}!Kxkq#g_<{TnO>|e3jUq^A3xC9mD7ZNd$0*MJ;__70nWN zy$5ZYMI~4R)P2>Q71dupz1>1wHQiN%8ripV=jE=(Quc5B&n$$r_n* z1mwd=?kqHom46Reb|aR~BeK~Qtv<2camIcgO`PuoLBX(mAji9PCNki}0@EjJTLq^x zpU7=tQ=7PJrONJc7m30A75`EJVd1_5cWv=UgsdQaDw1eRunO(^>)UFEQJU-5AkC;e zmp72}e1KdL3im6#v+0y^PIcv_Q-|5Qar?3b0<@voV?9X&|8g3LJ(cW3rQ>}13hm?w zM=wS4>^tK>IJTAj&uZBfg+FRWo$4_l=ir;L)ked4uX#t~Gvcp4m5HAlmU$`9o{>n8 z3dSth)FcJRb>$gaudP-lR5f>1nS@4I!W|=FtwolbO#qrB8iCp3Reuyk#C(wlJ~ckv zETBPVI+8H*=8T+}yjtO=Jr`ZEwzsB`ob!^poGy?^wzajEi(~+y98KTJGN-ZZBgUfR zU#V3LpL{zfjwXlQw&$ul3a70&PMY725f`lbvkoCiC(^Vg-_KzAurBwD6b|e{6Syygt%1dX4^M)Y;fEZ82Q}>t^ES1)%X}H1oGtPN_ z1K`nT<{dGOBRfea>t^5>MY^dPj;#~i#>OV@<0HJA)kM3Xp%&^MC2C*?d1Qk78nf-^ z0wb%7dA1D~{=itquFv2}C19IvU{}J?OVpAY0~I$l-d4C*UToZ$uN+}?!jan23A+p2 z9S?)?X?H{F^+&{%LrT+UF$`05;rnBPzJ&Q=?~&;znuAw`%UVW8QPah$&V!j@pCmI{ zF2H`B&)m45yqrCF+arjH(RKvo+jp-K6!sYjZ)HQbx?HIkHd{oyP9mt8^!QAxZ}aW3I^--%m+5@&Yx6b&pF(d z72A<4mOnGf-?dR}M5Mau0=zQPGArZJHN5N#9YQ_%(VstOEG#TIZNZk7MR4888uNCG5v^ zqNKTpoxkIrQWBlSGQ)Jhw*7!EsH z!(VvT5mT$Ryffstz%lhqs@&z`_F~|f)595HgLUjnP*MOLt>xGosw>>&o*Z0NY3v{- zY)|}N(}^Hhz+-HA7WUs^$IS;zZ|Fqd=-wCpI4V5^i#yTlf-dK%N)H*t+&Nfa7S{nU zN%fhgW{4Z*X3n=3ADrvLAQYi%>pRV967l+;axEz%!o(-f2Ugn=GBUS~%YM!8-`%vpah}|_RLSPD0iK%q*Dj~w~#Je zvAUuImiyDQV`3s8U!(YNd1Wv>6C>}uImpCZjo6X2@*0YCq~-Nw(kRT;_V<^QVOq2D!`Qo3l)t+Z4Om6x;^)Z2cfC{;uBWHn@=ByP1MfC z$_zx^GuocXFTzfL!D&1!p4=VuP@J1$2a%QeNOKM__5>fiMqX7_w+KNbylRpk0fjw8 zx`sP6n^3~3*Ji6T8#2DY%31#+h9!8Itq&sqoQGdvZr2K~2I)MiPv=9gWQz3q;4r|m zX)Cw@On3E>10lFc+DiuOpn1S}BIS`fGEupO%Yo z0&^kro<)T2(YW7w1eA7TL6sAJAX=Dm+tvE(s9fw3;iG>igzHr3&5!KJgV&%mcphGh z%?#VIX8c)AE#O4Mo~BV*q9#(d9jOLrfGwKiqQB0(`2fT6)4siRudlQ4)DI;%<+EYX zvutS#UCONE%0nbx6eqhLvwla{9)HD66??^8WF6GW_=d@rH}T^n!6Ebrpfpe$op08N zVp6?dfTq7ab7!(I6+;pfcGljfTYsqhS_i!b#|IJ=YV?=h$x>sV%DwG%s4*y9|J7tr z$)Um#Y-TF8;^E<70Bpc%$b2bpVMZB;8Ih71=3Gw6Y+3Bnq%M@6Bk|GmMcm}8*m=)5 z3AYbKjq-z9?=5B0M!(ja6U18AGG2G4$0F8#DK&iH>n^4h@_#;M#50qquVv`2W+eU4 zSCrR#6rJ(S)IbVbQsyrj&F-8c!txLy3D39#BT8kviWO1*M zMf-1y_5(X(t8cSYUUE34T1Y47mWr4h_vZ#0%)(n+TZhKSLq-!H)-OH|mk$YkY8T6A z&i(QU-ODHWIrc*78?LO+0RRScKbc8z<6in_^g$0gt1|*k%BSjmg8X77(iS2f@vNiZ zEr#UTsz-?)No}YJ6Hy);dd*{gv>)(dezdccXZ|6bUp^6~8b0slV(dh5j99%5>R%YI zZkN?+dQZ8LS;$*5b?><*;izL@k=KTxBUeegIgQEu=RAEn(L97b6t&5B@k22 zd|ZhTEQ1o$iS@6#jY|fC!XZ3N13aB)3NoU?!c=Gtkal!`!wzI_c;e{oQDYBMF3;mM z4%2eDIRB}C9V&f4jJYjnCM7Wq)Y41$lju?a>;Ckg*F1hgY7@Gd7zH}Z^ZbGxSk(6FqW=Me1(b4k-^LFYWeLP$Y)%y;X-__@vdr1#xvmLIM&v0he|BOt_ z%!1DoyY1YoybaSEMUdzE3JTK5^=d;Z9i-+QghUHGLU0sRmK`5Yt_AK5#ye2~dOmnX z{rJRm91{&oH4i`WarBbK{6~v%jyqz<-4#QXAPR0WJ;m@D` z4;D;-QuhXvKbX98xMvwy6$0hu7=`yd--y26s@e&II;D0nvMik)5nFeJO9+g?GM$f0 z^-uxL&_#|3AYxvozeN-*!pU*};l>_ujU%w}aK6&6!xZR8u~(9L$Yvg(y$bU_k0uZ0SIIqzzLw8Ymlz5^FI|FfO*%rF0XZ9{l>9}t+lM5Q zl}JpW_f~@yV(8rVqnd+mp%vmpUf8Bg^A|%+_tZrX*b!BIGdWT{paz2SP<*i8p9MPy z;BQV`4RTdFo)02Q<<@4>#Bo~R8l3cVzko56Rx3vI1UyJR8MkvB*n#`J)GpP$!s$2` z&Qjz8cjpfpE_EW`ofWX(V`N;oR*iiw@=`S>=hu1llOYDp10-`+r~tL>gLgCk5J-bJ zxI1dhPF|Rd~o+oDVFGW*yE$u2am;TDN^Th5{0x|w}E00ca zv%xhmc}PLws59oxs-4USo-7&$vQ)+|dh zZ>D@4Og@Ak7Gk1Si^H}%=Bu!D1^&B+|6DvQT+f9B#Yg|(m?T1Ay@;NOn4_R6DWJJ-e0>W8TYo!&yQEI5z+2hWJ(XfLEzE+A`Zdkejii`_ZjG!7493~#fCd=~GrS>FEjiZ*Y4WG2N9j;vDPm5; z7p)z2tbbOUP0>%@02A(_r!|yVlw?$uOZrZy4?L1X!^+3eBt^SaVY#hW{$W{Pieip|o-O-DP$OU!xoJn}ak;4I zbM0-2y<{`dy&CVQ+&)2iHNcJ}2|=*iYX;}n{By3mR=;4HPQK}vdm)?(>c&CVWGW_c zn=_mj)m;E>ZSDS`Prpyb*&dam>{Xy(QI{{YZcNXu;goaPKY}{;{jqLU%P7vS=I^DW zLZM-JyL&*;Mu&C89K0R*`6GNQH@#53pIp6L@x+Qx&Z@s93}4@gQCvTI%kv`TtteE# zmV7_+K|Tdoh#?|R$yD+dar4f^tV1cv`6Kn&XN=lToZS7$5R2I(fm>sd zFwHQWFlk3f_UpX*_vQ_@5cEWaU%}_nk`l)+Mt9Ut+KQd9Dg-sYF9yH*7Zz|P8MKxK zId=}@+0`1ey5{(h6aS*n_p5;P*3<8Mo0toq=suGs2h&txUnX?ba~;zz<20`dQEy4b zys!Rq-ZM1rm0MM%u_K-{S_lR|c<+DW*6Z1+z*Mk&xC7#E_;ULfKQ!N>C00gZ zc{hnMVOAiD=3IK0!f9W{*{B%Bv(J3b^YEnt^m>uEXnk*sT-qh~ussK5%?)nTFfV3o zx@I(55K~O@Z)V@MaiyAJ$lV-QG|c{Y*zDC{Gc>o|VWvt=7nf8Y@>ny18|GRFXp|s= zr-4o10LD4(IbWcLTRherHz{Irn8WXDd_H{zs!~u_Cp1zHicLwu4d)>W4Gqn52(N9=$w1fN zPnpil%;dC*iJle`)j{1>B>)+KvZdoDU8`1bzTFt>V6e*3yXj+Vvt%a_>{m7H8 zuWhzk{>YC%@@rttxi#*$>sXK+x_eBAz_Z9e3tjTIs`#thNa@#w zOR^#LRitBW3Yv({Q3EgvqBGlTuoZ0L)SXa(n~;>`eaEM&I;y2-@|;6cIV`}$DWGv6 z6j;`eB$b~J_C9HxZ{aa+dl+%9=;0xl#BEsA3P{)47MoR?1>Sj(@_QbDgl(&``N5=A zbj!MLjf?Z~kMz27y%D^SYg~SzU9cDaNDut({)YB;aTkc{&~QUWR*!GNJ#=@8Z&h`< zvp}zF-X#|3PMY|h2+EhgDOH!EFI1f@MgBn^oL$wk{ge$Ne`rw&eZ9-8J>!o?yRBsu z9CSvu#iR(}<-j?*T<-$m@H;Lwl*&xLZ==ZlaDn-1H5M(jYGaSbE%~Sg4q$hrkHJk> z1>@J>eyKZd9+|d}9}rm^fQ?x44v! zx^y$aE4e>iYL;z7Bh~v(+J$VFHGFSW1e&1HddZgv4Q|1#|2N{rW(1s7n<&jbIV{)2 zS_k0^r32Swbit^*TOAk>h}0~DhhIAqDH{s>3wZ)CS#DBR$!{DIf3l>Z%d{8NeeLYH zA!@Lvw-9t*;b68w?rr|~L9%rou%Rbc>3(3bHf`U^!h#fb-}FZW|;@|*y?+yr^X>z6oe8pMY68nL_>ikoDSVMV> z{(y%A=-Q&gZdP5yYIGg!Ebqa^)1u36Hi?Zh&#ArmVL=r(TNclUPQZkJCxK7zoG`Yd z3N$*^wH&RjwK9X3i3Z%>*3NU58SaTxi{Sxiwff~@`z#Cdd%tl%+HcT>e1-2Mqerhn za6v4j$~ICznb3l-}>EjW9f zMRfV+@da_9SA-5bL+Rb!nQq;t6Mea-D9`i3BqO?gV;#=`YIEUa-=+|r=fu?SRch`y z(fNUbULuT}@nMz8pv3Hs<3)O3U#A3}1sS2&Nn_(lNwbY!{@hL;^~G%M=k-6nYGBPC zB#Vqaz#w{m7O(4Mj|}2)$@mdTU8Ir?^d5C=_Pqj~=R@|(1q1}F>ulevdt+kc+-=kA z!{AY$FH}~KbPs@HdJix-ko&f0dM|QK?yUx!W(3ztRDe-{Tw!e_Uh2QnDrld$g!=;4 zXk1c^Y|K7r#sOV1X8k9+|EJ&sg~7w@E=S#(uj+oIWnV~~)FsX*xLS6eR6F)xjs72w zabSAC>G?O3{?GCMwZ}Xh|M76(E-|jP_;bB3DGTr$i}-!P>9chRKo`lPKD%?o{%$Tg z%$oRf(KeIHi{t&L=F;$#Q|mGwS?x@EWe*<#-DDZyyXPFm(Fajk{MhJebSc`-Lrbmb zzQK%}#mzU#GOz`x?Q5PvRG@mbkZ;qoD)J=Z&ZP*Rpu@kj+c!9~&L-t{AU9gD(&C>o zXTeh;7>hPlR?D8ECR$nO29kMV)~I?Bw}W6?P)HwU@pNcMo8Mlg95_WOg6_VENnV|&G=Qov$97!1+`qULO;KwFY8e5 zmCjX5e8x_WN;LftNIJ}*AsbjvmJc+FoaC)iKo^Bu${+1)6BnDIucABUT>O4I3-17! zy_(nrSvUIJar9N{rPvF}EZPe(KaDxotmqf1jWm8}f4A5HD`Hph7^2JzRx*L1h6tKhV_54W`W*}&u z@-gE;76XS`%Sez_jr-3Ep%YC`8(o3*LR{6C+gYro5WcrF|2usBH;RQ6;ub%aB*wVe zruLp>L1SqCKf-aVox;pWpZH@267DLaj}D9(!{TsB-DR-UN zg%SN5m(B^s&KHMHDcbxK!=|SVq1=7`i(&Vx*bR9@6x06VZqM~MN1WSB;^@dJbVRGj zcZX-O9A9}meNS*>_h+;Go=OMY6fF#3(7Hd@Kf~4q|LTbRvJ5#~%sCuSea)VOT57Zl zD!achT*QREf2l@Xh6>EIdvM4L-u~gaqd9c?rlRszHT`i9R^)iEtU+ub{`*h~D3975-DN`pLC>mg_iQ+u&c5*O@e}64FsW(Vm{>Ts0X<5!)hkM%?0kX}RLL?4YEq99swh-}`Ux z?Twy_Vv&ZS+|{zH{BaTg$U=z9($n;^uil11`t_DQpI$wGp#s`c%)r3FYx=tvV6Ang z;Qv9g!y3cHx|=jV(iDL6 zm=Y{jr1?I3^^pY{&NfP_!Z4m!8N-?Ay!<`w63 z0E|^s@FNtM)!5<(qtXBMa}Apv1M4QObrigiz*@C)|JnzZSL!pyQA4(4j96JTXnRMIwoCqQo+lo^AGy z4JomB)|py;DzNgE)-?0B`ouEztp&X>`uz!)Ai~uY-t1hHTuWb98vA;TYZ$#woX0(H zSj|My1!$P(^RrK^SG$R2bAThq?p6h1p8{BSzAn{sfiI($N>U zNXHBh;qUZ7cl8u$l>0~PjTv%#un@n}iEtE>nSTy;2EiRMXaF3g?-*^;&2yF6iGTR^ z)0gVs%wj?>xFTcCRcieDZE~}ry02sG(fVo!hVfEV$cXY&Hu%*SDzA$P^R6HThe zU?EYo)LefiBr;SiL|XfAT2Qt<`)cq2_HT|b&A=70rT*U~D#bBHbSk%W{!*by@~Hz@ ztP;7n{iYr1bZkVc1)gM9MT4Tgf@$;SdNo0m=!hps;J&l+7=J8os8VGJJd2M29L(D$t&eQ~Sz8;;A@E!H>U*px2~eqhMBWU1qZxx_tq4 z>&a8$l=7RR{?`7r@PSvnskjqXb`81@?$KHo@9WpEUWuSufkqhueS~X@Z>s0bjboff zUoQ#QS;;PpEt5EfDgIid;aQ^~&S6#FQa`WBY_>sRWBy)tg6v=%;B}ifFS}f?dU0uB zoNb3kLR8-e&K6n{S@}`RVoP;RS0j_%^SVd^R2=Brlu!;ptH4U$L2oAX7l+fm3$q zzC3&N3Ae1w;O^WXe!#8|JL|aA!1%+$05-O4UhcUrxvNEzQc>aLPvSN&bNhPBHZMos z+c`0n-Q320{ z?0;jJ4#hlAJ4jb!OHun`#o4S@|4JRunEpKdWU zo40@bO=qbCkdp-W<5fCDqCK<@>!n!X2Gf%;*)K#J#Gg-BL}gMn<<{3UG>8|nmKeg| zxq&a7aEqr8Ttaheg&;a0sf`;ssJaq;sidi|9k}Emgp6+U-4=P2rW}~rZb6VSzws{v z80=pk&Z;`EdZ7p0Y|%#Xl+68emr$V-;(gy_?NX_1=ezqZykX{MoX3yAZC4cx_-G1} z=7xeaB^%qY6UwRu>5KshTF_E&ME6#*%%Jku ztr5PKZ+av5l7r)qIGMtsTldwJf1^$o?38fP2F?DA*2aP*e1bSwPMruqrFYvR6Lq!BVB1L8)`Ch>FkVHo2Q z*6FY1@Q!$ICG41XA*u~MF4y_pfBu7IpzFidsYC6;PSxvMYw^y+>>jfXQ&ZY3SScbd zOudzQY8fJv^$vB54|hVz*_m!t=yG=6D#*xSo`d-hkB(e!FDzP&cv$i+!rr#b;zw;s zEVHGnYwd~<4O=UtMw5B%;sSpB*ipj%GxO7z)Uk21H1+zQJKv!jV*LuEp4|B~k+5rK z$-D}H9VIFyoe_eNz-yM3 zzH>YkpG+(ChiRm*{}c{A$X9~Y)bRa2ng@bG-5zFHV3aqJb!rONwR-|E1rK8lbZx9F z*)vD1=O(&{YHzHC=MJUUYLZ-}67M28w|d5V(;KT{w!@OmtFNz*348tewcvfPZ#D4? z$|S!jQ@jOyRDhyu6mFb?%?1CP=%}oK$_vIUR@xw$B3X&=R(wk@?iup5iKfj$#!={v PxERXv8gf-{OauQ1oL8{c literal 9469 zcmaiacT`hL*M1ZfPywYQRiuj`AiV^nN$+r}ks`hK9z~@iNS6RX;DVqKrFWEGrGwM} zp?5+Ffe^mKy}sXi*Z2G5w^q)XlbJoUX3p93?C06%MC)j&klwm~3j_j@syJSa6XJ1gZ}j^a!l$q<#t{yD~)jQ5PQAab1K^LlF8Y9UhY zdZt2jdz(3WqN(8;(EfJlB zUFA967u*Zn8cH+dcBz#sHK6{zZK13Cik8^8(=hQ%GxJ;~Qem!lZ=%S}|KiMOuB09^_eml0htF28_4lFqcV?#YmHw}yH;ic1 z*8>}+w^)jL8K4fa!?@s9-i5M zA@LV-3%imV$y~d!7Y~7cIm85oeY89O+nj*L0Ja`}^+rw&$&9`|HGUg!}5Ygo60=ZsOl1h-kM+$l%uX#XZlr$DfAp zMG+)wFC1=dNuL(%>~95Tm$pc-e)uNKE_q$%T1H!&Tg=*|)StFtw-&0M>Eouar>e=j z>8kssu9iu^%=#-~Nk-Z4870^rg`Ms%CM_uj|JYxLwYU@?h`ABpx}|mxX~G1i1^rk|99}oIKcWckcE#RnV5$S% z{M?2*IH9MTC0MT-XGr~ufP>?cTR=u}hxA;}f6$UKQQ%zmd!D-P$<9@)bRv%^*{kP; z10mu*&oT3#VF;Cg_@jkGqS2wIbDu5}#_z{DM0?!|kv@^3`;?eH+I_6;CKk(Vv5QKW zH_&!zvPX07A`yM`Qq>*R5DklZpAS@Ph~U`Q-R^15SBuitgz0}87K3Ms6g|s(_;iCb zRW0_!e`DlEwxsWfLqg_jchS)mh5HcQ>k$#}DAGe!cZBS~J;CoxU zt?s4$GBn!n{9|SEGGR)RgI=<3bOED-9=S3{(!c*4YA%h&3$uuZKjjg;vMJweLL+Qb z($j5v<&d6bgzU#UP87Xn7Vbs)1rP@jqLiBYd9AMJGAWsF(MFRq8{9i5k92i(M!gy1 zU6m z!=1M`D>l#SOEl@yAr8gA`JQoB@1Du7A2nlv`|4N&FUGQ;+Ig38c{n^=2-^u5 z?#s@P6{3_?VV@7V*!#(hgEa0R+M);84%W6{auE^AKE4QJ?fGloZyL><)|y7UN)nNQTUypl zr#^v$3@5k{?Jq7ZMW^~tOr6M@5xP2%kmCH@aPM@-V9drh%xu>jg4TE%rWlwQzxz8}BcwC~Pqu zfA$}Xb=&&Zb?&IQt1cqGsk`!SFyW@C_~tO{>pg5(R!Ud<{VXjycvw>Rnu-ELmey18 z?7SIt2sqL2JoWi*!mEi}RLyzJLoz*^OvermtC-C>^75N1GWTJyy);`=Ev{O-`D@cV z_9I$li1%Z+C&nj8v{+R4QUlHQOeV7xg20Jmx5b^e2#d7qEKduLbA0C{3NAQ~!-3iV z+(6wBD1!Z|#1H^aG5S3_=9OLeL+ zi-SWohMIk0aclA>fM(%bR?;gu4h03?(7Xtsp9X%KU0m|nK6_NrEcw$Bd1KWD^}1h6 zh=HTw#s9~4dWUPH_1!a&963lIUdJ+H?9w*&aufstE)*Pe?s7JXjvMCGyWFov{Ei3@ z*E6rxiL%0Qx7PcdoLvg~YV`}lzCSBcp$8~M)aZuQ(1y;~Uwga)sQcf5+TG)NCrI~W zUf%1V-#dQn92^fGJ|yVr=>ffpi@O85o06QIY$XcQwY1zV*-~$3*4gr3TW-F+K9(U+ zBSShqJr7Ab=V#qHN%q;RBmy?~<{2D9B&V}#Q#qk;PT+;OY*{_47b=r~A`=%JHxab| zljv%w{}>ghCBE97w0k(5BCa-%Lcs$*zyfwBIXxXDJyN7=y1@qzFh9IoMAa4bm$(BH z5}->;Zh-%RRY&0;i}WR_7mw^$i<|evIUXu-`SP|_Ft703?~h4KO3G7LSJxlIr=(CF zSL7Y2vWw7Hg&k&tqY9YgOfmBz6snrarO1$?xv&LgHrERWCQJj8BX^>&%I>efxcSYw z$Jh^MJGe%EE!&m}WNJl5hdkx@L=(NnFZ*F+WTf`=G(enj+4&+d@h%lLwW^x5rQ|B9 zr?dL*tBx54W9&Am-GJrS9UoifY4#C@Ut{7}>Ep94#QCy-MZEzHoMYiJzum8A#@jUf zl$1)}32m+OxCx|gSXt+BO%MnP>ZU9cY77J}`jOHQ(e#CGa z)@NijXJLP_?FOy~zI+!Y`!S1~n?=H_T>!^Y$i&q-*WmiurFlL13DPL6L#a|sElHqgEFtTpe()^y;p@Pfyo(JMjq#T%D5AOFE4)Ky6zj`I>a#C)cvOHZrIPa+Y7k zn$IY}6TdBmo0lg)g!P}QI&p&*a+WWXs09j_+{GHw)|NlS3&aQ*)ul^BzTaHGiD zv>M~|B%7~d*7e7uC$E<~gjBv5tNNfnl%&Gv1Ioc?;KL}vaS1W&G-*{Wy}vWX_HG`I zn0Q_PB^8V!3Ye?+4MWkcQc>D8f5g0VX?D1twq^(d@iYBOt(3PU=)|~}%xD=^!BQma zAKxTrj8(LAJ(GxV%>o+Gz>^dGOwo>tYcxGHm|a&oBRV?C;P{nZoIH-F(;heR%h((S&zV7v zhP!785h9D?QU@+&U-F4+(J$3pIF+3UhrKwY9{0hhpe(E%`UeJ2m%9f^MDZYxys4K`4xp7824BP!jNpk8#r* zc99~mxyg)ZRDf-sOIZ6uO5KU-cqjbVd7J&EesbGGNEY^*-7#~1=B3&$!C#CY_+c&L zY)pt{+1$*_{zy*tJxbYNo_Fv@1l?eWgVRBXArL1Q20KsVw389FGsIL-(N6o2%#dNi zS!ruwq2y&L7hBG`HvK2lO`E9ry`Sh;V90R zre1zJHM;vOdmtj0dh^scrh)D7+~Y64!1B|NNUkNd@1(=LEyQh14Ifyfh-qSvhFxMi z!g0T)>2|xrE2^uN4wZs`OC7`2|C=%BK>G(JyU5;(pCHi*CC(OhTVj_di)Moe|H_ z-~@i=&J2Q=i_pgxHC(KZ9^ED)XFj@pIj-ft035ZayD#fPhE+f63C}Mt?HQaf8w}T3^QncI zjAt==i|#J5H5}7^oe@V93;LZbE{1z)hKY*y=M==WcudWh`<%Ol`$Er-xdl)9W+F&> z$EmRa&K-P)IF{AHHDyI5o)Va2)zW;Gv*CfJ*_xNCzv8Wlj(AIjxW`i8@eEu$&O)#nmK(CaAP)pm$6uFN_Z(#h&HD54=5lP3oe~hTK20y9 zMC?)g*Di}?mnE$H%(owNf6#m^hyj&faOb z7Q*|-?Aq>u5LbVG=(}XEVoh0hTq`tbaGQde#?L_SG>IaS7Rey#Vu>4WRd~8_+!(z_ z%COn3&AP78Ci3TRmMv5|)Z;T}Q;yg{la(*Q)HP94#ALzbA!oDL*J2Lx29<~#jw4%v zlt>0cL&JNQ%#@V*s))&~5EBw&(4%>?-NVU%soQ-Qn}a7KFqX6BmP536*veq&6oxT- z3pr-TujPJbec}Sgf!$}4OPfL!IlXIti_bTF1 z0os)_#+szNT~U|s+Q$rTHLvS<)ZQL)q}qJ1Q81zG@pS9En##u}t9awGeP3d1g!Azp<6qD9x}%Pvo*R z#%UFMKxBT2kGDiD_rJ96_pHgNgNA0%#lMQkr*6+U zYh_4ScibUAMQ_oX_i@sT?k_exGq7fqFeM3 zDsO*+#z>H@v?#FalZ8x6L=e>&%q?{gi40jK?*@?h!{AM;j12A=EP;?LN`JW~0Q$o$ zd(W9_jJ8EWS>FBKmF2Mh(0Etdw?eg3^CBu}rHCQ9&GEsLajEBe-e*_c&XWgm*XZE~ zJm`;LRv`8%$sQ72F)KfYaA4Akh--(8J-?GF$zxwOEytVk(QNkw(g(h0^D(LlZguUA z*%?=VSpF;w$?OBjm4w|CY>n=!da?s~hA%Je+JKUS%W`7%ktY=um9^e8Vor{?xw*M_ z-@T)emvG%c0gWEOWB96Hm*n3Lb2vw>q)z14jzkbPFp*4)BRn>n+zOHSV#x`N~#f zpLK!D+uqL_b8#ILGcQ^iz*^bWOZ(c(Vrwf=_230C9xp5AAgBGa{t)uv0=s*fKLi)p zGT_GtFG0+z!1Ge>{iu3>gA-Fu-lXFquwWU)8xLNSU6c0Pg+%E910EkMnO};pA+Y>nI(uajK0&C^Tj?r;NALPx2 z0Z>IsvaI@XcJ1x8%0r@~teE}w4YKxH@X;gM6adQ*G$`_JOoYhfc6*D8GTlFLR1IcE zGE6hsHVY~uR1s^Mz#W-kN!%m-QhE#+snB0RSh3P6fg2K?HV8o#5Axq}fHqcgG+!L( z8J$hCB>0^>o)h3v!bnQWOAMtu;IaU$I9g+_P$Y(f^?wO=+A9qWDh`Qjnr!({yb)Rj zax>*|s(Djm2L{LoDK%VFVqunt36FFBMV5B6j|g8jwR+O;FZ*behV0>?69RY_J&q2z zCJ7YHCDC51&B=B4u0?vsMrj93Lz*&hxxPmw((+Rf_~rh+{Af=VA@b9AP^%eO zjO-;+GuY%J`H5Mz%VM0eHd4f~d}gO4IdOTA(@;rCsicLF0_qYZD(H6^htE7rFAD9y z5y=gkZrk{5nEkfk1QPms+xYjYOl0mhVz9*@-VpVDHP-&=8szs0=h^_dzJ_5H*)V&G zG>4KF8P*sHqoAbhcw^f=2iJ@#IIG}Ugh&Rz0|MfNJ5H#N{(x1i!}kG8f!fMEz8>B! z_{mKWkHbAe;CS!ni$D`-wKd#>j`$q~mZCWq+Baj@i-fJsO+J2Q>R$iUwc&&o&i5LV z@f8~qxu?v7cX11nJXt1WP>ZZMv6lc>XdJZ&tZxR+4Ea+sB)VaheR3Vftn;#cD^{`)4a(tdoD< z$x89UF4GofCLpTlz)!H-%F@77jXrs`Va{k>O9xUa7i~;Ym!md*hgHLD*ZU=GyqAA- zW<9Hd-dvHHqBSw`8=IIYnI)*NKbzzK-DYEZsh0*Vr|~nReiBj*If+$e_y^|zd>dx= zh$y>r;bRy;a|T;z?&q&x8Q)ErpRp0w*{ zSM+V7G_L;jp=0k!U^8ca?mQJYKBN-c&ysjR4`+juwmtzO8~{ckokiWo01fVADFsk1 z^Z;f}^J0n)e&G1uNOslPK0g<5;tq;L0MOVW z1ii;9`e!r^Mj@r+)29bb4SFlirAA%a>paZFBr-2vzT_w=DS4s29t)_|4Bz3XgarS` zuz1gKuaFSwZIV!3L&JByYL}-KA2m29%coqZQdX`!rArI>3gBMmA(--s5Lnf9;5Kca z5I()%(CRYrY54bem5V6DjTL1Zh+{LI+WmDSqBypHG`0a#r2IvE8GAc7Kt^>midVEVh^g^n^aJmL- z7qJ0s1ufr7YyR}366Xq|7pTlh5%Bd~6F9ee*N-3f1uY z_Rg9i&0;Pt9u$|%1>_S~WNUh-XkaZ^pGtuCeX|N2 zS9+u*Rg9+P`%5nMa=IR>w;~{w$edX1P#!Fo7R7hZ5l!&;9>-inw-ov{=&Vf^EgXS) zPb(`l9(oFSTE{h7XH}P{44Dv^d<(yPZ+Nd|MqM{^e{^Id3fCeaOA5-UEn)d?siqdg ziE~x4p{jyTf8*6Wl{#C#eM-W$$Jt3uhBwg(+qABs+0*99d=;o7yD+UtGUuVu(4(b_ z;%q1BH7@lVSN&aQB|%He@b$CX6m`)idq1?Rw~EB@gl~JiUTs`8BQ34--CN>|4Xs`F z2K=2Iq31e&;*2H99y7yq{Cu0DO1{peEhSp5OrhwyZ|_|Rx9VA#)Zzvh{LH%rno1-| zE_Sqeu)YOzMEtdBw-Je4`Ngsx<5kjDl^KG`(xuaUgUqf6hn`FFLY3zuq9zBFL7Jl} zdw9uyo6#OH@vfj=c1XcyzrXX#4y1_kj*t1XmjW#Zx&C22_pCLlipQ#Ep%U2Fo`0j$ z!GAkkr%5v;0O5Vh%zbSBUMs6h>%x6{bgQpKUQ{@(On9|$s>**aU94fMB0udfzu;w zh%48oW9SZ`?tj>eE&Xqd{0{kOln%EJ^~u9V&rKhIzTa4 zW$yD>Stn=Ojrs1}?ROLA=8Zu{B1osIR=bMDvQA~Xc;$8~svizd>(iuYXm0f?=il?U zU}67eT3UjozQLty7{shf5ciy};^x8k6`<&>nk`cQJ2&fz9cj`{x~pcu%J8r+!Ll8A zHg}>#^19v^M^&ZTjLrJeiTE>FyRNJ+eWgkX-p+`*_FdV0fL{?i99x2I1Y42k_M8t{ zdt!BOn(;+l>3dC1${Nb(!`vv!C_)Dm}YT2_ia(dueY&+q@5riZhc@ zGlO>C3g?lrL=-;KGZ92NiS}uW#lX(5lXX4gEeSr@H0ghyofp&c;^%dKUf!6~n)ut9 z^AqrZlw(hdQl37_Tr+t4&dzHvX~-;t`@fXy-}?7T09iX<$d)yBX$<;N=7KIz z{jc^bWvebpfhutqpuVDq0?;%4UN4oUV1;0&DV8BRr=dTR^!ta%(1&er{-{}JhVH}B zw;OH$5x4id@jgLKmdTcbu+#Y=6P0R9GP3xwf4lma_Q0VY1{qgU7uyEcsg{{KYq>}m zguJOk;1Q6EDOpYEFxO=d{oYw>OBkLLxdV|JMc^d7S5KGSe}DUlz$twZ3PJ1Ba-FAM z_U<0bV!v?pyBgx({(s}u%oJ}G6M7pG%TQ+SlQ$-WKG?42vyKNs*6H{ekFqRfXQ}X>DzN0l1A{sWe2&-OC-iZ|{E3CM)b{ zO{yH1@%41EFZt<;KpOS}F;aWsJuk`oD6Fm3Die*FFo-3p+xXD!pz4`N@*G{1%vOrq z$kO6sBqItt*PQ>N8PvqARirNh|Bbf^R@`zN$D_A7r92LcTDnWbUf8u1)?00!7M0al zMfMxR>?9b)Use<|+kpe;(!CCI)XO*j*0>1Ow@5|&ZCU0n8Bxx<$9FUzanroapG}v& ztmB-Zu&BspC|CZuhDJ)qV1K`&kx|-q&S?7eVfg!l4K%^;e_310vuS!*Npv7gLD;Te z&eoQ_wRJgLJ)TbRx`>EK!1+$iMWwxl#%4#3eOC(bQ3e6y)wcj|ffW8{nmzK2oSgUl z{Tfp^;8ajlbnZQQz4f2b|0vVuJZCw7=u=SHOK882M*Yo{R?M2n3-4<90Re2V>`J+m z024)Hu64yTaK-K=oWJrq#Z#e($l(7akm-f(UbIKuE5|-mj{6ZuFN`bKe!Z;Sb?WmU z;@JX>5!67=-HE}3{*XH;vI(gV5xvz5Ao;e54v=m^!@#Vr(E5{|8U2+}Hz>tTy_I}p zf%=d;pZdsSQ>Y#V-ReCb<>2IOb$H4qBs8+3LQ78{O~xcie1(V6(b1XMQO%D+H?}4b zU2<=(0ZflcP0c*a1vn2?ebvwr{is3b@Z`nC*>Q%@%NQqFWLSgDHK35` zn;8R=83W4lQw~pC-L!|kUMQ)k>*QFx1`v_`k-O1_|626Rk~V%%z!htF=$%alxJaIjo{GlTQ_^1nT%1O@ zRa0da;$F%}VG3OZ?`zHX6w5{*)Q1jBScPCmqg8JgF+OJT!^Cop5I(f_I#cCvM{G5O{J4X8vj1PKNq58r8{LLPtW}J796f3v$c-S9k zRV!jM07Q6-meOZYY`~Qe=fi?N?mn-)*uX6ga;gCDT`>`4kFaB$?7WKq0_3-8_4EAW zAk82d_od*rlKWogHf#!ipmc{(f+b!uyTEeLqI{>v;e6C_XOPpla^~`E_oXaWo$KrV zqs!Q-=DC>5#$oX4=f>vwx>l~sPdt6NUWhPC0*= prayer_cap && !cap_warning_given) + // We don't actually stop people from praying cause this can be used for ROLEPLAAAAY + to_chat(user, span_warning("You cannot gain any more piety from prayer!")) + cap_warning_given = TRUE + else if(istype(area, /area/station/service/chapel) || prob(check_how_religious(user))) // If you're in the chapel or if fate aligns. + if(cap_warning_given) + continue + adjust_piety(1) + to_chat(user, span_notice("You feel more pious after your prayer.")) + else + keep_going = FALSE + while (keep_going) + to_chat(user, span_notice("You stop praying.")) + // cleanup + keep_going = FALSE + active = FALSE + user.remove_status_effect(/datum/status_effect/spotlight_light/divine) + +// As the name implies, we take various factors that suggest a target's devotion, as well as a few misc. factors +/datum/action/cooldown/power/theologist/pious_prayer/proc/check_how_religious(mob/living/user) + // Combined total chance. + var/total_chance = 10 + + // Are you the chaplain? + if(is_chaplain_job(attached_mob.mind?.assigned_role)) + total_chance += 20 + // Do you have the religious quirk? + if(HAS_TRAIT(user, TRAIT_SPIRITUAL)) + total_chance += 5 + // Do you carry the bible on your person? + if(has_bible(user)) + total_chance += 5 + // Are you standing on a blessed tile? (Blessed with holy water). + if(locate(/obj/effect/blessing) in user.loc) + total_chance += 15 + + return total_chance + +// Most people don't but it'd be cool if they did. +/datum/action/cooldown/power/theologist/pious_prayer/proc/has_bible(mob/living/user) + if(!user) + return FALSE + return !!locate(/obj/item/book/bible) in user.get_all_contents() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm index 8616a8b8dcb51f..37c626dcf2f0af 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm @@ -1,7 +1,7 @@ /datum/power/theologist/smiting_strike name = "Smiting Strike" desc = "Channel energy into the item you are currently holding. Your next attack that hits with it against a creature deals 15 additional burn damage and sends them flying backwards 4 spaces. \ - This knockback cannot stun or damage on impact. Costs 5 Piety to use; recast to cancel. This effect ends if the item leaves your hands." + This knockback cannot stun or damage on impact. Costs 5 Piety to use. This effect ends if the item leaves your hands." action_path = /datum/action/cooldown/power/theologist/smiting_strike value = 5 @@ -12,7 +12,7 @@ /datum/action/cooldown/power/theologist/smiting_strike name = "Smiting Strike" desc = "Channel energy into the item you are currently holding. Your next attack that hits with it against a creature deals 15 additional burn damage and sends them flying backwards 4 spaces. \ - This knockback cannot stun or damage on impact. Costs 5 Piety to use; recast to cancel. This effect ends if the item leaves your hands." + This knockback cannot stun or damage on impact. Costs 5 Piety to use. This effect ends if the item leaves your hands." button_icon = 'icons/mob/actions/actions_cult.dmi' button_icon_state = "sword_fling" cooldown_time = 150 @@ -56,6 +56,7 @@ var/thingholdingimbued = currently_imbued.loc if(ismob(thingholdingimbued)) to_chat(thingholdingimbued, span_warning("The smiting energies leave [currently_imbued]")) + currently_imbued.RemoveElement(/datum/element/theologist_smite) currently_imbued = null currently_imbued = to_imbue currently_imbued.AddElement(/datum/element/theologist_smite, smite_damage, smite_knockback, FALSE, TRUE, TRUE) @@ -63,7 +64,7 @@ /datum/action/cooldown/power/theologist/smiting_strike/proc/imbue_global(obj/to_imbue) to_imbue.AddElement(/datum/element/theologist_smite, smite_damage, smite_knockback, FALSE, TRUE, FALSE) - to_chat(owner, span_notice("You infuse smiting energies into [currently_imbued]")) + to_chat(owner, span_notice("You infuse smiting energies into [to_imbue]")) // Whilst I originally considered adding just the knockback element, we kind-of want more control over when the smite fades. /datum/element/theologist_smite @@ -79,14 +80,12 @@ var/self_terminate_on_drop // The person assigned to be the holder of the object. var/mob/living/holder - // the glowy effect - var/mutable_appearance/target_glow - // The attached item - var/obj/item/attached_item + // the bless effect + var/image/bless_overlay // This is basically the knockback code but hybridized. Sue me. -/datum/element/theologist_smite/Attach(datum/target, smite_damage = 1, throw_distance = 1, throw_anchored = FALSE, throw_gentle = FALSE, self_terminate_on_drop = FALSE) +/datum/element/theologist_smite/Attach(atom/target, smite_damage = 1, throw_distance = 1, throw_anchored = FALSE, throw_gentle = FALSE, self_terminate_on_drop = FALSE) // While the balancer inside me suggests we restrict this to melee hits... I kind of want to see the fun of ranged smites. // For the future person to balance this; really just remove projectile_hit() and the first if in this sequence if you want to axe ranged. . = ..() @@ -104,39 +103,36 @@ src.throw_gentle = throw_gentle src.self_terminate_on_drop = self_terminate_on_drop - attached_item = target if(isitem(target) && self_terminate_on_drop) // No point tracking this if we aren't going to self_terminate on drop - RegisterSignal(attached_item, COMSIG_ITEM_DROPPED, PROC_REF(on_item_dropped)) + RegisterSignal(target, COMSIG_ITEM_DROPPED, PROC_REF(on_item_dropped)) + + // Applies the sparkling bless effect + // I'd normally do mutable_appearance but overlays are uppity with elements and this method works with rust soooo I am using it like this. + bless_overlay = image(icon = 'icons/effects/effects.dmi', icon_state = "blessed", layer = target.layer - 0.1) + RegisterSignal(target, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(apply_bless_overlay)) + target.update_appearance() - // Applies the glowing effect - target_glow = mutable_appearance( - icon = 'icons/effects/effects.dmi', - icon_state = "blessed", - layer = attached_item.layer - 0.1, - appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART - ) - attached_item.add_overlay(target_glow) // Checks if the item is no longer in our hands. If so, remove this element. /datum/element/theologist_smite/proc/on_item_dropped(datum/source, mob/user) SIGNAL_HANDLER if(self_terminate_on_drop) - Detach(attached_item) + Detach(source) return +/datum/element/theologist_smite/proc/apply_bless_overlay(atom/parent_atom, list/overlays) + SIGNAL_HANDLER -// Prevents signalers from loitering. -/datum/element/theologist_smite/Detach(datum/source) - UnregisterSignal(source, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_POST_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT)) - if(attached_item && self_terminate_on_drop) - UnregisterSignal(attached_item, COMSIG_ITEM_DROPPED) - - if(target_glow) - attached_item.cut_overlay(target_glow) - target_glow = null + if(bless_overlay) + overlays += bless_overlay +// Prevents signalers from loitering. +/datum/element/theologist_smite/Detach(atom/source) + UnregisterSignal(source, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_POST_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT, COMSIG_ATOM_UPDATE_OVERLAYS)) + if(self_terminate_on_drop) + UnregisterSignal(source, COMSIG_ITEM_DROPPED) REMOVE_TRAIT(source, TRAIT_HAS_SMITING_STRIKE, src) - attached_item = null + source.update_appearance() holder = null return ..() @@ -172,7 +168,7 @@ throw_distance *= -1 var/atom/throw_target = get_edge_target_turf(target, throw_dir) target.safe_throw_at(throw_target, throw_distance, 1, thrower, gentle = throw_gentle) - new /obj/effect/temp_visual/kinetic_blast(get_turf(target), "#ddd166") + new /obj/effect/temp_visual/electricity(get_turf(target), "#ddd166") playsound(target, 'sound/effects/magic/magic_block_holy.ogg', 75, TRUE) target.adjustFireLoss(smite_damage) to_chat(target, span_userdanger("You are knocked back by a burning, resonant energy!")) diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index 18cfe2a07fb62a..431494c86efbdf 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -213,10 +213,33 @@ var/list/required_powers = GLOB.powers_requirements_list[power_type] if(!length(required_powers)) return + for(var/datum/power/required_power_type as anything in required_powers) var/required_power_name = required_power_type.name - if(!(required_power_name in preferences.all_powers)) - return required_power_type + + // Exact requirement satisfied (current behaviour) + if(required_power_name in preferences.all_powers) + continue + + // Optional: allow subtypes, decided by the power we're trying to learn + if(power_type.required_allow_subtypes) + var/required_typepath = ispath(required_power_type) ? required_power_type : required_power_type.type + var/found_subtype = FALSE + + for(var/selected_power_name in preferences.all_powers) + var/datum/power/selected_power_type = SSpowers.powers[selected_power_name] + if(!selected_power_type) + continue + + if(ispath(selected_power_type.type, required_typepath)) + found_subtype = TRUE + break + + if(found_subtype) + continue + + return required_power_type + /** * Checks whether at least one of our powers requires the given power type, diff --git a/tgstation.dme b/tgstation.dme index dc1372710b9339..c14fb759e1cb53 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7445,6 +7445,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\entropic_mending.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike_upgrades.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\pious_prayer.dm" #include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" From fee7fc458e9849068391bc2d200e289769f8b29b Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 30 Jan 2026 11:17:26 +0100 Subject: [PATCH 035/212] I went to bed last night and forgot to change references, scheise. --- .../code/powers/sorcerous/theologist/pious_prayer.dm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm index 22ccd9b514dcce..fce916b48c2d7c 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm @@ -30,10 +30,10 @@ /datum/action/cooldown/power/theologist/pious_prayer/New() // Increase prayer cap based on various factors. // Are you the Chaplain? - if(is_chaplain_job(attached_mob.mind?.assigned_role)) + if(is_chaplain_job(owner.mind?.assigned_role)) prayer_cap = 15 // Do you have the religious quirk? - if(HAS_TRAIT(user, TRAIT_SPIRITUAL)) + if(HAS_TRAIT(owner, TRAIT_SPIRITUAL)) prayer_cap = 10 /datum/action/cooldown/power/theologist/pious_prayer/use_action(mob/living/user, atom/target) @@ -73,7 +73,7 @@ var/total_chance = 10 // Are you the chaplain? - if(is_chaplain_job(attached_mob.mind?.assigned_role)) + if(is_chaplain_job(user.mind?.assigned_role)) total_chance += 20 // Do you have the religious quirk? if(HAS_TRAIT(user, TRAIT_SPIRITUAL)) From d6770492340130f705024abcc189f8ed4994bc36 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 30 Jan 2026 22:35:37 +0100 Subject: [PATCH 036/212] Work on the thaumaturge systems. TGUI is coming along but isn't mechanically functional. Charges are functional. --- code/__DEFINES/~doppler_defines/powers.dm | 8 + .../thaumaturge/_thaumaturge_action.dm | 83 ++++++++ .../thaumaturge/_thaumaturge_power.dm | 9 + .../thaumaturge/_thaumaturge_preperation.dm | 189 ++++++++++++++++++ .../thaumaturge/_thaumaturge_root.dm | 43 +++- .../sorcerous/thaumaturge/phantasmal_tool.dm | 25 +++ .../sorcerous/theologist/_theologist_piety.dm | 96 --------- .../modular_powers/code/powers_subsystem.dm | 2 +- tgstation.dme | 4 + .../tgui/interfaces/ThaumaturgeSpellPrep.tsx | 119 +++++++++++ 10 files changed, 471 insertions(+), 107 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_power.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm create mode 100644 tgui/packages/tgui/interfaces/ThaumaturgeSpellPrep.tsx diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 0ca08b3e2199d7..5d6407fec34de7 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -45,6 +45,14 @@ /// Trait held by all under the sorcerous archetype. #define TRAIT_ARCHETYPE_SORCEROUS "archetype_sorcerous" +/** + * SORCEROUS: THAUMATURGE + * All defines related to the Thaumaturge powers. + */ + +// How much mana you practically can cap out at. +#define THAUMATURGE_MAX_MANA 50 + /** * SORCEROUS: ENIGMATIST * All defines related to the enigmatist powers. diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm new file mode 100644 index 00000000000000..a5ddc2e03e1202 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm @@ -0,0 +1,83 @@ +/datum/action/cooldown/power/thaumaturge + name = "abstract thaumaturge power action - ahelp this" + background_icon_state = "bg_star" + overlay_icon_state = "bg_default_border" + button_icon = 'icons/mob/actions/backgrounds.dmi' + + // We generally don't dabble with cooldowns but a GCD of 0.5 seconds is kinda handy to prevent you from blowing your load on all your charges by accident. + cooldown_time = 5 + // Unlike normal spells, we have charges. More of that explained below at action_success() + var/charges = 0 + // The cap on charges; you can't prepare more than these. If you leave this null, the spell will not interact with the charges system. + var/max_charges + // How many charges does it consume on use? + var/charges_to_use = 1 + // How much 'mana' does it cost to prepare this per charge? + var/prep_cost = 1 + + // Overlay that shows the number of charges + var/mutable_appearance/charge_overlay + +/datum/action/cooldown/power/thaumaturge/New() + update_charges_overlay() + +/datum/action/cooldown/power/thaumaturge/proc/adjust_charges(amount, override_cap) + if(!isnum(amount)) + return + var/cap_to = isnum(override_cap) ? override_cap : max_charges + charges = clamp(charges + amount, 0, cap_to) + + //theologist_ui?.maptext = FORMAT_PIETY_TEXT(charges) + +/* + Deviating massively from the original cooldown system, thaumaturge has charges they have to prepare and plan for in advance, just like the classic vanician spellcasting system. + Mechanically, we check if charges are 0. If so we Disable(). Otherwise, we deduct a charge and go on a short 1 second (or whatever is programmed in) cooldown. +*/ + +/datum/action/cooldown/power/thaumaturge/on_action_success(mob/living/user, atom/target) + SHOULD_CALL_PARENT(TRUE) + . = ..() + adjust_charges(-charges_to_use) + check_if_valid() + return + +// Checks if we have charges to use. +/datum/action/cooldown/power/thaumaturge/proc/check_if_valid() + if(charges <= 0 && max_charges) // If charges are 0 or less and it has a max_charges set. + disable() + else + enable() + update_charges_overlay() + +// Handles the UI stuff. +/datum/action/cooldown/power/thaumaturge/proc/update_charges_overlay() + var/atom/movable/ui_element = get_atom_moveable() + if(!ui_element) + return + if(!max_charges) + return + + ui_element.cut_overlay(charge_overlay) + charge_overlay = new/mutable_appearance + charge_overlay.maptext_width = 32 + charge_overlay.maptext_height = 16 + + // Bottom-left-ish + charge_overlay.maptext_x = 4 + charge_overlay.maptext_y = 0 + + charge_overlay.maptext = MAPTEXT("[charges]") + ui_element.add_overlay(charge_overlay) + build_all_button_icons(UPDATE_BUTTON_STATUS) + +// Get the moveable atom specifically for adjusting the number. +/datum/action/cooldown/power/thaumaturge/proc/get_atom_moveable() + for(var/datum/hud/hud_instance as anything in viewers) + var/atom/movable/screen/movable/action_button/action_button_instance = viewers[hud_instance] + if(istype(action_button_instance, /atom/movable/screen/movable/action_button)) + return action_button_instance + + + + + diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_power.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_power.dm new file mode 100644 index 00000000000000..103e2383c35a3e --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_power.dm @@ -0,0 +1,9 @@ +/datum/power/thaumaturge + name = "Thaumaturge Power" + desc = "Your Clairvoyance spell has succesfully revealed a most terrifying creature; an abstract parent type. Roll a DC18 Will save or become Confused for 1 minute. \ + If you have expert proficeincy in Lore: Programming or are wearing programmer socks; improve your degree of success by one step." + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THAUMATURGE + priority = POWER_PRIORITY_BASIC + abstract_parent_type = /datum/power/thaumaturge diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm new file mode 100644 index 00000000000000..045efb39009cdf --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm @@ -0,0 +1,189 @@ +/datum/component/thaumaturge_preparation + dupe_mode = COMPONENT_DUPE_UNIQUE + + // The mob we’re attached to is always `parent`. + var/mob/living/attached_mob + + // The 'mana' we have to allocate. This is basically 2x the power value in the powers menu. Note that the spell's own mana cost need not be propertional to the value. + var/mana + + // The mana that is currently being spend by spell preperation. + var/mana_spend + + // Maximum amount of mana you can have. + var/max_mana = THAUMATURGE_MAX_MANA + + // List of spells available to the user. + var/list/spell_list = list() + + // Spells being prepared in the UI + var/list/prepared_charges = list() + + // Spells prepared post-preperation. + var/list/applied_prepared_charges = list() + + +/datum/component/thaumaturge_preparation/Initialize() + . = ..() + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + attached_mob = parent + +// Validates mana and adds spells to the list. +/datum/component/thaumaturge_preparation/proc/validate_spells() + var/calculated_mana = 0 + spell_list = list() + for(var/datum/power/power_instance as anything in attached_mob.powers) + if(!power_instance) + continue + if(power_instance.path != POWER_PATH_THAUMATURGE) + continue + if(check_if_can_prepare(power_instance.action_path)) + spell_list.Add(power_instance) + calculated_mana += power_instance.value + mana = clamp(calculated_mana, 0, max_mana) + +// Checks if we can prepare the spell in our spellbook and if so adds it to the spell list. +/datum/component/thaumaturge_preparation/proc/check_if_can_prepare(action_type) + if(!istype(action_type, /datum/action/cooldown/power/thaumaturge)) + return FALSE + var/datum/action/cooldown/power/thaumaturge/cast_type = action_type + if(!cast_type.max_charges) + return FALSE + + return TRUE + +// Find the spell in the current spell_list and read its prep_cost. +/datum/component/thaumaturge_preparation/proc/get_prep_cost_for_spell_ref(spell_ref) + for(var/datum/power/power_instance as anything in spell_list) + if("[power_instance.action_path]" == spell_ref) + var/datum/action/cooldown/power/thaumaturge/action_instance = power_instance.action_path + return max(0, action_instance?.prep_cost || 0) + + return 0 + + +/* Below is responsible for all the TGUI stuff to do with spell preperation. + Save yourself if you need to touch this. +*/ +/datum/component/thaumaturge_preparation/ui_interact(mob/living/user, datum/tgui/ui) + if(!user) + return + + ui = SStgui.try_update_ui(user, src, ui) + if(ui) + return + + // Draft starts from applied state + prepared_charges = applied_prepared_charges.Copy() + + // Recalculate mana_spend from the draft + mana_spend = 0 + for(var/spell_ref in prepared_charges) + var/charges = prepared_charges[spell_ref] + if(!isnum(charges) || charges <= 0) + continue + mana_spend += (charges * get_prep_cost_for_spell_ref(spell_ref)) + + ui = new(user, src, "ThaumaturgeSpellPrep", "Spell Preparation") + ui.open() + + +/datum/component/thaumaturge_preparation/ui_state(mob/user) + // Pick an appropriate state; many UIs use always_state if only access control is server-side. + return GLOB.always_state + +/datum/component/thaumaturge_preparation/ui_data(mob/living/user) + var/list/spells_payload = list() + + for(var/datum/power/power_instance as anything in spell_list) + var/spell_ref = "[power_instance.action_path]" + var/current_charges = prepared_charges[spell_ref] + if(isnull(current_charges)) + current_charges = 0 + + var/datum/action/cooldown/power/thaumaturge/action_instance = power_instance.action_path + + var/prep_cost = action_instance?.prep_cost + if(isnull(prep_cost)) + prep_cost = 1 + + spells_payload += list(list( + "key" = spell_ref, + "name" = action_instance?.name || "Unknown Spell", + "charges" = current_charges, + "max_charges" = action_instance?.max_charges || 0, + "prep_cost" = prep_cost, + "icon" = action_instance?.button_icon, + "icon_state" = action_instance?.button_icon_state, + )) + + var/mana_remaining = max(mana - mana_spend, 0) + + return list( + "mana_total" = mana, + "mana_spend" = mana_spend, + "mana_remaining" = mana_remaining, + "spell_count" = length(spells_payload), + "spells" = spells_payload, + ) + + + +/datum/component/thaumaturge_preparation/ui_act(action, list/params, datum/tgui/ui) + . = ..() + if(.) + return + + if(action == "inc" || action == "dec") + var/spell_ref = params["ref"] + if(!spell_ref) + return TRUE + + // Validate spell exists (prevents spoofing) + var/found = FALSE + var/max_charges_local = 0 + + for(var/datum/power/power_instance as anything in spell_list) + if("[power_instance.action_path]" != spell_ref) + continue + + var/datum/action/cooldown/power/thaumaturge/action_instance = power_instance.action_path + max_charges_local = action_instance?.max_charges || 0 + found = TRUE + break + + if(!found || max_charges_local <= 0) + return TRUE + + var/current_charges = prepared_charges[spell_ref] + if(isnull(current_charges)) + current_charges = 0 + + var/prep_cost = get_prep_cost_for_spell_ref(spell_ref) + + if(action == "inc") + if(current_charges >= max_charges_local) + return TRUE + + if(mana_spend + prep_cost > mana) + return TRUE + + current_charges++ + mana_spend += prep_cost + + else + if(current_charges <= 0) + return TRUE + + current_charges-- + mana_spend = max(mana_spend - prep_cost, 0) + + prepared_charges[spell_ref] = current_charges + return TRUE + + if(action == "apply") + applied_prepared_charges = prepared_charges.Copy() + return TRUE + + return FALSE 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 80e3c1b16d628e..0443716fce32e5 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 @@ -1,9 +1,9 @@ -/datum/power/item_power/thaumaturge_root +/datum/power/thaumaturge_root name = "Spell Preparation" - desc = "Wizards, sorcerers, sages. These are all Thaumaturges, who channel the Resonant song \ - with their bodies and their words, whether they discover these through careful study or \ - strong intuition." + desc = "Grants you a Spell Focus, an unique item that allows you to charge your Thaumaturge spells while sleeping, and enhance them by holding it. Use the Spell Focus in your hand to change it's form." + + action_path = /datum/action/cooldown/power/thaumaturge/thaumaturge_root value = 5 mob_trait = TRAIT_ARCHETYPE_SORCEROUS @@ -11,15 +11,38 @@ path = POWER_PATH_THAUMATURGE priority = POWER_PRIORITY_ROOT +/* Previous Item Power stuff. TODO: Make Item power a variable rather than a subtype. /datum/power/item_power/thaumaturge_root/add_unique(client/client_source) - var/obj/item/book/random/spellbook = new(get_turf(power_holder)) - spellbook.name = "[power_holder.real_name]'s spellbook" - give_item_to_holder(spellbook, list(LOCATION_BACKPACK, LOCATION_HANDS)) + //var/obj/item/book/random/spellbook = new(get_turf(power_holder)) + //.name = "[power_holder.real_name]'s spellbook" + //give_item_to_holder(spellbook, list(LOCATION_BACKPACK, LOCATION_HANDS)) /datum/power/item_power/thaumaturge_root/add(client/client_source) - var/datum/action/cooldown/spell/touch/prestidigitation/that_magic_touch = new - that_magic_touch.Grant(power_holder) - + //var/datum/action/cooldown/spell/touch/prestidigitation/that_magic_touch = new + //that_magic_touch.Grant(power_holder) +*/ + +/datum/power/thaumaturge_root/post_add() + if(!power_holder) // So it doesn't runtime at init + return + // Spell preperation is so complicated we basically handle it all in a component, including the UI part. + power_holder.AddComponent(/datum/component/thaumaturge_preparation, power_holder) + . = ..() + +/datum/action/cooldown/power/thaumaturge/thaumaturge_root + name = "Spell Preperation" + desc = "Adjust the amount of charges your spells have! Requires sleeping with a Spell Focus on your person to apply (except the first time in a round)." + button_icon = 'icons/obj/antags/cult/structures.dmi' + button_icon_state = "pylon" + +/datum/action/cooldown/power/thaumaturge/thaumaturge_root/use_action(mob/living/user, atom/target) + var/datum/component/thaumaturge_preparation/prep_component = user.GetComponent(/datum/component/thaumaturge_preparation) + if(!prep_component) + to_chat(user, span_warning("Something terrible has happened; you're missing your preperation component. Yell at devs!")) + return FALSE + prep_component.validate_spells() // We call it here so all the spells are loaded when we open it. + prep_component.ui_interact(user) + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm new file mode 100644 index 00000000000000..8968ca292e1e79 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm @@ -0,0 +1,25 @@ +/* + For the people that don't want to hunt for tools, or are just too bothered to carry one. +*/ +/datum/power/thaumaturge/phantasmal_tool + name = "Phantasmal Tool" + desc = "Summons a basic tool of your choice in your hand, that disappears if dropped or after a duration. Requires Affinity 1 to cast. Affinity gives a chance to not consume charges on cast." + value = 3 + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THAUMATURGE + action_path = /datum/action/cooldown/power/thaumaturge/phantasmal_tool + required_powers = list(/datum/power/thaumaturge_root) + required_allow_subtypes = TRUE + +/datum/action/cooldown/power/thaumaturge/phantasmal_tool + name = "Phantasmal Tool" + desc = "Summons a basic tool of your choice in your hand, that disappears if dropped or after a duration. Has a chance not to consume charges." + button_icon = 'icons/obj/antags/cult/structures.dmi' + button_icon_state = "tomealtar" + + max_charges = 5 + +/datum/action/cooldown/power/thaumaturge/phantasmal_tool/use_action(mob/living/user, atom/target) + to_chat(user, span_notice("hello WORLD")) + return TRUE 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 0dbeaf54f7fd37..4f30ddb47935c3 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 @@ -24,21 +24,14 @@ if(is_chaplain_job(attached_mob.mind?.assigned_role)) max_piety *= 2 - // If your old system used signals, register them here. RegisterWithParent() - // If your old system processed over time, start that here. - // START_PROCESSING(SSprocessing, src) // only if you actually need processing - /datum/component/theologist_piety/RegisterWithParent() . = ..() if(attached_mob.hud_used) install_piety_hud(parent) else RegisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created)) - // Examples (swap for your real signals): - // RegisterSignal(attached_mob, COMSIG_MOB_LIFE, PROC_REF(on_life)) - // RegisterSignal(attached_mob, COMSIG_LIVING_DEATH, PROC_REF(on_death)) /datum/component/theologist_piety/UnregisterFromParent() // UnregisterSignal(attached_mob, list(COMSIG_..., COMSIG_...)) @@ -80,7 +73,6 @@ // Set initial text so it isn't blank until first adjust. theologist_ui.maptext = FORMAT_PIETY_TEXT(piety) - // THIS is the missing “why it only appears after changeling” hud_used.show_hud(hud_used.hud_version) /datum/component/theologist_piety/proc/adjust_piety(amount, override_cap) @@ -97,91 +89,3 @@ icon = 'icons/hud/blob.dmi' // TODO: Get sprites/UI for this. icon_state = "block" screen_loc = "WEST,CENTER-2:15" // TODO: Define & Move this. - -/* - -/datum/power/theologist_piety - name = "Piety" - desc = "Responsible for managing Piety." - abstract_parent_type = /datum/power - value = 0 - - mob_trait = TRAIT_ARCHETYPE_SORCEROUS - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THEOLOGIST - priority = POWER_PRIORITY_ROOT - - // Used as a resource. We permit decimal numbers, but the UI will always show non-decimals. - var/piety = 0 - - //At what point do we cap out piety? - var/max_piety = PIETY_MAX - - // The UI itself - var/atom/movable/screen/theologist_piety/theologist_ui - -/datum/power/theologist_piety/post_add() - . = ..() - - if(!power_holder) - return - - var/mob/living/living_holder = power_holder - if(living_holder.hud_used) - install_piety_hud(living_holder) - else - RegisterSignal(living_holder, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created)) - -/datum/power/theologist_piety/remove() - . = ..() - - if(!power_holder) - return - - var/mob/living/living_holder = power_holder - UnregisterSignal(living_holder, COMSIG_MOB_HUD_CREATED) - - if(living_holder.hud_used && theologist_ui) - living_holder.hud_used.infodisplay -= theologist_ui - qdel(theologist_ui) - theologist_ui = null - -/datum/power/theologist_piety/proc/on_hud_created(datum/source) - SIGNAL_HANDLER - - var/mob/living/living_holder = power_holder - if(!living_holder || !living_holder.hud_used) - return - - install_piety_hud(living_holder) - -/datum/power/theologist_piety/proc/install_piety_hud(mob/living/living_holder) - if(theologist_ui) // already installed - return - - var/datum/hud/hud_used = living_holder.hud_used - theologist_ui = new /atom/movable/screen/theologist_piety(null, hud_used) - hud_used.infodisplay += theologist_ui - - // Set initial text so it isn't blank until first adjust. - theologist_ui.maptext = FORMAT_PIETY_TEXT(piety) - - // THIS is the missing “why it only appears after changeling” - hud_used.show_hud(hud_used.hud_version) - -// UI Elements for Piety -/atom/movable/screen/theologist_piety - name = "piety" - icon = 'icons/hud/blob.dmi' // TODO: Get sprites/UI for this. - icon_state = "block" - screen_loc = "WEST,CENTER-2:15" // TODO: Define & Move this. - - -/datum/power/theologist_piety/proc/adjust_piety(amount, override_cap) - if(!isnum(amount)) - return - var/cap_to = isnum(override_cap) ? override_cap : max_piety - piety = clamp(piety + amount, 0, cap_to) - - theologist_ui?.maptext = FORMAT_PIETY_TEXT(piety) -*/ diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index a61e931ece12f2..c0f5e9f00068f0 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -1,7 +1,7 @@ // Both of these lists are shifted to glob so they are generated at world start instead of risking players doing preference stuff before the subsystem inits. GLOBAL_LIST_INIT_TYPED(powers_blacklist, /list/datum/power, list( - list(/datum/power/item_power/thaumaturge_root, /datum/power/enigmatist_root), + //list(/datum/power/item_power/thaumaturge_root, /datum/power/enigmatist_root), list(/datum/power/theologist_root/revered, /datum/power/theologist_root/shared, /datum/power/theologist_root/twisted) // The three Theologist Roots )) diff --git a/tgstation.dme b/tgstation.dme index c14fb759e1cb53..70e9b24b9928f8 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7435,6 +7435,10 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\lodestone_legends.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_action.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_power.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_preperation.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\phantasmal_tool.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_action.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/ThaumaturgeSpellPrep.tsx b/tgui/packages/tgui/interfaces/ThaumaturgeSpellPrep.tsx new file mode 100644 index 00000000000000..f76d1c50d94d7e --- /dev/null +++ b/tgui/packages/tgui/interfaces/ThaumaturgeSpellPrep.tsx @@ -0,0 +1,119 @@ +import { Box, Button, DmIcon, Section, Stack } from 'tgui-core/components'; + +import { useBackend } from '../backend'; +import { Window } from '../layouts'; + +type SpellEntry = { + key: string; + name: string; + charges: number; + max_charges: number; + prep_cost: number; + icon?: string; + icon_state?: string; +}; + +type Data = { + tguitheme?: string; + mana_remaining: number; + spell_count: number; + spells: SpellEntry[]; +}; + +export const ThaumaturgeSpellPrep = (_props) => { + const { data, act } = useBackend(); + const spells = data.spells || []; + + return ( + + +
+ + Mana remaining: {data.mana_remaining} + +
+ +
+ + {spells.map((spell) => ( + +
+ + + + +
+
+ ))} +
+
+ +
+ + + + Preparing spells for the first time applies the charges + instantly! + + + + + + +
+
+
+ ); +}; From 4ceeb141c75a3d5a8b4d2a8e410136730ec4e5ab Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 31 Jan 2026 12:03:15 +0100 Subject: [PATCH 037/212] Sleep-based charges and preperation system fully functional. --- .../thaumaturge/_thaumaturge_action.dm | 28 ++-- .../thaumaturge/_thaumaturge_preperation.dm | 122 +++++++++++++++++- .../thaumaturge/_thaumaturge_root.dm | 2 +- 3 files changed, 134 insertions(+), 18 deletions(-) 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 a5ddc2e03e1202..23f52ed3ceaf0b 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 @@ -19,35 +19,43 @@ var/mutable_appearance/charge_overlay /datum/action/cooldown/power/thaumaturge/New() + if(max_charges) + disable() // prep your spells first update_charges_overlay() +/datum/action/cooldown/power/thaumaturge/Trigger(mob/clicker, trigger_flags, atom/target) + SHOULD_CALL_PARENT(TRUE) + if(!check_if_valid()) + return FALSE + . = ..() + +/datum/action/cooldown/power/thaumaturge/on_action_success(mob/living/user, atom/target) + SHOULD_CALL_PARENT(TRUE) + . = ..() + adjust_charges(-charges_to_use) + check_if_valid() + return + /datum/action/cooldown/power/thaumaturge/proc/adjust_charges(amount, override_cap) if(!isnum(amount)) return var/cap_to = isnum(override_cap) ? override_cap : max_charges charges = clamp(charges + amount, 0, cap_to) - //theologist_ui?.maptext = FORMAT_PIETY_TEXT(charges) - /* Deviating massively from the original cooldown system, thaumaturge has charges they have to prepare and plan for in advance, just like the classic vanician spellcasting system. Mechanically, we check if charges are 0. If so we Disable(). Otherwise, we deduct a charge and go on a short 1 second (or whatever is programmed in) cooldown. */ -/datum/action/cooldown/power/thaumaturge/on_action_success(mob/living/user, atom/target) - SHOULD_CALL_PARENT(TRUE) - . = ..() - adjust_charges(-charges_to_use) - check_if_valid() - return - // Checks if we have charges to use. /datum/action/cooldown/power/thaumaturge/proc/check_if_valid() + update_charges_overlay() if(charges <= 0 && max_charges) // If charges are 0 or less and it has a max_charges set. disable() + return FALSE else enable() - update_charges_overlay() + return TRUE // Handles the UI stuff. /datum/action/cooldown/power/thaumaturge/proc/update_charges_overlay() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm index 045efb39009cdf..e74e94be2f0f28 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm @@ -4,11 +4,11 @@ // The mob we’re attached to is always `parent`. var/mob/living/attached_mob - // The 'mana' we have to allocate. This is basically 2x the power value in the powers menu. Note that the spell's own mana cost need not be propertional to the value. - var/mana + // The 'mana' we have to allocate. This is basically the power value of the spell in the powers menu. Note that the spell's own mana cost need not be propertional to the value. + var/mana = 0 // The mana that is currently being spend by spell preperation. - var/mana_spend + var/mana_spend = 0 // Maximum amount of mana you can have. var/max_mana = THAUMATURGE_MAX_MANA @@ -22,6 +22,11 @@ // Spells prepared post-preperation. var/list/applied_prepared_charges = list() + // If this is the first time preparing spells for the round. + var/first_time_preperation = TRUE + + // If they go to sleep, they'll recharge their actions. This is only set if it passes validation. + var/recharge_when_sleep = FALSE /datum/component/thaumaturge_preparation/Initialize() . = ..() @@ -29,8 +34,29 @@ return COMPONENT_INCOMPATIBLE attached_mob = parent +// We need to set these to interact with sleeping = gain charges +/datum/component/thaumaturge_preparation/RegisterWithParent() + . = ..() + RegisterSignal(attached_mob, COMSIG_LIVING_STATUS_SLEEP, PROC_REF(on_sleep_set)) + +/datum/component/thaumaturge_preparation/UnregisterFromParent() + UnregisterSignal(attached_mob, COMSIG_LIVING_STATUS_SLEEP) + . = ..() + +/datum/component/thaumaturge_preparation/proc/on_sleep_set(mob/living/source, amount) + SIGNAL_HANDLER + // Do we have queqed changes and is the flag that it passed validation on? + if(applied_prepared_charges && recharge_when_sleep) + //Do we have the focus on our person? + if(locate(/obj/item/book/bible) in attached_mob.get_all_contents()) + apply_spell_charges() + to_chat(attached_mob, span_notice("Your mind focuses on your spells, and through your dreams, you feel your Resonant power recharge!")) + else + to_chat(attached_mob, span_warning("You cannot recharge your spells without a Spell Focus on your person!")) + + // Validates mana and adds spells to the list. -/datum/component/thaumaturge_preparation/proc/validate_spells() +/datum/component/thaumaturge_preparation/proc/build_spells() var/calculated_mana = 0 spell_list = list() for(var/datum/power/power_instance as anything in attached_mob.powers) @@ -62,6 +88,88 @@ return 0 +// Starts the process of applying spells. Verification & all +/datum/component/thaumaturge_preparation/proc/apply_preperation() + if(!check_valid_preperation()) + recharge_when_sleep = FALSE + return + if(first_time_preperation) + if(apply_spell_charges()) + first_time_preperation = FALSE + to_chat(attached_mob, span_warning("Your spell preperation has been applied!")) + else + to_chat(attached_mob, span_warning("Something went wrong when applying spell charges; this shouldn't happen! Yell at a dev!")) + else + // For those curious how we trigger it, its the on_sleep_set() signaler at the top. + recharge_when_sleep = TRUE + to_chat(attached_mob, span_warning("Your changes have been saved! The next time you take the sleep action, the charges will be applied.")) + +// Applies the prepared spell charges. +/datum/component/thaumaturge_preparation/proc/apply_spell_charges() + if(!length(applied_prepared_charges)) + return FALSE + + for(var/datum/power/power in attached_mob.powers) + // Thaumaturge powers only. + if(power.path != POWER_PATH_THAUMATURGE) + continue + + var/datum/action/cooldown/power/thaumaturge/action = power.action_path + if(!action) + continue + + var/charges = applied_prepared_charges["[action.type]"] + if(isnull(charges)) + continue + + action.charges = clamp(charges, 0, action.max_charges) + + // Re-enable the power if it got charges, disable if it has 0 if it has max charges.. + if(action.charges) + action.enable() + else if(action.max_charges) + action.disable() + action.update_charges_overlay() + return TRUE + +// Reverifies that all the things picked for preperation are indeed valid. +/datum/component/thaumaturge_preparation/proc/check_valid_preperation() + var/total_mana_cost = 0 + build_spells() + for(var/prepared_key in applied_prepared_charges) + var/prepared_charges = applied_prepared_charges[prepared_key] + + // find matching action instance on the mob + var/datum/action/cooldown/power/thaumaturge/matching_action = get_applied_charges_matching_power(attached_mob.powers, prepared_key) + if(!matching_action) + to_chat(attached_mob, span_warning("Prepared power '[prepared_key]' not found on you!")) + return FALSE + + // Checks if the amount of charges are valid vs the max_charge + if(matching_action.max_charges < prepared_charges) + to_chat(attached_mob, span_warning("[matching_action]'s charges exceed the maximum!")) + return FALSE + total_mana_cost += (prepared_charges * matching_action.prep_cost) + + // Checks if the total mana cost of all the charges + if(mana < total_mana_cost) + to_chat(attached_mob, span_warning("You have spend more mana than you have!")) + return FALSE + return TRUE + +// Because TGUI gives it along as a string. +/datum/component/thaumaturge_preparation/proc/get_applied_charges_matching_power(list/powers_list, prepared_key) + for(var/datum/power/power in powers_list) + var/datum/action/cooldown/power/thaumaturge/action = power.action_path + if(!action) + continue + + // Becuase prepared key is a string we have to stringify action.type + if("[action.type]" == prepared_key) + return action + + return null + /* Below is responsible for all the TGUI stuff to do with spell preperation. Save yourself if you need to touch this. @@ -90,14 +198,13 @@ /datum/component/thaumaturge_preparation/ui_state(mob/user) - // Pick an appropriate state; many UIs use always_state if only access control is server-side. return GLOB.always_state /datum/component/thaumaturge_preparation/ui_data(mob/living/user) var/list/spells_payload = list() for(var/datum/power/power_instance as anything in spell_list) - var/spell_ref = "[power_instance.action_path]" + var/spell_ref = "[power_instance.action_path.type]" var/current_charges = prepared_charges[spell_ref] if(isnull(current_charges)) current_charges = 0 @@ -145,7 +252,7 @@ var/max_charges_local = 0 for(var/datum/power/power_instance as anything in spell_list) - if("[power_instance.action_path]" != spell_ref) + if("[power_instance.action_path.type]" != spell_ref) continue var/datum/action/cooldown/power/thaumaturge/action_instance = power_instance.action_path @@ -184,6 +291,7 @@ if(action == "apply") applied_prepared_charges = prepared_charges.Copy() + apply_preperation() return TRUE return FALSE 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 0443716fce32e5..d642c088329f5f 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 @@ -40,7 +40,7 @@ if(!prep_component) to_chat(user, span_warning("Something terrible has happened; you're missing your preperation component. Yell at devs!")) return FALSE - prep_component.validate_spells() // We call it here so all the spells are loaded when we open it. + prep_component.build_spells() // We call it here so all the spells are loaded when we open it. prep_component.ui_interact(user) return TRUE From 3d76236e274992e6ec0d743db970ac6b9631ea64 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 31 Jan 2026 13:04:13 +0100 Subject: [PATCH 038/212] Adds affinity to Thaumaturge. --- code/game/objects/items.dm | 2 + .../thaumaturge/_thaumaturge_action.dm | 47 +++++++++++++++++-- .../thaumaturge/_thaumaturge_preperation.dm | 2 +- .../affinity/thaumaturge_affinity.dm | 15 ++++++ .../affinity/thaumaturge_spell_focus.dm | 41 ++++++++++++++++ .../sorcerous/thaumaturge/phantasmal_tool.dm | 3 +- tgstation.dme | 2 + 7 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 2cc4c189cb4ee0..75ea737dcd1d55 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -70,6 +70,8 @@ // See above; sort by TOP PRIORITY to BOTTOM PRIORITY with the bodyshapes as keys (DIGI | FEMME > DIGI > FEMME > HUMANOID) // !!KEYS IN THIS SHOULD BE IDENTICAL TO SUPPORTED_BODYSHAPES!! var/list/bodyshape_icon_files + // Used for the affinity system in the Powers system, by Thaumaturge. + var/affinity = 0 /// DOPPLER SHIFT ADDITION END /* !!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!! 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 23f52ed3ceaf0b..f9aeb1d428b5e2 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 @@ -18,16 +18,22 @@ // Overlay that shows the number of charges var/mutable_appearance/charge_overlay + // How much affinity is currently affecting the action. + var/affinity + // How much affinity is required to use the action. + var/required_affinity + /datum/action/cooldown/power/thaumaturge/New() if(max_charges) disable() // prep your spells first update_charges_overlay() -/datum/action/cooldown/power/thaumaturge/Trigger(mob/clicker, trigger_flags, atom/target) - SHOULD_CALL_PARENT(TRUE) - if(!check_if_valid()) - return FALSE +/datum/action/cooldown/power/thaumaturge/try_use(mob/living/user, atom/target) . = ..() + if(!check_if_valid()) // checks for charges + return FALSE + if(ishuman(user)) // We're not checking for clothes on cats + affinity = get_affinity(user) /datum/action/cooldown/power/thaumaturge/on_action_success(mob/living/user, atom/target) SHOULD_CALL_PARENT(TRUE) @@ -42,6 +48,38 @@ var/cap_to = isnum(override_cap) ? override_cap : max_charges charges = clamp(charges + amount, 0, cap_to) +/* + Affinity system stuff here. Dress like a mage, get bonuses. +*/ +/datum/action/cooldown/power/thaumaturge/proc/get_affinity(mob/living/user) + var/highest_affinity = 0 + + // Checks if you're wearing items with affinity. This has to be clothing; wearing your staff does not count. + var/list/equipped_items = user.get_equipped_items() + for(var/obj/item/equipped_item as anything in equipped_items) + if(!equipped_item) + continue + if(!istype(equipped_item, /obj/item/clothing)) + continue + + var/obj/item/clothing/equipped_clothing = equipped_item + if(equipped_clothing.affinity > highest_affinity) + highest_affinity = equipped_clothing.affinity + + // Checks if you're holding items with affinity. + for(var/obj/item/held_item as anything in user.held_items) + if(!held_item) + continue + + // Holding clothing shouldn't contribute + if(istype(held_item, /obj/item/clothing)) + continue + + if(held_item.affinity > highest_affinity) + highest_affinity = held_item.affinity + + return highest_affinity + /* Deviating massively from the original cooldown system, thaumaturge has charges they have to prepare and plan for in advance, just like the classic vanician spellcasting system. Mechanically, we check if charges are 0. If so we Disable(). Otherwise, we deduct a charge and go on a short 1 second (or whatever is programmed in) cooldown. @@ -88,4 +126,3 @@ - diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm index e74e94be2f0f28..9a40de7694b100 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm @@ -50,7 +50,7 @@ //Do we have the focus on our person? if(locate(/obj/item/book/bible) in attached_mob.get_all_contents()) apply_spell_charges() - to_chat(attached_mob, span_notice("Your mind focuses on your spells, and through your dreams, you feel your Resonant power recharge!")) + to_chat(attached_mob, span_notice("Your mind focuses on your spells, and through your dreams, you feel your Thaumaturge powers recharge!")) else to_chat(attached_mob, span_warning("You cannot recharge your spells without a Spell Focus on your person!")) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm new file mode 100644 index 00000000000000..e21d8d1235fca9 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm @@ -0,0 +1,15 @@ +/* So, affinity is a system that applies a value to objects; and the amount of affinity is based on the item. +We have to apply this retroactively to existing items, which is what this file is for. If you make something new, include it as a var instead. +*/ + +/* Rule of thumb on what belongs in what category: +Tier 1 Affinity are things vaguely magical. A jester's hat, a cape, in essence any form of slightly mystical drip falls in this category. This is all you need to get access to most non-flashy, non-combat spells. +Tier 2 Affinity are things that are usually more obvious but not cumbersome. This category is primarily populated by any Spell Focus item that can fit in a backpack (pondering your orbs and the likes), or clearly magical items that aren't pronounced on the sprite (such as amulets). +Tier 3 Affinity is where you really start dressing magically. The job-specific thaumaturge robes, for example Security's, have this tier of affinity. Usually these robes provide some degree of utility, such as armor. This includes body and head slots. Spell Focuses, such as wands, that can't fit in the backpack but can fit in the belt/suit slot also go here. Most spells will cap out at this requirement. +Tier 4 Affinity is basically the full wizard dress. Any wizard robes without significant side-effects, such as the costume wizard robes, satisfy this condition. +Tier 5 Affinity is specially reserved for Spell Focus staves, which can only be worn on the back or held in hand. In turn, holding it gets you great power. Just make sure not to lose it. +Whilst Tier 5 is the cap for normal player content, Antagonist and other rare equipment can exceed these affinity tiers. Steal an actual Wizard's hat and you may just get your hands on a Tier 7 item. +*/ + +/obj/item/clothing/suit/wizrobe + affinity = 4 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm new file mode 100644 index 00000000000000..72adc5a53fef72 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm @@ -0,0 +1,41 @@ +/obj/item/spell_focus + name = "thaumaturge's spell focus" + desc = "An orb of raw thaumaturgic resonance, adjustable to take on any form of your choosing. Needed to restore thaumaturgic powers." + icon = 'icons/obj/weapons/guns/projectiles.dmi' + icon_state = "ice_1" + slot_flags = ITEM_SLOT_BELT + w_class = WEIGHT_CLASS_TINY + obj_flags = UNIQUE_RENAME + affinity = 2 // check thaumaturge_affinity.dm if you ever wonder what deserves what affinity + /// Short description of what this item is capable of, for radial menu uses. + var/menu_description = "An orb of energy. Fits in pockets. Can be worn on the belt. Very convenient and not visible in your hands, but doesn't do much more than that." + +/obj/item/spell_focus/wand + name = "thaumaturge's wand" + desc = "A pointy stick, attuned to work with thaumaturgic resonance. Capable of restoring thaumaturgic powers when resting." + icon = 'icons/obj/weapons/guns/magic.dmi' + icon_state = "nothingwand-drained" + inhand_icon_state = "wand" + lefthand_file = 'icons/mob/inhands/items_lefthand.dmi' + righthand_file = 'icons/mob/inhands/items_righthand.dmi' + slot_flags = ITEM_SLOT_BELT + w_class = WEIGHT_CLASS_NORMAL + affinity = 3 + menu_description = "A classical magic wand. Fits in your backpack and on your belt, and provides more affinity than the orb; but does not fit in any pockets and is clearly visible when held." + +/obj/item/spell_focus/staff + name = "thaumaturge's staff" + desc = "A big ol' staff, attuned to work with thaumaturgic resonance. Makes for an excellent focus for thaumaturgic powers, and is capable of restoring thaumaturgic powers when resting." + icon = 'icons/obj/weapons/staff.dmi' + icon_state = "godstaff-blue" + inhand_icon_state = "godstaff-blue" + icon_angle = -45 + lefthand_file = 'icons/mob/inhands/weapons/staves_lefthand.dmi' + righthand_file = 'icons/mob/inhands/weapons/staves_righthand.dmi' + w_class = WEIGHT_CLASS_HUGE + force = 7 + slot_flags = ITEM_SLOT_BACK + affinity = 5 + menu_description = "A staff with an orb on the end. Because it is bulky, it can only be stored in the back slot, but offers a very high amount of Affinity in return. As well as being very apt for whacking youngsters." + + diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm index 8968ca292e1e79..c9f3d03644074e 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm @@ -19,7 +19,8 @@ button_icon_state = "tomealtar" max_charges = 5 + required_affinity = 1 /datum/action/cooldown/power/thaumaturge/phantasmal_tool/use_action(mob/living/user, atom/target) - to_chat(user, span_notice("hello WORLD")) + to_chat(user, span_notice("hello WORLD [affinity]")) return TRUE diff --git a/tgstation.dme b/tgstation.dme index 70e9b24b9928f8..dc48eb5af77e29 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7438,6 +7438,8 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_action.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_power.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_preperation.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_spell_focus.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_affinity.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\phantasmal_tool.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_action.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_power.dm" From f91b7c50a5860ed1445bce488a60714b08e44bfd Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 31 Jan 2026 18:13:38 +0100 Subject: [PATCH 039/212] Finalized Thaumaturge mechanics; finished phantasmal tool as a spell. --- .../thaumaturge/_thaumaturge_action.dm | 9 +- .../thaumaturge/_thaumaturge_preperation.dm | 6 +- .../thaumaturge/_thaumaturge_root.dm | 1 + .../affinity/thaumaturge_affinity.dm | 2 +- .../sorcerous/thaumaturge/phantasmal_tool.dm | 109 +++++++++++++++++- 5 files changed, 115 insertions(+), 12 deletions(-) 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 f9aeb1d428b5e2..e2951ed9417677 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 @@ -29,16 +29,19 @@ update_charges_overlay() /datum/action/cooldown/power/thaumaturge/try_use(mob/living/user, atom/target) - . = ..() if(!check_if_valid()) // checks for charges return FALSE if(ishuman(user)) // We're not checking for clothes on cats affinity = get_affinity(user) + if(affinity < required_affinity) // Do we have the minimal required affinity + owner.balloon_alert(user, "requires [required_affinity] affinity!") + return FALSE + . = ..() -/datum/action/cooldown/power/thaumaturge/on_action_success(mob/living/user, atom/target) +/datum/action/cooldown/power/thaumaturge/on_action_success(mob/living/user, atom/target, override_charges) SHOULD_CALL_PARENT(TRUE) . = ..() - adjust_charges(-charges_to_use) + adjust_charges(isnull(override_charges) ? -charges_to_use : -override_charges) check_if_valid() return diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm index 9a40de7694b100..19fd0f4bcc0c12 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm @@ -48,11 +48,11 @@ // Do we have queqed changes and is the flag that it passed validation on? if(applied_prepared_charges && recharge_when_sleep) //Do we have the focus on our person? - if(locate(/obj/item/book/bible) in attached_mob.get_all_contents()) + for(var/obj/item/spell_focus/focus_item in attached_mob.get_all_contents()) apply_spell_charges() to_chat(attached_mob, span_notice("Your mind focuses on your spells, and through your dreams, you feel your Thaumaturge powers recharge!")) - else - to_chat(attached_mob, span_warning("You cannot recharge your spells without a Spell Focus on your person!")) + return + to_chat(attached_mob, span_warning("You cannot recharge your spells without a Spell Focus on your person!")) // Validates mana and adds spells to the list. 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 d642c088329f5f..fb055154204537 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 @@ -27,6 +27,7 @@ return // Spell preperation is so complicated we basically handle it all in a component, including the UI part. power_holder.AddComponent(/datum/component/thaumaturge_preparation, power_holder) + power_holder.put_in_hands(new /obj/item/spell_focus) . = ..() /datum/action/cooldown/power/thaumaturge/thaumaturge_root diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm index e21d8d1235fca9..7670e1f2f2842c 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 @@ -5,7 +5,7 @@ We have to apply this retroactively to existing items, which is what this file i /* Rule of thumb on what belongs in what category: Tier 1 Affinity are things vaguely magical. A jester's hat, a cape, in essence any form of slightly mystical drip falls in this category. This is all you need to get access to most non-flashy, non-combat spells. Tier 2 Affinity are things that are usually more obvious but not cumbersome. This category is primarily populated by any Spell Focus item that can fit in a backpack (pondering your orbs and the likes), or clearly magical items that aren't pronounced on the sprite (such as amulets). -Tier 3 Affinity is where you really start dressing magically. The job-specific thaumaturge robes, for example Security's, have this tier of affinity. Usually these robes provide some degree of utility, such as armor. This includes body and head slots. Spell Focuses, such as wands, that can't fit in the backpack but can fit in the belt/suit slot also go here. Most spells will cap out at this requirement. +Tier 3 Affinity is where you really start dressing magically. The job-specific thaumaturge robes, for example Security's, have this tier of affinity. Usually these robes provide some degree of utility, such as armor. This includes body and head slots. Spell Focuses, such as wands, that can't fit in the pocket but can fit in the belt/suit slot also go here. Most spells will cap out at this requirement. Tier 4 Affinity is basically the full wizard dress. Any wizard robes without significant side-effects, such as the costume wizard robes, satisfy this condition. Tier 5 Affinity is specially reserved for Spell Focus staves, which can only be worn on the back or held in hand. In turn, holding it gets you great power. Just make sure not to lose it. Whilst Tier 5 is the cap for normal player content, Antagonist and other rare equipment can exceed these affinity tiers. Steal an actual Wizard's hat and you may just get your hands on a Tier 7 item. diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm index c9f3d03644074e..8d55545bc970e6 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm @@ -3,7 +3,7 @@ */ /datum/power/thaumaturge/phantasmal_tool name = "Phantasmal Tool" - desc = "Summons a basic tool of your choice in your hand, that disappears if dropped or after a duration. Requires Affinity 1 to cast. Affinity gives a chance to not consume charges on cast." + desc = "Summons a basic tool of your choice in your hand, that disappears after a duration, or if it is dropped/used to attack a person. Requires Affinity 1 to cast. Affinity gives a chance to not consume charges on cast." value = 3 archetype = POWER_ARCHETYPE_SORCEROUS @@ -14,13 +14,112 @@ /datum/action/cooldown/power/thaumaturge/phantasmal_tool name = "Phantasmal Tool" - desc = "Summons a basic tool of your choice in your hand, that disappears if dropped or after a duration. Has a chance not to consume charges." - button_icon = 'icons/obj/antags/cult/structures.dmi' - button_icon_state = "tomealtar" + desc = "Summons a basic tool of your choice in your hand, that disappears after a duration, or if it is dropped/used to attack a person." + button_icon = 'icons/obj/weapons/club.dmi' + button_icon_state = "hypertool" max_charges = 5 required_affinity = 1 /datum/action/cooldown/power/thaumaturge/phantasmal_tool/use_action(mob/living/user, atom/target) - to_chat(user, span_notice("hello WORLD [affinity]")) + if(user.get_active_held_item() && user.get_inactive_held_item()) + user.balloon_alert(user, "hands are not empty!") + + var/list/tool_type_to_image = get_phantasmal_tool_radial_images() + + // No anchor, no require_near, no adjacency gate. + // show_radial_menu returns the key from the assoc list (here: a typepath). + var/selected_tool_type = show_radial_menu( + user, + user, // "anchor" just for placement; using the user keeps it simple + tool_type_to_image, + tooltips = TRUE + ) + + if(!selected_tool_type) + return FALSE + + // Creates item, adds the special phantasmal tool properties, give to user. + var/obj/item/new_tool_item = new selected_tool_type(user) + new_tool_item.AddElement(/datum/element/phantasmal_tool) + if(!user.put_in_hands(new_tool_item)) + qdel(new_tool_item) // destroys the item if it fails to put it in our hands, as it shouldn't ever exist out of hands. + return TRUE + +// To potentially refund it, we run a small check. +/datum/action/cooldown/power/thaumaturge/on_action_success(mob/living/user, atom/target, override_charges) + var/chance_to_refund = clamp(11 * affinity + 30, 20, 100) // Caps out at 85 for T5. + if(prob(chance_to_refund)) + override_charges = 0 + to_chat(owner, span_notice("Your [name] spell did not consume a charge!")) + else if(chance_to_refund >= 51) // At this point it's more common that it does not consume a charge, so we invert them and tell them when it does consume a charge! + to_chat(owner, span_warning("Your [name] spell consumed a charge!")) + return ..(user, target, override_charges) + +/datum/action/cooldown/power/thaumaturge/proc/phantasmal_tool_menu_check(mob/user) + if(!istype(user)) + return FALSE + if(user.incapacitated) + return FALSE + return TRUE + +/datum/action/cooldown/power/thaumaturge/proc/get_phantasmal_tool_radial_images() + var/static/list/tool_type_to_image + if(tool_type_to_image) + return tool_type_to_image + + tool_type_to_image = list() + + var/list/allowed_tool_types = list( + /obj/item/weldingtool, + /obj/item/screwdriver, + /obj/item/wirecutters, + /obj/item/crowbar, + /obj/item/wrench, + /obj/item/multitool + ) + + for(var/tool_type in allowed_tool_types) + // One-time temporary instance to fetch icon/icon_state reliably + var/obj/item/temporary_tool = new tool_type + tool_type_to_image[tool_type] = image(temporary_tool.icon, temporary_tool.icon_state) + qdel(temporary_tool) + + + return tool_type_to_image + + +// The element we attach with phantasmal tool. Handles making it harmless, duration and disappearing on. +/datum/element/phantasmal_tool + element_flags = ELEMENT_DETACH_ON_HOST_DESTROY + + // The item we're attached to. + var/obj/item/attached_item + +/datum/element/phantasmal_tool/Attach(datum/target) + . = ..() + attached_item = target + attached_item.alpha = 200 + attached_item.color = "#66cbdd" + attached_item.force = 0 + attached_item.AddElementTrait(TRAIT_ON_HIT_EFFECT, REF(src), /datum/element/on_hit_effect) + RegisterSignal(attached_item, COMSIG_ON_HIT_EFFECT, PROC_REF(break_on_hit)) + RegisterSignal(attached_item, COMSIG_ITEM_DROPPED, PROC_REF(on_item_dropped)) + +/datum/element/phantasmal_tool/Detach(datum/source) + UnregisterSignal(source, COMSIG_ON_HIT_EFFECT) + UnregisterSignal(source, COMSIG_ITEM_DROPPED) + REMOVE_TRAIT(source, TRAIT_ON_HIT_EFFECT, REF(src)) + return ..() + +// Checks if the item is no longer in our hands. If so, destroy hte item. +/datum/element/phantasmal_tool/proc/on_item_dropped(datum/source, mob/user) + SIGNAL_HANDLER + qdel(attached_item) + +/datum/element/phantasmal_tool/proc/break_on_hit(datum/source, atom/damage_target, hit_zone, throw_hit) + SIGNAL_HANDLER + if(ismob(damage_target)) + playsound(attached_item, 'sound/items/ceramic_break.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + qdel(attached_item) From 79afddf0ca150004b4c7f8da1e22e0cd99acd93f Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 6 Feb 2026 11:25:56 +0100 Subject: [PATCH 040/212] Added resonance immunity as a system, added projectile support, added gale blast as a thaumaturge spell, tweaked some other misc things. --- code/__DEFINES/~doppler_defines/powers.dm | 3 + .../resonant/aberrant/resonant_immune.dm | 18 ++ .../code/powers/resonant/silence_trauma.dm | 5 +- .../powers/sorcerous/_resonant_projectile.dm | 35 ++++ .../thaumaturge/_thaumaturge_action.dm | 9 +- .../thaumaturge/_thaumaturge_preperation.dm | 2 +- .../thaumaturge/_thaumaturge_root.dm | 2 +- .../sorcerous/thaumaturge/blend_for_me.dm | 136 ++++++++++++++ .../sorcerous/thaumaturge/gale_blast.dm | 175 ++++++++++++++++++ .../sorcerous/thaumaturge/phantasmal_tool.dm | 15 +- .../modular_powers/code/powers_action.dm | 106 ++++++++++- .../code/powers_magic_immunity.dm | 43 +++++ tgstation.dme | 5 + 13 files changed, 529 insertions(+), 25 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm create mode 100644 modular_doppler/modular_powers/code/powers_magic_immunity.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 5d6407fec34de7..10906b18694085 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -37,6 +37,9 @@ // Trait for when you are unable to use resonant powers #define TRAIT_RESONANCE_SILENCED "RESONANCE_SILENCED" +// Trait for when you are immune to resonant powers +#define TRAIT_ANTIRESONANCE "TRAIT_ANTIRESONANCE" + /** * SORCEROUS * All defines related to the sorcerous archetype. diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm new file mode 100644 index 00000000000000..784d6922ebe79b --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm @@ -0,0 +1,18 @@ +/* + You're immune to resonant antics! But also you're permanently silenced. +*/ +/datum/power/aberrant/anomaly/counter_resonance + name = "Counter-Resonance Anomaly" + desc = "You have a counteractive effect on resonance-based phenomena. You are immune to resonance-based magic (but not normal magic!), and you cannot use any resonance-based powers." + value = 9 + + archetype = POWER_ARCHETYPE_RESONANT + path = POWER_PATH_ABERRANT + +/datum/power/aberrant/anomaly/counter_resonance/add() + ADD_TRAIT(power_holder, TRAIT_ANTIRESONANCE, src) + ADD_TRAIT(power_holder, TRAIT_RESONANCE_SILENCED, src) + +/datum/power/aberrant/anomaly/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/silence_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm index 1607710619df96..eda31526ee681b 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm @@ -14,10 +14,9 @@ for(var/datum/power/power_instance in powers_list) power_instance.dispel() - // TODO: actually make the silence work (the spell_requirements code scares me) - //ADD_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAUMA_TRAIT) + ADD_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAUMA_TRAIT) . = ..() /datum/brain_trauma/magic/resonance_silenced/on_lose() - //REMOVE_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAUMA_TRAIT) + REMOVE_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAUMA_TRAIT) ..() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm b/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm new file mode 100644 index 00000000000000..0d0a77a3ddea38 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm @@ -0,0 +1,35 @@ +// Ideally if you make projectiles for this system that are resonant based, you use this one to actually auto-handle the antimagic stuff. +// Otherwise this is largely similar to obj/projectile/magic +/obj/projectile/resonant + name = "bolt" + icon_state = "energy" + damage = 0 // MOST magic projectiles pass the "not a hostile projectile" test, despite many having negative effects + damage_type = OXY + armour_penetration = 100 + armor_flag = NONE + /// determines what type of antimagic can block the spell projectile. + // We have to play coy with the existing magic resistance system, for checking against resonance use victim.can_block_resonance(antimagic_charge_cost) + var/antimagic_flags = MAGIC_RESISTANCE + /// determines the drain cost on the antimagic item + var/antimagic_charge_cost = 1 + + // The power that made the projectile. + var/datum/action/cooldown/power/creating_power + +// TODO: actually uhh, add resonant anti-magic to this lmao. +/obj/projectile/resonant/prehit_pierce(atom/target) + . = ..() + + if(isliving(target)) + var/mob/living/victim = target + if(victim.can_block_magic(antimagic_flags, antimagic_charge_cost) || victim.can_block_resonance(antimagic_charge_cost)) + visible_message(span_warning("[src] fizzles on contact with [victim]!")) + return PROJECTILE_DELETE_WITHOUT_HITTING + + if(istype(target, /obj/machinery/hydroponics)) // even plants can block antimagic + var/obj/machinery/hydroponics/plant_tray = target + if(!plant_tray.myseed) + return + if(plant_tray.myseed.get_gene(/datum/plant_gene/trait/anti_magic)) + visible_message(span_warning("[src] fizzles on contact with [plant_tray]!")) + return PROJECTILE_DELETE_WITHOUT_HITTING 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 e2951ed9417677..2020fdf657d85c 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 @@ -6,7 +6,7 @@ // We generally don't dabble with cooldowns but a GCD of 0.5 seconds is kinda handy to prevent you from blowing your load on all your charges by accident. cooldown_time = 5 - // Unlike normal spells, we have charges. More of that explained below at action_success() + // Unlike normal spells, we have charges. More of that explained below at check_if_valid() var/charges = 0 // The cap on charges; you can't prepare more than these. If you leave this null, the spell will not interact with the charges system. var/max_charges @@ -18,7 +18,7 @@ // Overlay that shows the number of charges var/mutable_appearance/charge_overlay - // How much affinity is currently affecting the action. + // How much affinity is currently affecting the action. It is deliberate we snap-shot this on cast. var/affinity // How much affinity is required to use the action. var/required_affinity @@ -38,6 +38,9 @@ return FALSE . = ..() +// The charge deduction is handled on_action_success and thusly gains override_charges as an arg. +// If your spell does anything unusual with charges such as refunds or costing multiples, this is where you would handle that. +// You can otherwise use on_action_success as normal, just make sure to call parrent. /datum/action/cooldown/power/thaumaturge/on_action_success(mob/living/user, atom/target, override_charges) SHOULD_CALL_PARENT(TRUE) . = ..() @@ -85,7 +88,7 @@ /* Deviating massively from the original cooldown system, thaumaturge has charges they have to prepare and plan for in advance, just like the classic vanician spellcasting system. - Mechanically, we check if charges are 0. If so we Disable(). Otherwise, we deduct a charge and go on a short 1 second (or whatever is programmed in) cooldown. + Mechanically, we check if charges are 0. If so we Disable(). Otherwise, we deduct a charge and go on a short cooldown. */ // Checks if we have charges to use. diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm index 19fd0f4bcc0c12..b8d0259c1d0497 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm @@ -67,7 +67,7 @@ if(check_if_can_prepare(power_instance.action_path)) spell_list.Add(power_instance) calculated_mana += power_instance.value - mana = clamp(calculated_mana, 0, max_mana) + mana = clamp(calculated_mana * 2, 0, max_mana) // Checks if we can prepare the spell in our spellbook and if so adds it to the spell list. /datum/component/thaumaturge_preparation/proc/check_if_can_prepare(action_type) 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 fb055154204537..aaf125c9eb16f2 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 @@ -27,7 +27,7 @@ return // Spell preperation is so complicated we basically handle it all in a component, including the UI part. power_holder.AddComponent(/datum/component/thaumaturge_preparation, power_holder) - power_holder.put_in_hands(new /obj/item/spell_focus) + power_holder.put_in_hands(new /obj/item/spell_focus/staff) // TODO: Change to normal spell focus item after testing. . = ..() /datum/action/cooldown/power/thaumaturge/thaumaturge_root diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm new file mode 100644 index 00000000000000..4253d28feb9262 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm @@ -0,0 +1,136 @@ +// Will it blend? +// Emulates the effects of a grinder on the target in your hand. Can be used offensively too through aggressive grabs. +// TODO: Test on undersized characters. + +/datum/power/thaumaturge/blend_for_me + name = "Blend For Me" + desc = "Grinds the item in your hand as if it were inserted in a grinder, then conjures a glass to hold it (if you're grinding). Right-hand for grinding, left-hand for juicing. Can be used on people using an aggressive grab to inflict brute damage and bleeding. \ + Affinity gives a chance to not consume charges." + value = 3 + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THAUMATURGE + action_path = /datum/action/cooldown/power/thaumaturge/blend_for_me + required_powers = list(/datum/power/thaumaturge_root) + +/datum/action/cooldown/power/thaumaturge/blend_for_me + name = "Blend For Me" + desc = "The younger cousin of another notorious spell; grinds the item in your hand as if it were inserted in a grinder, then conjures a glass to hold it (if you're grinding). Right-hand for grinding, left-hand for juicing. Can be used on people using an aggressive grab to inflict brute damage and bleeding." + button_icon = 'icons/obj/machines/kitchen.dmi' + button_icon_state = "juicer" + + cooldown_time = 50 // we don't want people spamming the blender noise. that's it. that's the whole reason why we force a 5 second cooldown. + max_charges = 6 + required_affinity = 1 + + // The grab damage. + // TODO: define + var/grab_blend_brute = 15 + +/datum/action/cooldown/power/thaumaturge/blend_for_me/use_action(mob/living/user, atom/target) + // Are we grinding or juicing? + var/grinding + // What item is in our active hand? + var/obj/item/active_held_item = user.get_active_held_item() + if(!active_held_item) + return FALSE + + // Is the item too big? + if(active_held_item.w_class >= WEIGHT_CLASS_BULKY) + user.balloon_alert(user, "Too large to blend!") + return FALSE + + // Which hand is active? + var/held_index = user.get_held_index_of_item(active_held_item) + if(!held_index) + return FALSE + var/active_is_right_hand = IS_RIGHT_INDEX(held_index) + if(active_is_right_hand) // If its in the right hand we grind, otherwise we blend. + grinding = TRUE + + return will_it_blend(user, active_held_item, grinding) + + +// Attempts to blend the item. +/datum/action/cooldown/power/thaumaturge/blend_for_me/proc/will_it_blend(mob/living/user, obj/item/input_item, grinding) + // Start cooldown immediately (anti-spam) + StartCooldown() + + // FX + user.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 40) + playsound(user, grinding ? 'sound/machines/blender.ogg' : 'sound/machines/juicer.ogg', 50, TRUE) + + // Channel + if(!do_after(user, 4 SECONDS, target = user)) + return FALSE + + // Temporary buffer to house the results so we can neatly transfer it to the same hand. + var/turf/user_turf = get_turf(user) + if(!user_turf) + return FALSE + + var/obj/effect/abstract/thaum_blend_buffer/buffer = new(user_turf, 30) + + // We reject multiple stacks of items for now. Despite multiple attempts, it seems to just NOT WORK?! + // If you can figure out how, please do :) + if(istype(input_item, /obj/item/stack)) + var/obj/item/stack/stack_item = input_item + if(stack_item.amount > 1) + user.balloon_alert(user, "Split the stack first!") + return FALSE + + var/success + // The blending process + if(grinding) + success = input_item.grind(buffer.reagents, user) + else + success = input_item.juice(buffer.reagents, user) + + if(!success) // If it somehow fails to grind/juice + user.balloon_alert(user, "[input_item] resists being processed!") + qdel(buffer) + return FALSE + + if(buffer.reagents.total_volume <= 0) // If somehow we grind something but ntohing comes out. + user.balloon_alert(user, "Nothing useful comes out.") + qdel(buffer) + return FALSE + + // Conjure bottle AFTER grind so hands are likely freed + var/obj/item/reagent_containers/cup/glass/bottle/small/result_bottle = new(user_turf) + user.put_in_hands(result_bottle) + + // Transfer contents + buffer.reagents.trans_to(result_bottle, buffer.reagents.total_volume, transferred_by = user) + qdel(buffer) + + return TRUE + +// To potentially refund it, we run a small check. +/datum/action/cooldown/power/thaumaturge/blend_for_me/on_action_success(mob/living/user, atom/target, override_charges) + var/chance_to_refund = clamp(11 * affinity + 30, 20, 100) // Caps out at 85 for T5. + if(prob(chance_to_refund)) + override_charges = 0 + to_chat(owner, span_notice("Your [name] spell did not consume a charge!")) + else if(chance_to_refund >= 51) // At this point it's more common that it does not consume a charge, so we invert them and tell them when it does consume a charge! + to_chat(owner, span_warning("Your [name] spell consumed a charge!")) + return ..(user, target, override_charges) + + +// We create a temporary buffer for holding the reagents, given that our 'blender' in this case isn't a conventional object. +/obj/effect/abstract/thaum_blend_buffer + name = "resonant blender" + desc = "You think you're so fancy seeing invisible coder objects huh? Reaaal magician right here." + invisibility = INVISIBILITY_ABSTRACT + anchored = TRUE + density = FALSE + + var/datum/reagents/reagent_buffer + var/buffer_volume = 50 + +/obj/effect/abstract/thaum_blend_buffer/Initialize(mapload, new_buffer_volume) + . = ..() + if(isnum(new_buffer_volume) && new_buffer_volume > 0) + buffer_volume = new_buffer_volume + reagents = new /datum/reagents(buffer_volume, src) + reagents.flags = TRANSPARENT | DRAINABLE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm new file mode 100644 index 00000000000000..ad8d0462811ae5 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm @@ -0,0 +1,175 @@ +/* + Shoots a blast of wind for various neat purposes; mostly just to push your co-workers around and the occasional fire. + Perfectly encapsulates the design philosophy of thaumaturge spells being big on util and not specifically good at one thing. +*/ + +// Maximum amount of items we can push with this spell. +#define THAUMATURGE_GALE_BLAST_PUSH_LIMIT 20 + +/datum/power/thaumaturge/gale_blast + name = "Gale Blast" + desc = "Shoots forth a blast of wind. The blast keeps traveling until it hits a solid structure, extinguishing any fires and dragging along any items with it. If it hits a creature, it knocks them back 3 spaces and extinguishes them. Requires Affinity 3. Extra affinity gives a chance to knockback further." + value = 3 + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THAUMATURGE + action_path = /datum/action/cooldown/power/thaumaturge/gale_blast + required_powers = list(/datum/power/thaumaturge_root) + +/datum/action/cooldown/power/thaumaturge/gale_blast + name = "Gale blast" + desc = "Shoots forth a blast of wind. The blast keeps traveling until it hits a solid structure, extinguishing any fires and dragging along any items with it. If it hits a creature, it knocks them back 3 spaces and extinguishes them." + button_icon = 'icons/effects/effects.dmi' + button_icon_state = "smoke" + + max_charges = 6 + required_affinity = 3 + prep_cost = 3 + projectile_type = /obj/projectile/resonant/gale_blast + click_to_activate = TRUE + +/datum/action/cooldown/power/thaumaturge/use_action(mob/living/user, atom/target) + . = ..() + playsound(user, 'sound/effects/podwoosh.ogg', 60, TRUE, SILENCED_SOUND_EXTRARANGE) + + +// The projectile itself +/obj/projectile/resonant/gale_blast + name = "gale blast" + icon = 'icons/effects/effects.dmi' + icon_state = "smoke" + + // Tweak as needed. + var/knockback_range = 3 + +// Code for dragging along objects. +/obj/projectile/resonant/gale_blast/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change) + . = ..() + + var/turf/current_turf = get_turf(src) + if(!current_turf) + return + + // Handless moving objects along with it. + drag_along_movables(current_turf, dir) + // Extinguishes hotspots. Doesn't mess with atmos. + extinguish_hotspots_on_turf(current_turf) + +/obj/projectile/resonant/gale_blast/proc/drag_along_movables(turf/current_turf, travel_dir) + if(!current_turf || !travel_dir) + return + + var/turf/next_turf = get_step(current_turf, travel_dir) + if(!next_turf) + return + + var/pushed_atoms = 0 + + // Checks if we're allowed to drag it and if the space can be passed through. + for(var/atom/movable/movable_instance as anything in current_turf) + // We cap the amount of items that can be moved similar to push brooms to prevent you from casting LAGIMUS MAXIMUS. + if(pushed_atoms >= THAUMATURGE_GALE_BLAST_PUSH_LIMIT) + break + + if(!can_wind_drag(movable_instance, current_turf)) + continue + + if(!movable_instance.CanPass(movable_instance, next_turf, travel_dir)) + continue + + // Drags along the object. + movable_instance.Move(next_turf) + // Also extinguishes it. + movable_instance.extinguish() + pushed_atoms++ + +// Checks if we can drag along the target. +/obj/projectile/resonant/gale_blast/proc/can_wind_drag(atom/movable/movable_instance, turf/current_turf) + if(!movable_instance) + return FALSE + + // Core rule: anchored objects do not move + if(movable_instance.anchored) + return FALSE + + // Do not drag living mobs; knockback is handled separately + if(isliving(movable_instance)) + return FALSE + + // Only drag things actually sitting on the turf + if(movable_instance.loc != current_turf) + return FALSE + + return TRUE + +// Extinguishes fire in the target space. +/obj/projectile/resonant/gale_blast/proc/extinguish_hotspots_on_turf(turf/current_turf) + if(!current_turf) + return + + for(var/obj/effect/hotspot/hotspot_instance as anything in current_turf) + if(hotspot_instance.type != /obj/effect/hotspot) // only delete fires! + continue + qdel(hotspot_instance) + +/* + On hit effects below +*/ + +// Helpers functions do most of the work here. +/obj/projectile/resonant/gale_blast/on_hit(atom/target, blocked, pierce_hit) + . = ..() + extinguish_hit_target(target) + apply_knockback(target) + + +// Handles the knockback on hit +/obj/projectile/resonant/gale_blast/proc/apply_knockback(atom/hit_atom) + var/atom/movable/movable_target = hit_atom + if(!istype(movable_target)) + return + + if(movable_target.anchored) + return + + var/turf/target_turf = get_turf(movable_target) + if(!target_turf) + return + + // Knockback direction = projectile travel direction at impact + var/knockback_dir = dir + if(!knockback_dir) + return + + var/turf/destination_turf = target_turf + for(var/step_count in 1 to 3) + var/turf/next_turf = get_step(destination_turf, knockback_dir) + if(!next_turf) + break + destination_turf = next_turf + + // chance to knockback slightly farther based on affinity. + // This is really ugly. + var/knockback_dist = knockback_range + var/datum/action/cooldown/power/thaumaturge/power = creating_power + var/affinity = power.affinity + var/extra_knockback_chance = clamp(25 * (affinity - 3), 0, 100) // Caps out at 50 for T5. + + if(prob(extra_knockback_chance)) + knockback_dist += 1 + + movable_target.safe_throw_at(destination_turf, knockback_dist, 2, firer) + playsound(movable_target, 'sound/effects/bamf.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + +// Extinguishes the target we just hit. +/obj/projectile/resonant/gale_blast/proc/extinguish_hit_target(atom/hit_atom) + if(!hit_atom) + return + + if(isliving(hit_atom)) + var/mob/living/living_target = hit_atom + living_target.extinguish_mob() + return + + // Items / other atoms that can burn + hit_atom.extinguish() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm index 8d55545bc970e6..bfa795055b4341 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm @@ -10,7 +10,6 @@ path = POWER_PATH_THAUMATURGE action_path = /datum/action/cooldown/power/thaumaturge/phantasmal_tool required_powers = list(/datum/power/thaumaturge_root) - required_allow_subtypes = TRUE /datum/action/cooldown/power/thaumaturge/phantasmal_tool name = "Phantasmal Tool" @@ -18,8 +17,9 @@ button_icon = 'icons/obj/weapons/club.dmi' button_icon_state = "hypertool" - max_charges = 5 + max_charges = 7 required_affinity = 1 + prep_cost = 2 /datum/action/cooldown/power/thaumaturge/phantasmal_tool/use_action(mob/living/user, atom/target) if(user.get_active_held_item() && user.get_inactive_held_item()) @@ -27,8 +27,7 @@ var/list/tool_type_to_image = get_phantasmal_tool_radial_images() - // No anchor, no require_near, no adjacency gate. - // show_radial_menu returns the key from the assoc list (here: a typepath). + // show_radial_menu returns the key from the assoc list. var/selected_tool_type = show_radial_menu( user, user, // "anchor" just for placement; using the user keeps it simple @@ -44,11 +43,11 @@ new_tool_item.AddElement(/datum/element/phantasmal_tool) if(!user.put_in_hands(new_tool_item)) qdel(new_tool_item) // destroys the item if it fails to put it in our hands, as it shouldn't ever exist out of hands. - + playsound(user, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) return TRUE // To potentially refund it, we run a small check. -/datum/action/cooldown/power/thaumaturge/on_action_success(mob/living/user, atom/target, override_charges) +/datum/action/cooldown/power/thaumaturge/phantasmal_tool/on_action_success(mob/living/user, atom/target, override_charges) var/chance_to_refund = clamp(11 * affinity + 30, 20, 100) // Caps out at 85 for T5. if(prob(chance_to_refund)) override_charges = 0 @@ -57,14 +56,14 @@ to_chat(owner, span_warning("Your [name] spell consumed a charge!")) return ..(user, target, override_charges) -/datum/action/cooldown/power/thaumaturge/proc/phantasmal_tool_menu_check(mob/user) +/datum/action/cooldown/power/thaumaturge/phantasmal_tool/proc/phantasmal_tool_menu_check(mob/user) if(!istype(user)) return FALSE if(user.incapacitated) return FALSE return TRUE -/datum/action/cooldown/power/thaumaturge/proc/get_phantasmal_tool_radial_images() +/datum/action/cooldown/power/thaumaturge/phantasmal_tool/proc/get_phantasmal_tool_radial_images() var/static/list/tool_type_to_image if(tool_type_to_image) return tool_type_to_image diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 54a2d667c09ff8..9ee2f3ad033470 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -18,9 +18,9 @@ /// while `DEAD` allows the ability to be used on any stat values. var/req_stat = CONSCIOUS /// If your power has an active state of any type, use this. - var/active = FALSE - /// Does this ability stop working if you are silenced? - var/disabled_by_silence = TRUE + var/active + /// Is this a resonant ability (read: magical)? Detemrines 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? @@ -33,9 +33,13 @@ var/need_hands_free = TRUE /// Maximum targeting range (in tiles) for click_to_activate powers. Set to 0 or null for no range limit. - var/target_range = 7 + var/target_range /// If set, clicked target MUST be of this type (or subtype). - var/target_type = null + var/target_type + + /// Imitates the effects of action/cooldown/spell/pointed/projectile if set to a projectile by shooting the listed projectile towards the target space. Useful for actions that just shoot stuff without too much nuance. + /// If used with click_to_activate, it will shoot towards the targeted space. Otherwise, it shoots directly forwards. + var/obj/projectile/projectile_type // When you press the button // Attempts to actively use the action @@ -43,6 +47,12 @@ SHOULD_CALL_PARENT(TRUE) if(!can_use(user, target)) return FALSE + // Checking for anti-resonance/anti-magic below which really is a pain. + if(!projectile_type && resonant && ismob(target)) // If it is not a projectile spell, and if the spell is resonance based, and if the target is a mob. + var/mob/mob_target = target + if(mob_target.can_block_resonance(1)) // Runs the special can_block_resonance function which also handles the anti-magic part. + // I would like to deduct resources on spell fail, but that is going to be so utterly complex. TODO for the future chap who wants this. + return FALSE if(use_action(user, target)) on_action_success() return TRUE @@ -56,7 +66,7 @@ if(disabled_by_incapacitate && HAS_TRAIT(user, TRAIT_INCAPACITATED)) owner.balloon_alert(user, "incapacitated!") return FALSE - if(disabled_by_silence && HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED)) + if(resonant && HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED)) owner.balloon_alert(user, "silenced!") return FALSE if(need_hands_free && HAS_TRAIT(user, TRAIT_HANDS_BLOCKED)) @@ -77,7 +87,12 @@ return TRUE // Now we do THINGS! +// Make sure you return TRUE or FALSE to tell the power that it has succesfully (or unsuccesfully) been used and trigger on_action_success. /datum/action/cooldown/power/proc/use_action(mob/living/user, atom/target) + // Most spells won't use this so they are free to override this, but projectile spells use this step to actually fire ze misiles. + if(projectile_type) + return fire_projectile(user, target) + return TRUE // Anything that should happen as a result of use_action returning TRUE. @@ -107,9 +122,9 @@ Handles all the logic involved in using a targeted, click-based action. StartCooldown() return TRUE -/// Intercepts client owner clicks to activate the ability. -/// Called by the base click intercept system on left click. -/// Whilst /datum/action/cooldown does have click support, it doesn't support range-detecting and target filtering, so we are overriding that with our own. +// Intercepts client owner clicks to activate the ability. +// Called by the base click intercept system on left click. +// Whilst /datum/action/cooldown does have click support, it doesn't support range-detecting and target filtering, so we are overriding that with our own. /datum/action/cooldown/power/InterceptClickOn(mob/living/clicker, params, atom/target) if(!IsAvailable(feedback = TRUE)) return FALSE @@ -145,3 +160,76 @@ Handles all the logic involved in using a targeted, click-based action. clicker.next_click = world.time + click_cd_override return TRUE + +/* +Projectile action code down below +*/ + +// Fires the configured or given projectile at the clicked target. +// This assumes you are shooting just one projectile. Override if you need multi-shot, spread, special spawn logic, etc. +/datum/action/cooldown/power/proc/fire_projectile(mob/living/user, atom/target, obj/projectile/projectile_override) + SHOULD_CALL_PARENT(TRUE) + + // We allow for projectile_override in the event that you want to call fire_projectile outside of its standard use. + var/projectile_path = projectile_override ? projectile_override : projectile_type + if(!projectile_path || !user || !target) + return FALSE + + var/turf/user_turf = get_turf(user) + if(!user_turf) + return FALSE + + // If no clicked target was provided (non-click cast), shoot forward. + if(!target) + var/turf/aim_turf = user_turf + var/aim_range = target_range ? target_range : 7 + + for(var/step_count in 1 to aim_range) + var/turf/next_turf = get_step(aim_turf, user.dir) + if(!next_turf) + break + aim_turf = next_turf + + target = aim_turf + + // Still validate after we possibly auto-filled target + if(!target) + return FALSE + + var/obj/projectile/projectile_instance = new projectile_path(user_turf) + ready_projectile(projectile_instance, target, user) + + projectile_instance.fire() + return TRUE + +// Sets up a projectile for firing. +// Mirrors cooldown/spell/pointed/projectile +/datum/action/cooldown/power/proc/ready_projectile(obj/projectile/projectile_instance, atom/target, mob/living/user) + SHOULD_CALL_PARENT(TRUE) + + if(!projectile_instance) + return + + projectile_instance.firer = user + projectile_instance.fired_from = src + projectile_instance.aim_projectile(target, user) + + // Saves an instance of the creating power for reference. Usually you want this to check for affinity scaling on Thaumaturge. + // This only works on resonant projectiles. If you have a non-resonant projectile that needs this for some reason, override the proc. + if(istype(projectile_instance, /obj/projectile/resonant)) + var/obj/projectile/resonant/resonant_proj = projectile_instance + resonant_proj.creating_power = src + + // If you want “on hit” logic for the power, hook it here. + RegisterSignal(projectile_instance, COMSIG_PROJECTILE_SELF_ON_HIT, PROC_REF(on_power_projectile_hit)) + +// Signal handler for projectile hits; relays into an overridable proc. +/datum/action/cooldown/power/proc/on_power_projectile_hit(datum/source, mob/firer, atom/target, angle, hit_limb) + SIGNAL_HANDLER + + on_projectile_hit(source, firer, target, angle, hit_limb) + +// Override in specific powers if you want “on hit” effects that connect back to the spell, e.g some-sort of ongoing effect. +// Anything that should otherwise happen normally on projectile hit should preferably be handled in /obj/projectile/.../on_hit +/datum/action/cooldown/power/proc/on_projectile_hit(datum/source, mob/firer, atom/target, angle, hit_limb) + return diff --git a/modular_doppler/modular_powers/code/powers_magic_immunity.dm b/modular_doppler/modular_powers/code/powers_magic_immunity.dm new file mode 100644 index 00000000000000..1948ce4b3f1b5c --- /dev/null +++ b/modular_doppler/modular_powers/code/powers_magic_immunity.dm @@ -0,0 +1,43 @@ +/* + I originally considered overriding the original /mob/proc/can_block_magic but really keeping it modular is the name of the game. + +*/ + +/mob/proc/can_block_resonance(charge_cost = 1) + var/list/antimagic_sources = list() + var/is_resonance_blocked = FALSE + + if(SEND_SIGNAL(src, COMSIG_MOB_RECEIVE_MAGIC, MAGIC_RESISTANCE, charge_cost, antimagic_sources) & COMPONENT_MAGIC_BLOCKED) // Normal magic immunity applies too. + is_resonance_blocked = TRUE + if(HAS_TRAIT(src, TRAIT_ANTIMAGIC)) // Normal magic immunity. + is_resonance_blocked = TRUE + if(HAS_TRAIT(src, TRAIT_ANTIRESONANCE)) // Resonance based magic immunity. + is_resonance_blocked = TRUE + + if(is_resonance_blocked && charge_cost > 0 && !HAS_TRAIT(src, TRAIT_RECENTLY_BLOCKED_MAGIC)) + on_block_resonance_effects(antimagic_sources) + return is_resonance_blocked + +// Called when we succesfully block a resonant effect.. +/mob/proc/on_block_resonance_effects() + return + +/mob/living/on_block_resonance_effects(list/antimagic_sources) + ADD_TRAIT(src, TRAIT_RECENTLY_BLOCKED_MAGIC, MAGIC_TRAIT) + addtimer(TRAIT_CALLBACK_REMOVE(src, TRAIT_RECENTLY_BLOCKED_MAGIC, MAGIC_TRAIT), 6 SECONDS) + + var/mutable_appearance/antimagic_effect + var/antimagic_color + var/atom/antimagic_source = length(antimagic_sources) ? pick(antimagic_sources) : src + + visible_message( + span_warning("[src] pulses blue as [ismob(antimagic_source) ? p_they() : antimagic_source] absorbs resonant energy!"), + span_userdanger("An intense resonant aura pulses around [ismob(antimagic_source) ? "you" : antimagic_source] as it dissipates into the air!"), + ) + antimagic_effect = mutable_appearance('icons/effects/effects.dmi', "shield-old", MOB_SHIELD_LAYER) + antimagic_color = LIGHT_COLOR_DARK_BLUE + playsound(src, 'sound/effects/magic/magic_block.ogg', 50, TRUE) + + mob_light(range = 2, power = 2, color = antimagic_color, duration = 5 SECONDS) + add_overlay(antimagic_effect) + addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, cut_overlay), antimagic_effect), 5 SECONDS) diff --git a/tgstation.dme b/tgstation.dme index dc48eb5af77e29..6f0c4c5ce6c2f8 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7428,7 +7428,9 @@ #include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm" #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" +#include "modular_doppler\modular_powers\code\powers_magic_immunity.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_root.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_spell.dm" @@ -7441,6 +7443,8 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_spell_focus.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_affinity.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\phantasmal_tool.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\blend_for_me.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\gale_blast.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_action.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" @@ -7474,6 +7478,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\magic_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\brain_trauma.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\resonant_immune.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" #include "modular_doppler\modular_quirks\bouncy\bouncy.dm" From 03c0f86cde824056fc4f2de20482f8711610fae6 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 6 Feb 2026 15:27:00 +0100 Subject: [PATCH 041/212] Added magic_barrage (this took too much work). Minor tweaks to other things. --- .../resonant/aberrant/resonant_immune.dm | 2 +- .../code/powers/resonant/psyker/levitate.dm | 3 +- .../powers/resonant/psyker/telekinesis.dm | 6 +- .../sorcerous/thaumaturge/blend_for_me.dm | 6 +- .../sorcerous/thaumaturge/gale_blast.dm | 6 +- .../sorcerous/thaumaturge/magic_barrage.dm | 259 ++++++++++++++++++ .../sorcerous/thaumaturge/phantasmal_tool.dm | 2 +- tgstation.dme | 1 + 8 files changed, 272 insertions(+), 13 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm index 784d6922ebe79b..6fc29b3b429193 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm @@ -3,7 +3,7 @@ */ /datum/power/aberrant/anomaly/counter_resonance name = "Counter-Resonance Anomaly" - desc = "You have a counteractive effect on resonance-based phenomena. You are immune to resonance-based magic (but not normal magic!), and you cannot use any resonance-based powers." + 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." value = 9 archetype = POWER_ARCHETYPE_RESONANT 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 c3745892e6402b..760d2d444b39d2 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -25,7 +25,6 @@ /datum/action/cooldown/power/psyker/levitate/use_action() . = ..() - psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) if(!active) owner.AddElement(/datum/element/forced_gravity, 0) owner.AddElement(/datum/element/simple_flying) @@ -53,7 +52,7 @@ caster_effect = null playsound(owner, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) - return + return TRUE /datum/action/cooldown/power/psyker/levitate/process(seconds_per_tick) if(!owner) 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 f880f44c4512d3..c779f9333099ce 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -1,6 +1,6 @@ /* This leviathan of spaghetti is based off of the MODsuit modules. -It is a lazy port to the current acitons powers system from the spells system and has a lot wonkiness as a consequence. -BIG TODO: FIX THAT +It is a lazy port to the current acitons powers system from the spells system and has a lot wonkiness as a consequence, including not using use_action. +TODO: FIX THAT */ /datum/power/psyker_power/telekinesis @@ -42,7 +42,7 @@ BIG TODO: FIX THAT /datum/action/cooldown/power/psyker/telekinesis/Trigger(mob/clicker, trigger_flags, atom/target) . = ..() - // We run this here cause telekinesis doesn't use use_action for some awful reason and I cba to fix it. + // We run this here cause telekinesis doesn't use use_action because we need click intercepts. ValidateOrgan() if(grabbed_atom) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm index 4253d28feb9262..19ab5fb9c069af 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm @@ -1,12 +1,11 @@ // Will it blend? // Emulates the effects of a grinder on the target in your hand. Can be used offensively too through aggressive grabs. -// TODO: Test on undersized characters. /datum/power/thaumaturge/blend_for_me name = "Blend For Me" desc = "Grinds the item in your hand as if it were inserted in a grinder, then conjures a glass to hold it (if you're grinding). Right-hand for grinding, left-hand for juicing. Can be used on people using an aggressive grab to inflict brute damage and bleeding. \ Affinity gives a chance to not consume charges." - value = 3 + value = 2 archetype = POWER_ARCHETYPE_SORCEROUS path = POWER_PATH_THAUMATURGE @@ -20,8 +19,9 @@ button_icon_state = "juicer" cooldown_time = 50 // we don't want people spamming the blender noise. that's it. that's the whole reason why we force a 5 second cooldown. - max_charges = 6 + max_charges = 8 required_affinity = 1 + prep_cost = 2 // The grab damage. // TODO: define 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 ad8d0462811ae5..7832741d25ee79 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 @@ -17,18 +17,18 @@ required_powers = list(/datum/power/thaumaturge_root) /datum/action/cooldown/power/thaumaturge/gale_blast - name = "Gale blast" + name = "Gale Blast" desc = "Shoots forth a blast of wind. The blast keeps traveling until it hits a solid structure, extinguishing any fires and dragging along any items with it. If it hits a creature, it knocks them back 3 spaces and extinguishes them." button_icon = 'icons/effects/effects.dmi' button_icon_state = "smoke" - max_charges = 6 + max_charges = 7 required_affinity = 3 prep_cost = 3 projectile_type = /obj/projectile/resonant/gale_blast click_to_activate = TRUE -/datum/action/cooldown/power/thaumaturge/use_action(mob/living/user, atom/target) +/datum/action/cooldown/power/thaumaturge/gale_blast/use_action(mob/living/user, atom/target) . = ..() playsound(user, 'sound/effects/podwoosh.ogg', 60, TRUE, SILENCED_SOUND_EXTRARANGE) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm new file mode 100644 index 00000000000000..56327cee4a49fd --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm @@ -0,0 +1,259 @@ +/* + Deviates from standard norms by being a projectile spell with click functionalities, but using neither because it is too 'unique' in its application. + Really its an example of what being clever gets you. +*/ + +/datum/power/thaumaturge/magic_barrage + name = "Magical Barrage" + desc = "Shoots a volley of magic projectiles equal to your Affinity. You can either fire single shots with a short delay between shots, or shoot all your remaining shots in a barrage. Requires Affinity 3." + value = 4 + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THAUMATURGE + action_path = /datum/action/cooldown/power/thaumaturge/magical_barrage + required_powers = list(/datum/power/thaumaturge_root) + +/datum/action/cooldown/power/thaumaturge/magical_barrage + name = "Magical Barrage" + desc = "Shoots a volley of magic projectiles equal to your Affinity. Left click to fire single shots with a short delay between shots, or right click to shoot all your remaining shots in a barrage." + button_icon = 'icons/obj/weapons/guns/projectiles.dmi' + button_icon_state = "arcane_barrage" + + max_charges = 6 + required_affinity = 3 + prep_cost = 4 + + // The projectile we fire + var/projectile_path = /obj/projectile/resonant/magic_barrage + + // Barrage state (we use the basetype's active var to indicate mode) + var/missiles_remaining = 0 + var/list/orbiting_missiles = list() + var/time_between_initial_missiles = 0.15 SECONDS // Missiles spawned sequentially to prevent stacking. + var/missile_orbit_radius = 20 + var/missile_rotation_speed = 15 + + // Cooldown for single shots. + var/next_single_shot_time = 0 + var/single_shot_delay = 3 + + // Wind-up before you can start casting. + var/barrage_ready_time = 0 + +/datum/action/cooldown/power/thaumaturge/magical_barrage/use_action(mob/living/user, atom/target) + // Toggle the barrage firing mode. + if(active) + disable_barrage(user, span_warning("You dispel the magic missiles.")) + return FALSE + + if(user != owner) + // Safety: this action should only ever be used by its owner. + return FALSE + + active = TRUE + missiles_remaining = clamp(affinity, 3, 10) + next_single_shot_time = world.time // allow immediate first shot + + // prevent firing until all the projectiles are ready + barrage_ready_time = world.time + round((missiles_remaining - 1) * time_between_initial_missiles) + spawn_orbitals(missiles_remaining) + RegisterSignal(owner, COMSIG_MOB_CLICKON, PROC_REF(on_owner_clickon)) + to_chat(owner, span_notice("Magical missiles orbit you. Left-click: Fire one. Right-click: Fire all.")) + return TRUE + +// Dispel effect +/datum/power/thaumaturge/magic_barrage/Dispel() + if(active) + disable_barrage(user, span_warning("Your magic missiles were dispelled!")) + return TRUE + +// Turns off barrage mode and cleans up signals + orbitals. +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/disable_barrage(mob/living/user, message) + if(!active) + return + + active = FALSE + missiles_remaining = 0 + + if(owner) + UnregisterSignal(owner, COMSIG_MOB_CLICKON) + + clear_orbitals() + + if(user && message) + to_chat(user, message) + +// Click handler while barrage mode is active. +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/on_owner_clickon(mob/living/clicker, atom/target, params) + SIGNAL_HANDLER + + if(!active) + return + if(clicker != owner) + return + if(missiles_remaining <= 0) + disable_barrage(owner, null) + return + + // Don't shoot yourself dummy. + if(target == owner) + return + + // Params may already be a list (depends on the signal source). + var/list/modifiers + if(islist(params)) + modifiers = params + else + modifiers = params2list(params) + + // Right click: dump all remaining missiles. + if(LAZYACCESS(modifiers, "right") || LAZYACCESS(modifiers, "button") == "right") + if(fire_projectile_shotgun(owner, target, projectile_path, pellet_count = missiles_remaining)) + disable_barrage(owner, null) + return + + // Left click: single shot + if(fire_single_shot(owner, target)) + missiles_remaining-- + remove_one_orbital() + if(missiles_remaining <= 0) + disable_barrage(owner, null) + +// Proc for firing a single shot. +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/fire_single_shot(mob/living/user, atom/target) + if(world.time < next_single_shot_time) // anti spam-click. + return FALSE + + next_single_shot_time = world.time + single_shot_delay + + playsound(owner, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + return fire_projectile(user, target, projectile_path) + + +// Special proc for shotgunning it. +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/fire_projectile_shotgun(mob/living/user, atom/target, obj/projectile/projectile_override, pellet_count = 5, cone_degrees = 18, angle_jitter_degrees = 1) + SHOULD_CALL_PARENT(TRUE) + + if(!can_fire_now(user)) + return FALSE + + var/projectile_path = projectile_override ? projectile_override : projectile_type + if(!projectile_path || !user || !target) + return FALSE + + var/turf/user_turf = get_turf(user) + var/turf/target_turf = get_turf(target) + if(!user_turf || !target_turf) + return FALSE + + pellet_count = clamp(pellet_count, 1, 50) + cone_degrees = clamp(cone_degrees, 0, 90) + angle_jitter_degrees = clamp(angle_jitter_degrees, 0, 15) + + // Base angle from shooter to clicked turf + var/base_angle = get_angle(user_turf, target_turf) + + // Evenly distribute pellets across [-cone/2 .. +cone/2] + var/half_cone = cone_degrees / 2 + var/step = (pellet_count > 1) ? (cone_degrees / (pellet_count - 1)) : 0 + + var/fired_any = FALSE + + for(var/pellet_index in 1 to pellet_count) + var/angle_offset + + if(pellet_count <= 1 || cone_degrees <= 0) + angle_offset = 0 + else + angle_offset = -half_cone + (pellet_index - 1) * step + + // Small jitter so it doesn't look like a perfectly spaced laser fan + if(angle_jitter_degrees) + angle_offset += rand(-angle_jitter_degrees * 10, angle_jitter_degrees * 10) / 10 + + var/obj/projectile/projectile_instance = new projectile_path(user_turf) + ready_projectile(projectile_instance, target, user) + + projectile_instance.fire(base_angle + angle_offset, target) + projectile_instance.spread = 2 + fired_any = TRUE + + playsound(owner, 'sound/effects/magic/magic_missile.ogg', 75, TRUE) + return fired_any + +// checks if we're allowed to fire after cast +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/can_fire_now(mob/living/user) + if(world.time < barrage_ready_time) + user.balloon_alert(user, "Wait for the missiles!") + return FALSE + return TRUE + + +// the projectile in question +/obj/projectile/resonant/magic_barrage + name = "magic missile" + icon_state = "arcane_barrage" + damage = 10 + damage_type = BURN + armour_penetration = 25 // Great for civilian use, less-so on armored opponents. + armor_flag = LASER + pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE // unfortunately for you this is a magical LASER + +/* Code for orbitals below */ +/obj/effect/magic_missile_orbiter + name = "magic missile" + icon = 'icons/obj/weapons/guns/projectiles.dmi' + icon_state = "arcane_barrage" + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + layer = ABOVE_MOB_LAYER + anchored = TRUE + alpha = 180 + + +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/spawn_orbitals(amount) + clear_orbitals() + if(amount <= 0 || QDELETED(owner)) + return + + for(var/missile_num in 1 to amount) + var/time_until_created = (missile_num - 1) * time_between_initial_missiles + if(time_until_created <= 0) + create_orbital() + else + addtimer(CALLBACK(src, PROC_REF(create_orbital)), time_until_created) + +/// Creates one missile and makes it orbit +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/create_orbital() + if(QDELETED(src) || QDELETED(owner)) + return + + var/obj/effect/magic_missile_orbiter/orbiter = new(get_turf(owner)) + orbiter.transform = matrix() + orbiter.transform.Scale(0.5, 0.5) + orbiting_missiles += orbiter + orbiter.orbit(owner, missile_orbit_radius, rotation_speed = missile_rotation_speed) + RegisterSignal(orbiter, COMSIG_QDELETING, PROC_REF(on_orbiter_deleted)) + playsound(owner, 'sound/effects/magic/blink.ogg', 75, TRUE) + +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/clear_orbitals() + if(!length(orbiting_missiles)) + return + QDEL_LIST(orbiting_missiles) + orbiting_missiles.Cut() + +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/remove_one_orbital() + if(!length(orbiting_missiles)) + return FALSE + qdel(orbiting_missiles[1]) + return TRUE + +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/on_orbiter_deleted(obj/effect/magic_missile_orbiter/orbiter) + SIGNAL_HANDLER + + if(!(orbiter in orbiting_missiles)) + return + + if(!QDELETED(owner)) + orbiter.stop_orbit(owner.orbiters) + + orbiting_missiles -= orbiter diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm index bfa795055b4341..c74a0bc32b4c36 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm @@ -19,7 +19,7 @@ max_charges = 7 required_affinity = 1 - prep_cost = 2 + prep_cost = 3 /datum/action/cooldown/power/thaumaturge/phantasmal_tool/use_action(mob/living/user, atom/target) if(user.get_active_held_item() && user.get_inactive_held_item()) diff --git a/tgstation.dme b/tgstation.dme index 6f0c4c5ce6c2f8..f212dcf0dd053d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7445,6 +7445,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\phantasmal_tool.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\blend_for_me.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\gale_blast.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\magic_barrage.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_action.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" From 0672ed454fc90d285b34f460bf2af21ea4809d19 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 6 Feb 2026 16:21:42 +0100 Subject: [PATCH 042/212] Fixed some spell prep UI stuff. Removed dispel from magic barrage (need to make this use listeners). Allows spell focuses to be swapped like the null rod. --- modular_doppler/modular_powers/code/_power.dm | 2 +- .../thaumaturge/_thaumaturge_preperation.dm | 11 +++++--- .../thaumaturge/_thaumaturge_root.dm | 9 ++++--- .../affinity/thaumaturge_spell_focus.dm | 26 ++++++++++++++++++- .../sorcerous/thaumaturge/magic_barrage.dm | 8 +----- .../tgui/interfaces/ThaumaturgeSpellPrep.tsx | 22 +++++++++++----- 6 files changed, 56 insertions(+), 22 deletions(-) diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index da4cbb8b01b9a9..fa7cd379131888 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -43,7 +43,7 @@ var/required_allow_subtypes // The path, if applicable, to the action. - var/datum/action/cooldown/action_path + var/datum/action/cooldown/power/action_path /datum/power/New() . = ..() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm index b8d0259c1d0497..b6b14d9bae146b 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm @@ -82,12 +82,12 @@ // Find the spell in the current spell_list and read its prep_cost. /datum/component/thaumaturge_preparation/proc/get_prep_cost_for_spell_ref(spell_ref) for(var/datum/power/power_instance as anything in spell_list) - if("[power_instance.action_path]" == spell_ref) + if("[power_instance.action_path.type]" == spell_ref) var/datum/action/cooldown/power/thaumaturge/action_instance = power_instance.action_path return max(0, action_instance?.prep_cost || 0) - return 0 + // Starts the process of applying spells. Verification & all /datum/component/thaumaturge_preparation/proc/apply_preperation() if(!check_valid_preperation()) @@ -96,13 +96,13 @@ if(first_time_preperation) if(apply_spell_charges()) first_time_preperation = FALSE - to_chat(attached_mob, span_warning("Your spell preperation has been applied!")) + to_chat(attached_mob, span_notice("Your spell preperation has been applied!")) else to_chat(attached_mob, span_warning("Something went wrong when applying spell charges; this shouldn't happen! Yell at a dev!")) else // For those curious how we trigger it, its the on_sleep_set() signaler at the top. recharge_when_sleep = TRUE - to_chat(attached_mob, span_warning("Your changes have been saved! The next time you take the sleep action, the charges will be applied.")) + to_chat(attached_mob, span_notice("Your changes have been saved! The next time you take the sleep action, the charges will be applied.")) // Applies the prepared spell charges. /datum/component/thaumaturge_preparation/proc/apply_spell_charges() @@ -229,14 +229,17 @@ return list( "mana_total" = mana, + "mana_max" = max_mana, "mana_spend" = mana_spend, "mana_remaining" = mana_remaining, "spell_count" = length(spells_payload), "spells" = spells_payload, + "first_time_preperation" = first_time_preperation, ) + /datum/component/thaumaturge_preparation/ui_act(action, list/params, datum/tgui/ui) . = ..() if(.) 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 aaf125c9eb16f2..f61b135445e59d 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 @@ -27,14 +27,17 @@ return // Spell preperation is so complicated we basically handle it all in a component, including the UI part. power_holder.AddComponent(/datum/component/thaumaturge_preparation, power_holder) - power_holder.put_in_hands(new /obj/item/spell_focus/staff) // TODO: Change to normal spell focus item after testing. + power_holder.put_in_hands(new /obj/item/spell_focus) . = ..() /datum/action/cooldown/power/thaumaturge/thaumaturge_root name = "Spell Preperation" desc = "Adjust the amount of charges your spells have! Requires sleeping with a Spell Focus on your person to apply (except the first time in a round)." - button_icon = 'icons/obj/antags/cult/structures.dmi' - button_icon_state = "pylon" + button_icon = 'icons/obj/storage/book.dmi' + button_icon_state = "ithaqua" + + // Lets you tweak it while you sleep. + disabled_by_incapacitate = FALSE /datum/action/cooldown/power/thaumaturge/thaumaturge_root/use_action(mob/living/user, atom/target) var/datum/component/thaumaturge_preparation/prep_component = user.GetComponent(/datum/component/thaumaturge_preparation) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm index 72adc5a53fef72..3eac5dd6f09dbb 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm @@ -1,6 +1,6 @@ /obj/item/spell_focus name = "thaumaturge's spell focus" - desc = "An orb of raw thaumaturgic resonance, adjustable to take on any form of your choosing. Needed to restore thaumaturgic powers." + desc = "An orb of raw thaumaturgic resonance, adjustable to take on any form of your choosing, one-time only. Needed to restore thaumaturgic powers." icon = 'icons/obj/weapons/guns/projectiles.dmi' icon_state = "ice_1" slot_flags = ITEM_SLOT_BELT @@ -10,6 +10,30 @@ /// Short description of what this item is capable of, for radial menu uses. var/menu_description = "An orb of energy. Fits in pockets. Can be worn on the belt. Very convenient and not visible in your hands, but doesn't do much more than that." +/obj/item/spell_focus/Initialize(mapload) + . = ..() + + // Only the base focus should offer "picks". Subtypes are the end result. + if(type != /obj/item/spell_focus) + return + + var/list/focuses = list() + for(var/obj/item/spell_focus/focus_type as anything in typesof(/obj/item/spell_focus)) + focuses[focus_type] = initial(focus_type.menu_description) + + AddComponent(/datum/component/subtype_picker, focuses, CALLBACK(src, PROC_REF(on_spell_focus_picked))) + +/obj/item/spell_focus/proc/on_spell_focus_picked(obj/item/spell_focus/new_focus, mob/living/picker) + if(!istype(new_focus)) + return + + new_focus.on_selected(src, picker) + +/obj/item/spell_focus/proc/on_selected(obj/item/spell_focus/old_focus, mob/living/picker) + // Preserve unique renames (if the old focus was renamed by a player). + if(old_focus.name != initial(old_focus.name)) + name = old_focus.name + /obj/item/spell_focus/wand name = "thaumaturge's wand" desc = "A pointy stick, attuned to work with thaumaturgic resonance. Capable of restoring thaumaturgic powers when resting." 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 56327cee4a49fd..1b8ab9485d446c 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 @@ -3,7 +3,7 @@ Really its an example of what being clever gets you. */ -/datum/power/thaumaturge/magic_barrage +/datum/power/thaumaturge/magical_barrage name = "Magical Barrage" desc = "Shoots a volley of magic projectiles equal to your Affinity. You can either fire single shots with a short delay between shots, or shoot all your remaining shots in a barrage. Requires Affinity 3." value = 4 @@ -61,12 +61,6 @@ to_chat(owner, span_notice("Magical missiles orbit you. Left-click: Fire one. Right-click: Fire all.")) return TRUE -// Dispel effect -/datum/power/thaumaturge/magic_barrage/Dispel() - if(active) - disable_barrage(user, span_warning("Your magic missiles were dispelled!")) - return TRUE - // Turns off barrage mode and cleans up signals + orbitals. /datum/action/cooldown/power/thaumaturge/magical_barrage/proc/disable_barrage(mob/living/user, message) if(!active) diff --git a/tgui/packages/tgui/interfaces/ThaumaturgeSpellPrep.tsx b/tgui/packages/tgui/interfaces/ThaumaturgeSpellPrep.tsx index f76d1c50d94d7e..e5e5781d3548d8 100644 --- a/tgui/packages/tgui/interfaces/ThaumaturgeSpellPrep.tsx +++ b/tgui/packages/tgui/interfaces/ThaumaturgeSpellPrep.tsx @@ -16,6 +16,9 @@ type SpellEntry = { type Data = { tguitheme?: string; mana_remaining: number; + mana_total: number; + mana_max: number; + first_time_preperation: boolean; spell_count: number; spells: SpellEntry[]; }; @@ -34,7 +37,8 @@ export const ThaumaturgeSpellPrep = (_props) => {
- Mana remaining: {data.mana_remaining} + Mana remaining: {data.mana_remaining} /{' '} + {data.mana_total}
@@ -101,14 +105,20 @@ export const ThaumaturgeSpellPrep = (_props) => {
- - Preparing spells for the first time applies the charges - instantly! - + {data.first_time_preperation ? ( + + Preparing spells for the first time applies the charges + instantly! + + ) : ( + + Your prepared charges will be applied the next time you sleep. + + )} From 656cbd890063aaa18d83158add7a675eb049dc4c Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 6 Feb 2026 17:11:11 +0100 Subject: [PATCH 043/212] Misc tweaks to item spawning / other bits. --- modular_doppler/modular_powers/code/_power.dm | 39 +++++++++---------- .../powers/sorcerous/_resonant_projectile.dm | 2 +- .../thaumaturge/_thaumaturge_root.dm | 15 ++----- .../theologist/smiting_strike_upgrades.dm | 11 +++++- 4 files changed, 33 insertions(+), 34 deletions(-) diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index fa7cd379131888..31f53d2ed60f66 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -45,6 +45,11 @@ // The path, if applicable, to the action. var/datum/action/cooldown/power/action_path + // Where items were spawned for the power, if any. + var/list/where_items_spawned + /// If true, the backpack automatically opens on post_add(). Usually set to TRUE when an item is equipped inside the player's backpack. + var/open_backpack = FALSE + /datum/power/New() . = ..() for(var/trait in no_process_traits) @@ -169,10 +174,22 @@ /// This proc is guaranteed to run if the mob has a client when the power is added. /// Otherwise, it runs once on the next COMSIG_MOB_LOGIN. /datum/power/proc/post_add() + SHOULD_CALL_PARENT(TRUE) // Grants appropriate actions in the UI if(action_path) var/new_action_path = grant_action(action_path) action_path = new_action_path + // If we give items to the player and open_backpack is true, have it open on round start. + if(open_backpack) + var/mob/living/carbon/human/human_holder = power_holder + // post_add() can be called via delayed callback. Check they still have a backpack equipped before trying to open it. + if(human_holder.back) + human_holder.back.atom_storage.show_contents(human_holder) + // Informs the players of any spawned items. + for(var/chat_string in where_items_spawned) + to_chat(power_holder, chat_string) + + where_items_spawned = null return // Adds activateable power buttons. @@ -222,14 +239,6 @@ return FALSE return TRUE -/// Subtype power that has some bonus logic to spawn items for the player. -/datum/power/item_power - /// Lazylist of strings describing where all the power items have been spawned. - var/list/where_items_spawned - /// If true, the backpack automatically opens on post_add(). Usually set to TRUE when an item is equipped inside the player's backpack. - var/open_backpack = FALSE - abstract_parent_type = /datum/power/item_power - /** * Handles inserting an item in any of the valid slots provided, then allows for post_add notification. * @@ -241,7 +250,7 @@ * * default_location - If the item isn't possible to equip in a valid slot, this is a description of where the item was spawned. * * notify_player - If TRUE, adds strings to where_items_spawned list to be output to the player in [/datum/power/item_power/post_add()] */ -/datum/power/item_power/proc/give_item_to_holder(obj/item/power_item, list/valid_slots, flavour_text = null, default_location = "at your feet", notify_player = FALSE) +/datum/power/proc/give_item_to_holder(obj/item/power_item, list/valid_slots, flavour_text = null, default_location = "at your feet", notify_player = FALSE) if(ispath(power_item)) power_item = new power_item(get_turf(power_holder)) @@ -254,15 +263,3 @@ if(notify_player) LAZYADD(where_items_spawned, span_boldnotice("You have \a [power_item] [where]. [flavour_text]")) - -/datum/power/item_power/post_add() - if(open_backpack) - var/mob/living/carbon/human/human_holder = power_holder - // post_add() can be called via delayed callback. Check they still have a backpack equipped before trying to open it. - if(human_holder.back) - human_holder.back.atom_storage.show_contents(human_holder) - - for(var/chat_string in where_items_spawned) - to_chat(power_holder, chat_string) - - where_items_spawned = null diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm b/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm index 0d0a77a3ddea38..580dc22866519f 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm @@ -22,7 +22,7 @@ if(isliving(target)) var/mob/living/victim = target - if(victim.can_block_magic(antimagic_flags, antimagic_charge_cost) || victim.can_block_resonance(antimagic_charge_cost)) + if(victim.can_block_resonance(antimagic_charge_cost) || victim.can_block_magic(antimagic_flags, antimagic_charge_cost)) visible_message(span_warning("[src] fizzles on contact with [victim]!")) return PROJECTILE_DELETE_WITHOUT_HITTING 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 f61b135445e59d..d8c913ffbdbec5 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 @@ -11,23 +11,16 @@ path = POWER_PATH_THAUMATURGE priority = POWER_PRIORITY_ROOT -/* Previous Item Power stuff. TODO: Make Item power a variable rather than a subtype. -/datum/power/item_power/thaumaturge_root/add_unique(client/client_source) - //var/obj/item/book/random/spellbook = new(get_turf(power_holder)) - //.name = "[power_holder.real_name]'s spellbook" - //give_item_to_holder(spellbook, list(LOCATION_BACKPACK, LOCATION_HANDS)) - -/datum/power/item_power/thaumaturge_root/add(client/client_source) - //var/datum/action/cooldown/spell/touch/prestidigitation/that_magic_touch = new - //that_magic_touch.Grant(power_holder) -*/ +/datum/power/thaumaturge_root/add_unique(client/client_source) + var/obj/item/spell_focus/spell_focus = new(get_turf(power_holder)) + spell_focus.name = "[power_holder.real_name]'s spell focus" + give_item_to_holder(spell_focus, list(LOCATION_BACKPACK, LOCATION_HANDS)) /datum/power/thaumaturge_root/post_add() if(!power_holder) // So it doesn't runtime at init return // Spell preperation is so complicated we basically handle it all in a component, including the UI part. power_holder.AddComponent(/datum/component/thaumaturge_preparation, power_holder) - power_holder.put_in_hands(new /obj/item/spell_focus) . = ..() /datum/action/cooldown/power/thaumaturge/thaumaturge_root diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm index 4e7057a2063dec..4c73927390145a 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm @@ -9,10 +9,19 @@ Most of the effects are already baked into the existing power for convenience. archetype = POWER_ARCHETYPE_SORCEROUS path = POWER_PATH_THEOLOGIST required_powers = list(/datum/power/theologist/smiting_strike) + action_path = null // So we don't give em another use of the ability. -/datum/power/theologist/smiting_strike/imbue_armaments/post_add() +/datum/power/theologist/smiting_strike/imbue_armaments/add() + . = ..() var/datum/power/theologist/smiting_strike/smiting_strike = power_holder.get_power(/datum/power/theologist/smiting_strike) var/datum/action/cooldown/power/theologist/smiting_strike/smite_action = smiting_strike.action_path // I really should find a better way to get the variables of actions. smite_action.smite_damage -= 5 smite_action.smite_knockback -= 2 smite_action.can_imbue_multiples = TRUE + +/datum/power/theologist/smiting_strike/imbue_armaments/remove() + var/datum/power/theologist/smiting_strike/smiting_strike = power_holder.get_power(/datum/power/theologist/smiting_strike) + var/datum/action/cooldown/power/theologist/smiting_strike/smite_action = smiting_strike.action_path + smite_action.smite_damage += 5 + smite_action.smite_knockback += 2 + smite_action.can_imbue_multiples = FALSE From 9ba4c2256ef4776b16966dddd28e79d16e552cd5 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Feb 2026 11:49:08 +0100 Subject: [PATCH 044/212] Added dispel functionality to everything. Various little tweaks here and there. --- code/__DEFINES/~doppler_defines/powers.dm | 10 ++++ modular_doppler/modular_powers/code/_power.dm | 5 -- .../code/powers/resonant/psyker/levitate.dm | 34 ++++++----- .../code/powers/resonant/silence_trauma.dm | 11 +--- .../powers/sorcerous/_resonant_projectile.dm | 20 +++++++ .../sorcerous/thaumaturge/gale_blast.dm | 4 +- .../sorcerous/thaumaturge/magic_barrage.dm | 41 +++++++++---- .../sorcerous/thaumaturge/phantasmal_tool.dm | 18 +++--- .../theologist/_theologist_root_revered.dm | 14 ++++- .../theologist/_theologist_root_shared.dm | 23 +++++-- .../theologist/_theologist_root_twisted.dm | 30 +++++++++- .../sorcerous/theologist/smiting_strike.dm | 8 +++ .../modular_powers/code/powers_action.dm | 6 +- ..._magic_immunity.dm => powers_antimagic.dm} | 60 +++++++++++++++++++ tgstation.dme | 2 +- 15 files changed, 225 insertions(+), 61 deletions(-) rename modular_doppler/modular_powers/code/{powers_magic_immunity.dm => powers_antimagic.dm} (50%) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 10906b18694085..cc105a7aef1b06 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -40,6 +40,16 @@ // Trait for when you are immune to resonant powers #define TRAIT_ANTIRESONANCE "TRAIT_ANTIRESONANCE" +// Listener for dispelling +#define COMSIG_ATOM_DISPEL "atom_dispel" + +// Bitflag return value(s) from handlers: +#define DISPEL_RESULT_DISPELLED (1<<0) + +// Bitflags for how dispel should behave +#define DISPEL_CASCADE_CARRIED (1<<0) + + /** * SORCEROUS * All defines related to the sorcerous archetype. diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index 31f53d2ed60f66..4b1a40c25a9949 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -165,11 +165,6 @@ /datum/power/proc/remove() return -/// Dispels the current power, basically a forced-off switch. Normally only used by resonant powers. -/// TRUE = There was a power, and whatever it was, its off now. FALSE = There was nothing to turn off -/datum/power/proc/dispel() - return FALSE - /// Any special effects or chat messages which should be applied. /// This proc is guaranteed to run if the mob has a client when the power is added. /// Otherwise, it runs once on the next COMSIG_MOB_LOGIN. 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 760d2d444b39d2..0d2d276df04604 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -7,13 +7,6 @@ required_powers = list(/datum/power/psyker_root) action_path = /datum/action/cooldown/power/psyker/levitate -/datum/power/psyker_power/levitate/dispel() - // TODO: Ask Ephe on how to do this better. - var/datum/action/cooldown/power/psyker/levitate/to_be_dispelled = action_path - if(to_be_dispelled.dispel()) - return TRUE - return FALSE - /datum/action/cooldown/power/psyker/levitate name = "Levitate" desc = "Toggles levitation, causing you to ignore the ground. Also allows for propulsion in zero-gravity. Passively drains stress while in use." @@ -36,10 +29,11 @@ icon = 'icons/effects/effects.dmi', icon_state = "psychic", layer = owner.layer - 0.1, + alpha = 100, appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART ) owner.add_overlay(caster_effect) - playsound(owner, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + playsound(owner, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) else owner.RemoveElement(/datum/element/forced_gravity, 0) owner.RemoveElement(/datum/element/simple_flying) @@ -50,7 +44,7 @@ if(caster_effect) owner.cut_overlay(caster_effect) caster_effect = null - playsound(owner, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + playsound(owner, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) return TRUE @@ -70,9 +64,19 @@ add_stress(cost * seconds_per_tick) // Dispel function; basically off-switch and possibly comedic faceplant +/datum/action/cooldown/power/psyker/levitate/Grant(mob/granted_to) + . = ..() + if(resonant) + RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + +/datum/action/cooldown/power/psyker/levitate/Remove(mob/removed_from) + . = ..() + if(resonant) + UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL) + +/datum/action/cooldown/power/psyker/levitate/proc/on_dispel(mob/owner, atom/dispeller) + SIGNAL_HANDLER -// TODO: TURN THIS INTO A LISTENER -/datum/action/cooldown/power/psyker/levitate/proc/dispel() var/mob/living/carbon/human/victim = owner if(active) owner.RemoveElement(/datum/element/forced_gravity, 0) @@ -83,7 +87,7 @@ if(caster_effect) owner.cut_overlay(caster_effect) caster_effect = null - playsound(owner, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + playsound(owner, 'sound/effects/magic/cosmic_energy.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) // Do you have anything to brace your fall? Or do you possibly manage to get lucky? var/obj/item/organ/wings/gliders = owner.get_organ_by_type(/obj/item/organ/wings) @@ -91,9 +95,9 @@ to_chat(owner, span_warning("You drop to the ground, but manage to catch yourself!")) else to_chat(owner, span_userdanger("You drop to the ground!")) - playsound(owner, 'sound/effects/desecration/desecration-02.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + playsound(owner, 'sound/effects/desecration/desecration-02.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) victim.adjustBruteLoss(5) victim.Knockdown(3 SECONDS) - return TRUE + return DISPEL_RESULT_DISPELLED - return FALSE + return NONE diff --git a/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm index eda31526ee681b..cd2284ea0af406 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm @@ -1,19 +1,12 @@ /datum/brain_trauma/magic/resonance_silenced name = "Aresonaphasia" desc = "Patient is unable to wield their own Resonant powers." - scan_desc = "resonance silenced" + scan_desc = "resonance silence" gain_text = span_notice("You feel like you're no longer in touch with your own Resonant powers.") lose_text = span_notice("You begin to feel your Resonant Powers returning.") /datum/brain_trauma/magic/resonance_silenced/on_gain() - // Dispel everything - var/list/powers_list = owner.powers - if(!length(powers_list)) - return - - for(var/datum/power/power_instance in powers_list) - power_instance.dispel() - + dispel(owner, src) ADD_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAUMA_TRAIT) . = ..() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm b/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm index 580dc22866519f..fd9df1c3c2f004 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm @@ -33,3 +33,23 @@ if(plant_tray.myseed.get_gene(/datum/plant_gene/trait/anti_magic)) visible_message(span_warning("[src] fizzles on contact with [plant_tray]!")) return PROJECTILE_DELETE_WITHOUT_HITTING + +// Signalers for dispels; in the event you're shooting into an antimagic zone or something like that. +/obj/projectile/resonant/fire(fire_angle, atom/direct_target) + SHOULD_CALL_PARENT(TRUE) + . = ..() + RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + +/obj/projectile/resonant/Destroy() + SHOULD_CALL_PARENT(TRUE) + . = ..() + UnregisterSignal(src, COMSIG_ATOM_DISPEL) + +// todo: test +/obj/projectile/resonant/proc/on_dispel(obj/projectile/projectile, atom/dispeller) + SIGNAL_HANDLER + if(dispeller) + projectile.visible_message(span_warning("[name] disappears into thin air as it makes contact with [dispeller]!")) + else + projectile.visible_message(span_warning("[name] disappears into thin air!")) + qdel(projectile) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/gale_blast.dm index 7832741d25ee79..2c54714d40ce35 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 @@ -30,7 +30,7 @@ /datum/action/cooldown/power/thaumaturge/gale_blast/use_action(mob/living/user, atom/target) . = ..() - playsound(user, 'sound/effects/podwoosh.ogg', 60, TRUE, SILENCED_SOUND_EXTRARANGE) + playsound(user, 'sound/effects/podwoosh.ogg', 60, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) // The projectile itself @@ -159,7 +159,7 @@ knockback_dist += 1 movable_target.safe_throw_at(destination_turf, knockback_dist, 2, firer) - playsound(movable_target, 'sound/effects/bamf.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + playsound(movable_target, 'sound/effects/bamf.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) // Extinguishes the target we just hit. /obj/projectile/resonant/gale_blast/proc/extinguish_hit_target(atom/hit_atom) 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 1b8ab9485d446c..41847aebbe122a 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 @@ -5,7 +5,7 @@ /datum/power/thaumaturge/magical_barrage name = "Magical Barrage" - desc = "Shoots a volley of magic projectiles equal to your Affinity. You can either fire single shots with a short delay between shots, or shoot all your remaining shots in a barrage. Requires Affinity 3." + desc = "Shoots a volley of magic projectiles equal to your Affinity + 2. You can either fire single shots with a short delay between shots, or shoot all your remaining shots in a barrage. Requires Affinity 3." value = 4 archetype = POWER_ARCHETYPE_SORCEROUS @@ -15,7 +15,7 @@ /datum/action/cooldown/power/thaumaturge/magical_barrage name = "Magical Barrage" - desc = "Shoots a volley of magic projectiles equal to your Affinity. Left click to fire single shots with a short delay between shots, or right click to shoot all your remaining shots in a barrage." + desc = "Shoots a volley of magic projectiles equal to your Affinity + 2. Left click to fire single shots with a short delay between shots, or right click to shoot all your remaining shots in a barrage." button_icon = 'icons/obj/weapons/guns/projectiles.dmi' button_icon_state = "arcane_barrage" @@ -24,16 +24,18 @@ prep_cost = 4 // The projectile we fire - var/projectile_path = /obj/projectile/resonant/magic_barrage + var/obj/projectile/projectile_path = /obj/projectile/resonant/magic_barrage - // Barrage state (we use the basetype's active var to indicate mode) + // How many missiles we have left to fire. var/missiles_remaining = 0 + + // Orbital fluff var/list/orbiting_missiles = list() - var/time_between_initial_missiles = 0.15 SECONDS // Missiles spawned sequentially to prevent stacking. + var/time_between_initial_missiles = 0.12 SECONDS // Missiles spawned sequentially to prevent stacking. var/missile_orbit_radius = 20 var/missile_rotation_speed = 15 - // Cooldown for single shots. + // Cooldown for single shots in miliseconds. var/next_single_shot_time = 0 var/single_shot_delay = 3 @@ -47,11 +49,10 @@ return FALSE if(user != owner) - // Safety: this action should only ever be used by its owner. return FALSE active = TRUE - missiles_remaining = clamp(affinity, 3, 10) + missiles_remaining = clamp(affinity + 2, 3, 10) next_single_shot_time = world.time // allow immediate first shot // prevent firing until all the projectiles are ready @@ -104,7 +105,7 @@ if(LAZYACCESS(modifiers, "right") || LAZYACCESS(modifiers, "button") == "right") if(fire_projectile_shotgun(owner, target, projectile_path, pellet_count = missiles_remaining)) disable_barrage(owner, null) - return + return // Left click: single shot if(fire_single_shot(owner, target)) @@ -187,7 +188,7 @@ /obj/projectile/resonant/magic_barrage name = "magic missile" icon_state = "arcane_barrage" - damage = 10 + damage = 7.5 damage_type = BURN armour_penetration = 25 // Great for civilian use, less-so on armored opponents. armor_flag = LASER @@ -224,6 +225,8 @@ var/obj/effect/magic_missile_orbiter/orbiter = new(get_turf(owner)) orbiter.transform = matrix() orbiter.transform.Scale(0.5, 0.5) + orbiter.icon = projectile_path.icon // if you end up editing the projectile, it should also affect the orbitals. + orbiter.icon_state = projectile_path.icon_state // ditto on above orbiting_missiles += orbiter orbiter.orbit(owner, missile_orbit_radius, rotation_speed = missile_rotation_speed) RegisterSignal(orbiter, COMSIG_QDELETING, PROC_REF(on_orbiter_deleted)) @@ -251,3 +254,21 @@ orbiter.stop_orbit(owner.orbiters) orbiting_missiles -= orbiter + +// Dispel functionality +/datum/action/cooldown/power/thaumaturge/magical_barrage/Grant(mob/granted_to) + . = ..() + if(resonant) + RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + +/datum/action/cooldown/power/thaumaturge/magical_barrage/Remove(mob/removed_from) + . = ..() + if(resonant) + UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL) + +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/on_dispel(mob/owner, atom/dispeller) + SIGNAL_HANDLER + if(!active) + return NONE + disable_barrage(owner, span_userdanger("Your magic missiles vanish as they are dispelled!")) + return DISPEL_RESULT_DISPELLED diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm index c74a0bc32b4c36..b77cf6511917ad 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm @@ -99,26 +99,28 @@ /datum/element/phantasmal_tool/Attach(datum/target) . = ..() attached_item = target + attached_item.item_flags = DROPDEL | ABSTRACT attached_item.alpha = 200 attached_item.color = "#66cbdd" attached_item.force = 0 attached_item.AddElementTrait(TRAIT_ON_HIT_EFFECT, REF(src), /datum/element/on_hit_effect) RegisterSignal(attached_item, COMSIG_ON_HIT_EFFECT, PROC_REF(break_on_hit)) - RegisterSignal(attached_item, COMSIG_ITEM_DROPPED, PROC_REF(on_item_dropped)) + RegisterSignal(attached_item, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) /datum/element/phantasmal_tool/Detach(datum/source) UnregisterSignal(source, COMSIG_ON_HIT_EFFECT) - UnregisterSignal(source, COMSIG_ITEM_DROPPED) + UnregisterSignal(source, COMSIG_ATOM_DISPEL) REMOVE_TRAIT(source, TRAIT_ON_HIT_EFFECT, REF(src)) return ..() -// Checks if the item is no longer in our hands. If so, destroy hte item. -/datum/element/phantasmal_tool/proc/on_item_dropped(datum/source, mob/user) - SIGNAL_HANDLER - qdel(attached_item) - /datum/element/phantasmal_tool/proc/break_on_hit(datum/source, atom/damage_target, hit_zone, throw_hit) SIGNAL_HANDLER if(ismob(damage_target)) - playsound(attached_item, 'sound/items/ceramic_break.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + playsound(attached_item, 'sound/items/ceramic_break.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + qdel(attached_item) + +/datum/element/phantasmal_tool/proc/on_dispel(datum/source, atom/dispeller) + SIGNAL_HANDLER + if(attached_item) + playsound(attached_item, 'sound/items/ceramic_break.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) qdel(attached_item) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index c72ee2682971d9..250cf82103432d 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -73,6 +73,7 @@ /datum/status_effect/power/burden_revered/on_apply() ADD_TRAIT(owner, TRAIT_ANALGESIA, type) + RegisterSignal(owner, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) return TRUE // Sets the link with the original action @@ -88,9 +89,21 @@ ..() /datum/status_effect/power/burden_revered/on_remove() + UnregisterSignal(owner, COMSIG_ATOM_DISPEL) REMOVE_TRAIT(owner, TRAIT_ANALGESIA, type) return +// Dispel functionality +/datum/status_effect/power/burden_revered/proc/on_dispel(mob/owner, atom/dispeller) + SIGNAL_HANDLER + to_chat(owner, span_userdanger("Your [burden_power.name] deactives prematurely!")) + if(!owner == burden_power.owner) + to_chat(burden_power.owner, span_warning("Your [burden_power.name] has been dispelled!")) + burden_power.StartCooldownSelf(150) // Just so you don't immediately reapply it. + expire() + return DISPEL_RESULT_DISPELLED + + // This is where the heal budgeting happens. /datum/status_effect/power/burden_revered/tick(seconds_between_ticks) var/healing_amount = (base_healing_amount * seconds_between_ticks) @@ -157,4 +170,3 @@ name = "A Burden Revered" desc = "You passively heal damage, and are immune to pain for it's duration." icon_state = "designated_target" // Placeholder - diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index 86e60dd5035d0c..eea3deace3410d 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -86,6 +86,9 @@ * Always-called cleanup. Use manual = TRUE when the user actively cancels the power. */ /datum/action/cooldown/power/theologist/theologist_root/shared/proc/clear_link(manual = FALSE) + // gets rid of the dispel signaler + UnregisterSignal(current_target, COMSIG_ATOM_DISPEL) + UnregisterSignal(owner, COMSIG_ATOM_DISPEL) // gets rid of the beam if(current_beam) UnregisterSignal(current_beam, COMSIG_QDELETING) @@ -112,10 +115,21 @@ */ /datum/action/cooldown/power/theologist/theologist_root/shared/proc/beam_died() SIGNAL_HANDLER - current_beam = null - active = FALSE // avoid re-qdel clear_link() +/** + * Called when the target or the caster is dispelled + */ +/datum/action/cooldown/power/theologist/theologist_root/shared/proc/on_dispel(mob/owner, atom/dispeller) + SIGNAL_HANDLER + if(!active) + return NONE + to_chat(owner, span_userdanger("Your burdens are no longer shared!")) + to_chat(current_target, span_userdanger("Your burdens are no longer shared!")) + clear_link + StartCooldownSelf(150) // Just so you don't immediately reapply it. + return DISPEL_RESULT_DISPELLED + /** * Starts (or re-targets) the link between the user and a clicked target. * Returning TRUE means: the power was used successfully and should start cooldown (and unset targeting mode). @@ -129,10 +143,13 @@ current_target = new_target last_check = 0 + active = TRUE // Create a beam from user -> target. This mirrors medbeam.dm's Beam() lifecycle. current_beam = user.Beam(current_target, icon_state = "light_beam", time = 10 MINUTES, maxdistance = target_range, beam_type = /obj/effect/ebeam/medical, beam_color = "#ddd166") RegisterSignal(current_beam, COMSIG_QDELETING, PROC_REF(beam_died)) + RegisterSignal(user, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) target_glow = mutable_appearance( icon = 'icons/effects/effects.dmi', @@ -141,8 +158,6 @@ appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART ) current_target.add_overlay(target_glow) - - active = TRUE active_effect = current_target.apply_status_effect(/datum/status_effect/power/burden_shared) return TRUE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index f2b04b4b9fdd6a..cd126f95c81acd 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -35,13 +35,23 @@ //The beam effect when channeling var/datum/beam/current_beam + // The current target of the effect + var/mob/living/current_target + + //Tells the do_while loop to keep_going + var/keep_going /datum/action/cooldown/power/theologist/theologist_root/twisted/use_action(mob/living/user, mob/living/target) + // We define the target just for the on_dispel listener + current_target = target // Because we have a do_while, it won't get to the usual unset_click_ability() until after the efffect resolves, so we have to run it here. unset_click_ability(owner, FALSE) - //Tells the do_while loop to keep_going - var/keep_going = TRUE - owner.visible_message(span_warning("[owner] lays a hand on [target.get_visible_name()], twisting their injurioes into other, smaller injuries!"), span_notice("You twist [target.get_visible_name()]'s injuries!")) + keep_going = TRUE + owner.visible_message(span_warning("[owner.get_visible_name()] lays a hand on [target.get_visible_name()], twisting their injurioes into other, smaller injuries!"), span_notice("You twist [target.get_visible_name()]'s injuries!")) + // Listeners for dispelling. + RegisterSignal(user, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + // Does the healing and damage do @@ -76,6 +86,10 @@ target.remove_status_effect(/datum/status_effect/spotlight_light/resonant) QDEL_NULL(current_beam) + // unregister signal + UnregisterSignal(current_target, COMSIG_ATOM_DISPEL) + UnregisterSignal(owner, COMSIG_ATOM_DISPEL) + // Handles piety gain var/piety_gained = max(0, floor(healing_done * THEOLOGIAN_PIETY_HEALING_COEFFICIENT)) // resets for next time @@ -169,3 +183,13 @@ no_more_damaging = FALSE return TRUE + +// Dispel effect +/datum/action/cooldown/power/theologist/theologist_root/twisted/proc/on_dispel(mob/owner, atom/dispeller) + SIGNAL_HANDLER + if(!active) + return NONE + keep_going = FALSE + owner.visible_message(span_warning("The resonant link between [owner.get_visible_name()] and [current_target.get_visible_name()] is broken!!"), span_notice("Your [name] is dispelled!")) + StartCooldownSelf(300) + return DISPEL_RESULT_DISPELLED diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm index 37c626dcf2f0af..38ea3ec673cffa 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm @@ -111,6 +111,8 @@ bless_overlay = image(icon = 'icons/effects/effects.dmi', icon_state = "blessed", layer = target.layer - 0.1) RegisterSignal(target, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(apply_bless_overlay)) target.update_appearance() + // dispel listener + RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) // Checks if the item is no longer in our hands. If so, remove this element. @@ -128,6 +130,7 @@ // Prevents signalers from loitering. /datum/element/theologist_smite/Detach(atom/source) + UnregisterSignal(source, COMSIG_ATOM_DISPEL) UnregisterSignal(source, list(COMSIG_ITEM_AFTERATTACK, COMSIG_HOSTILE_POST_ATTACKINGTARGET, COMSIG_PROJECTILE_ON_HIT, COMSIG_ATOM_UPDATE_OVERLAYS)) if(self_terminate_on_drop) UnregisterSignal(source, COMSIG_ITEM_DROPPED) @@ -172,3 +175,8 @@ playsound(target, 'sound/effects/magic/magic_block_holy.ogg', 75, TRUE) target.adjustFireLoss(smite_damage) to_chat(target, span_userdanger("You are knocked back by a burning, resonant energy!")) + +// The on dispel effect +/datum/element/theologist_smite/proc/on_dispel(atom/source, atom/dispeller) + SIGNAL_HANDLER + Detach(source) diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 9ee2f3ad033470..3aabe29c16177a 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -19,7 +19,7 @@ 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)? Detemrines if this ability stop working if you are silenced and if we check against target magic immunites. + /// 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 @@ -48,7 +48,7 @@ if(!can_use(user, target)) return FALSE // Checking for anti-resonance/anti-magic below which really is a pain. - if(!projectile_type && resonant && ismob(target)) // If it is not a projectile spell, and if the spell is resonance based, and if the target is a mob. + if(!projectile_type && resonant && ismob(target) && target != user) // If it is not a projectile spell, and if the spell is resonance based, and if the target is a mob, and if the target is not us. var/mob/mob_target = target if(mob_target.can_block_resonance(1)) // Runs the special can_block_resonance function which also handles the anti-magic part. // I would like to deduct resources on spell fail, but that is going to be so utterly complex. TODO for the future chap who wants this. @@ -220,7 +220,7 @@ Projectile action code down below var/obj/projectile/resonant/resonant_proj = projectile_instance resonant_proj.creating_power = src - // If you want “on hit” logic for the power, hook it here. + // If you want “on hit” logic for your power, hook it here. RegisterSignal(projectile_instance, COMSIG_PROJECTILE_SELF_ON_HIT, PROC_REF(on_power_projectile_hit)) // Signal handler for projectile hits; relays into an overridable proc. diff --git a/modular_doppler/modular_powers/code/powers_magic_immunity.dm b/modular_doppler/modular_powers/code/powers_antimagic.dm similarity index 50% rename from modular_doppler/modular_powers/code/powers_magic_immunity.dm rename to modular_doppler/modular_powers/code/powers_antimagic.dm index 1948ce4b3f1b5c..44ce0dc72b7ad2 100644 --- a/modular_doppler/modular_powers/code/powers_magic_immunity.dm +++ b/modular_doppler/modular_powers/code/powers_antimagic.dm @@ -41,3 +41,63 @@ mob_light(range = 2, power = 2, color = antimagic_color, duration = 5 SECONDS) add_overlay(antimagic_effect) addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, cut_overlay), antimagic_effect), 5 SECONDS) + +/* +Dispel proc handler +*/ + +/proc/dispel(atom/target, atom/dispeller, dispel_flags = 0) + if(!target) + return FALSE + + var/signal_result = SEND_SIGNAL(target, COMSIG_ATOM_DISPEL, dispeller) + var/was_dispersed = (signal_result & DISPEL_RESULT_DISPELLED) + + // Only cascade if explicitly requested AND target is a mob + if((dispel_flags & DISPEL_CASCADE_CARRIED) && ismob(target)) + var/mob/living/target_mob = target + + for(var/obj/item/held_item in target_mob.held_items) + if(dispel(held_item, dispeller)) + was_dispersed = TRUE + + for(var/obj/item/worn_item in target_mob.get_equipped_items()) + if(dispel(worn_item, dispeller)) + was_dispersed = TRUE + + // SFX that a dispel occurred. + if(was_dispersed) + playsound(target, 'sound/effects/magic/smoke.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + return was_dispersed + + +/* + Very simple wiz spell to test dispel functionality, plus for admeme purposes. +*/ + +/datum/action/cooldown/spell/pointed/resonant_dispel + name = "Dispel Resonance" + desc = "Ends the weaker, resonance-based magics on the target and anything contained on or within. Doesn't dispel any ADVANCED magic!" + button_icon_state = "emp" + + sound = 'sound/effects/magic/disable_tech.ogg' + school = SCHOOL_EVOCATION + cooldown_time = 10 SECONDS + cooldown_reduction_per_rank = 2 SECONDS + + invocation = "WE AK." // I am glad I did not add invocations to Thaumaturge because my creativity with these would ruin server prop. + invocation_type = INVOCATION_SHOUT + spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC + + active_msg = "You prepare to dispel a target..." + +/datum/action/cooldown/spell/pointed/resonant_dispel/cast(atom/cast_on) + . = ..() + if(ismob(cast_on)) + var/mob/living/living_target = cast_on + if(living_target.can_block_magic(antimagic_flags)) + to_chat(owner, span_warning("Your dispel failed to work!")) + return FALSE + + dispel(cast_on, owner, DISPEL_CASCADE_CARRIED) + return TRUE diff --git a/tgstation.dme b/tgstation.dme index f212dcf0dd053d..5baab398e28f28 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7428,7 +7428,7 @@ #include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm" #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" -#include "modular_doppler\modular_powers\code\powers_magic_immunity.dm" +#include "modular_doppler\modular_powers\code\powers_antimagic.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" From c5c2cb010c8f3a759ed5d1f23adb25b908c7b88b Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Feb 2026 14:52:09 +0100 Subject: [PATCH 045/212] Now filters for 2 specific paths instead of 1 specific archetype. Added brazen bindings and anti-resonant cuffs. Latter is in the secvend. Blend for me now can blend people. --- .../code/powers/security/resonant_cuffs.dm | 31 ++++++++++++ .../sorcerous/thaumaturge/blend_for_me.dm | 44 +++++++++++++++-- .../sorcerous/thaumaturge/brazen_bindings.dm | 49 +++++++++++++++++++ .../theologist/_theologist_root_shared.dm | 2 +- .../code/powers_prefs_middleware.dm | 15 ++++-- .../modular_powers/code/powers_subsystem.dm | 13 +++-- tgstation.dme | 2 + 7 files changed, 141 insertions(+), 15 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/security/resonant_cuffs.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm diff --git a/modular_doppler/modular_powers/code/powers/security/resonant_cuffs.dm b/modular_doppler/modular_powers/code/powers/security/resonant_cuffs.dm new file mode 100644 index 00000000000000..888562147f84a2 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/security/resonant_cuffs.dm @@ -0,0 +1,31 @@ +// Antiresonant cuffs. They're like normal cuffs but slightly worse and put a dampener on resonant folk. +/obj/item/restraints/handcuffs/antiresonant + name = "resonant suppressant handcuffs" + desc = "Handcuffs laced with leaded brass on the interior, with a plentitude of runes and a bit of circuitry sticking out. Capable of suppressing resonant powers. How R&D came up with this one is a miracle in itself." + icon_state = "handcuffAlien" + color = "#ee3d3d" // til we get a proper sprite for these things. + breakouttime = 50 SECONDS + handcuff_time = 4.5 SECONDS + custom_price = PAYCHECK_COMMAND * 0.6 + +/obj/item/restraints/handcuffs/antiresonant/attempt_to_cuff(mob/living/carbon/victim, mob/living/user) + . = ..() + playsound(victim, 'sound/effects/magic/magic_block.ogg', 75, TRUE, -2) + +/obj/item/restraints/handcuffs/antiresonant/equipped(mob/living/user, slot) + . = ..() + if(slot == ITEM_SLOT_HANDCUFFED) + to_chat(user, span_warning("A shudder goes down your spine; [name] seem to suppress resonant powers!")) + dispel(user, src) + ADD_TRAIT(user, TRAIT_RESONANCE_SILENCED, src) + RegisterSignal(src, COMSIG_ITEM_PRE_UNEQUIP, PROC_REF(on_uncuff)) // Surely there is an unequip proc I am just missing? + +/obj/item/restraints/handcuffs/antiresonant/proc/on_uncuff(datum/source) + REMOVE_TRAIT(usr, TRAIT_RESONANCE_SILENCED, src) + UnregisterSignal(src, COMSIG_ITEM_PRE_UNEQUIP) + +// Adds the antiresonant cuffs to the sec vend. +/obj/machinery/vending/security + products_doppler = list( + /obj/item/restraints/handcuffs/antiresonant = 6, + ) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm index 19ab5fb9c069af..152d8873664921 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm @@ -4,7 +4,7 @@ /datum/power/thaumaturge/blend_for_me name = "Blend For Me" desc = "Grinds the item in your hand as if it were inserted in a grinder, then conjures a glass to hold it (if you're grinding). Right-hand for grinding, left-hand for juicing. Can be used on people using an aggressive grab to inflict brute damage and bleeding. \ - Affinity gives a chance to not consume charges." + Requires Affinity 1. Affinity gives a chance to not consume charges." value = 2 archetype = POWER_ARCHETYPE_SORCEROUS @@ -14,7 +14,7 @@ /datum/action/cooldown/power/thaumaturge/blend_for_me name = "Blend For Me" - desc = "The younger cousin of another notorious spell; grinds the item in your hand as if it were inserted in a grinder, then conjures a glass to hold it (if you're grinding). Right-hand for grinding, left-hand for juicing. Can be used on people using an aggressive grab to inflict brute damage and bleeding." + desc = "The younger cousin of a remarkably wicked spell; grinds the item in your hand as if it were inserted in a grinder, then conjures a glass to hold it (if you're grinding). Right-hand for grinding, left-hand for juicing. Can be used on people using an aggressive grab to inflict brute damage and bleeding." button_icon = 'icons/obj/machines/kitchen.dmi' button_icon_state = "juicer" @@ -23,11 +23,16 @@ required_affinity = 1 prep_cost = 2 - // The grab damage. - // TODO: define - var/grab_blend_brute = 15 + // The grab damage per tick. + var/grab_blend_brute = 10 + // How many cycles can we blend a person. + var/grab_blend_duration = 3 /datum/action/cooldown/power/thaumaturge/blend_for_me/use_action(mob/living/user, atom/target) + // Check first if we are pulling a person. If so we go for the grab_blend + if(owner.pulling && owner.grab_state <= GRAB_AGGRESSIVE && isliving(owner.pulling)) + return will_a_person_blend(user, owner.pulling) + // Are we grinding or juicing? var/grinding // What item is in our active hand? @@ -50,6 +55,35 @@ return will_it_blend(user, active_held_item, grinding) +// Attemps to blend A PERSON. +// Keep in mind that if you try to blend an undersized person in your hand, it will use will_it_blend instead. +/datum/action/cooldown/power/thaumaturge/blend_for_me/proc/will_a_person_blend(mob/living/user, mob/living/target) + // How many times has our do_while hurt the person? + var/blend_attacks = 0 + owner.visible_message(span_danger("[owner] begins to magically grind [target]'s body to bits!"), span_notice("You begin to grind [target] into a pulp.")) + playsound(user, 'sound/machines/blender.ogg' , 50, TRUE) + do + user.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 10) + target.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 10) + if(do_after(owner, 10, target = target)) + target.adjustBruteLoss(grab_blend_brute) + // Carbon mobs can receive wounds. + if(iscarbon(target)) + var/mob/living/carbon/thatpoorguy = target + // 30% chance to receive a severe wound + if(prob(30)) + thatpoorguy.cause_wound_of_type_and_severity(WOUND_SLASH, null, WOUND_SEVERITY_SEVERE, WOUND_SEVERITY_SEVERE) + else + thatpoorguy.cause_wound_of_type_and_severity(WOUND_SLASH, null, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_MODERATE) + // Scream for the first time cause this is HORRIFYING. + if(blend_attacks == 0) + target.emote("scream") + playsound(user, SFX_DESECRATION, 75, TRUE, SILENCED_SOUND_EXTRARANGE) + blend_attacks++ + else + break + while (blend_attacks <= grab_blend_duration) + return TRUE // Attempts to blend the item. /datum/action/cooldown/power/thaumaturge/blend_for_me/proc/will_it_blend(mob/living/user, obj/item/input_item, grinding) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm new file mode 100644 index 00000000000000..a5e96056ad581f --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm @@ -0,0 +1,49 @@ +/* + To deal with those pesky resonant users that keep using their POWERS. +*/ + +/datum/power/thaumaturge/brazen_bindings + name = "Brazen Bindings" + desc = "Summons a set of manacles made from brass, capable of dispelling and disabling Resonant powers on the bound target. The magic that made them is fragile, causing them to break once someone escapes. Requires Affinity 1. Additional affinity increases the time it takes to break out." + value = 3 + + archetype = POWER_ARCHETYPE_SORCEROUS + path = POWER_PATH_THAUMATURGE + action_path = /datum/action/cooldown/power/thaumaturge/brazen_bindings + required_powers = list(/datum/power/thaumaturge_root) + +/datum/action/cooldown/power/thaumaturge/brazen_bindings + name = "Brazen Bindings" + desc = "Summons a set of manacles made from brass, capable of dispelling and disabling Resonant powers on the bound target. The magic that made them is fragile, causing them to break once someone escapes." + button_icon = 'icons/obj/weapons/restraints.dmi' + button_icon_state = "brass_manacles" + + max_charges = 7 + required_affinity = 1 + prep_cost = 3 + +/datum/action/cooldown/power/thaumaturge/brazen_bindings/use_action(mob/living/user, atom/target) + if(user.get_active_held_item() && user.get_inactive_held_item()) + user.balloon_alert(user, "hands are not empty!") + return FALSE + + // Creates item, adds the special phantasmal tool properties, give to user. + var/obj/item/restraints/handcuffs/antiresonant/brazen/new_cuffs = new /obj/item/restraints/handcuffs/antiresonant/brazen + new_cuffs.breakouttime += (affinity - 1) * 5 + user.put_in_hands(new_cuffs) + playsound(user, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + return TRUE + +// the special ones conjured by thaumaturges. +/obj/item/restraints/handcuffs/antiresonant/brazen/ + name = "brazen manacles" + desc = "Bulky, enchanted and resonant manacles made out of brass and laden with (cheap) gemstones. They're held together using a sliver of resonant power, causing them to break into an unuseable mess once removed." + icon_state = "brass_manacles" + w_class = WEIGHT_CLASS_NORMAL + breakouttime = 30 SECONDS // default for 1affinity. For comparison, zipties are 30seconds and normal cuffs are 1min. + handcuff_time = 6 SECONDS + color = null // only til we get a proper sprite for the base cuffs, which are currrently colored red. + +/obj/item/restraints/handcuffs/antiresonant/brazen/on_uncuff(datum/source, mob/equipper, slot) + . = ..() + qdel(src) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index eea3deace3410d..68debf00d312e9 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -126,7 +126,7 @@ return NONE to_chat(owner, span_userdanger("Your burdens are no longer shared!")) to_chat(current_target, span_userdanger("Your burdens are no longer shared!")) - clear_link + clear_link() StartCooldownSelf(150) // Just so you don't immediately reapply it. return DISPEL_RESULT_DISPELLED diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index 431494c86efbdf..7b102ed19c06d4 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -145,11 +145,18 @@ if(power_name in preferences.all_powers) return FALSE // Already have this power. - // Make sure we stay in the same archetype. + // Make sure we don't exceed 2 distinct paths. if(length(preferences.all_powers)) - var/datum/power/first_power_type = SSpowers.powers[preferences.all_powers[1]] - if(power_type.archetype != first_power_type.archetype) - to_chat(user, span_boldwarning("Mismatched archetype!")) + var/list/unique_paths = list() + // Collect the distinct paths the player already has + for(var/power_key in preferences.all_powers) + var/datum/power/existing_power = SSpowers.powers[power_key] + if(!existing_power) + continue + unique_paths[existing_power.path] = TRUE + // If the new power's path isn't already present, it would add a new path + if(!(power_type.path in unique_paths) && length(unique_paths) >= 2) + to_chat(user, span_boldwarning("You can only have powers from two paths!")) return FALSE // Make sure we have the required powers. diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index c0f5e9f00068f0..ea9f09988745cb 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -141,6 +141,9 @@ PROCESSING_SUBSYSTEM_DEF(powers) var/maximum_balance = MAXIMUM_POWER_POINTS var/list/all_powers = get_powers() + // Track distinct paths we accept while filtering this batch + var/list/unique_paths = list() + // TODO: work out how to filter powers missing their requirements. // This could be higher priorities, but could also be at the same priority level. // TODO: work out how to filter for going over the balance cap without introducing major issues. @@ -165,11 +168,11 @@ PROCESSING_SUBSYSTEM_DEF(powers) if(!ispath(power_type)) continue - // Make sure we only have one overarching archetype. - if(isnull(current_archetype)) - current_archetype = power_type.archetype - else if(current_archetype != power_type.archetype) - continue // Mismatched archetype, discard. + // Make sure we only have up to two distinct paths. + if(!(power_type.path in unique_paths)) + if(length(unique_paths) >= 2) + continue // Third distinct path, discard. + unique_paths[power_type.path] = TRUE // Make sure we don't have incompatible powers var/blacklisted = FALSE diff --git a/tgstation.dme b/tgstation.dme index 5baab398e28f28..f50fce7de5bd91 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7429,6 +7429,7 @@ #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" #include "modular_doppler\modular_powers\code\powers_antimagic.dm" +#include "modular_doppler\modular_powers\code\powers\security\resonant_cuffs.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" @@ -7446,6 +7447,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\blend_for_me.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\gale_blast.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\magic_barrage.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\brazen_bindings.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_action.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" From 89cd0a7c751a3173866fc342e45dcbace6ec0a3b Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Feb 2026 16:04:57 +0100 Subject: [PATCH 046/212] Made magic barrage slightly more expensive. Made Blend for Me last longer. --- .../sorcerous/thaumaturge/blend_for_me.dm | 69 ++++++++++--------- .../sorcerous/thaumaturge/magic_barrage.dm | 8 +-- 2 files changed, 40 insertions(+), 37 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm index 152d8873664921..cda6378ca07912 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm @@ -24,13 +24,13 @@ prep_cost = 2 // The grab damage per tick. - var/grab_blend_brute = 10 + var/grab_blend_brute = 9 // How many cycles can we blend a person. - var/grab_blend_duration = 3 + var/grab_blend_duration = 4 /datum/action/cooldown/power/thaumaturge/blend_for_me/use_action(mob/living/user, atom/target) // Check first if we are pulling a person. If so we go for the grab_blend - if(owner.pulling && owner.grab_state <= GRAB_AGGRESSIVE && isliving(owner.pulling)) + if(person_blend_conditions(user, target)) return will_a_person_blend(user, owner.pulling) // Are we grinding or juicing? @@ -55,36 +55,6 @@ return will_it_blend(user, active_held_item, grinding) -// Attemps to blend A PERSON. -// Keep in mind that if you try to blend an undersized person in your hand, it will use will_it_blend instead. -/datum/action/cooldown/power/thaumaturge/blend_for_me/proc/will_a_person_blend(mob/living/user, mob/living/target) - // How many times has our do_while hurt the person? - var/blend_attacks = 0 - owner.visible_message(span_danger("[owner] begins to magically grind [target]'s body to bits!"), span_notice("You begin to grind [target] into a pulp.")) - playsound(user, 'sound/machines/blender.ogg' , 50, TRUE) - do - user.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 10) - target.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 10) - if(do_after(owner, 10, target = target)) - target.adjustBruteLoss(grab_blend_brute) - // Carbon mobs can receive wounds. - if(iscarbon(target)) - var/mob/living/carbon/thatpoorguy = target - // 30% chance to receive a severe wound - if(prob(30)) - thatpoorguy.cause_wound_of_type_and_severity(WOUND_SLASH, null, WOUND_SEVERITY_SEVERE, WOUND_SEVERITY_SEVERE) - else - thatpoorguy.cause_wound_of_type_and_severity(WOUND_SLASH, null, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_MODERATE) - // Scream for the first time cause this is HORRIFYING. - if(blend_attacks == 0) - target.emote("scream") - playsound(user, SFX_DESECRATION, 75, TRUE, SILENCED_SOUND_EXTRARANGE) - blend_attacks++ - else - break - while (blend_attacks <= grab_blend_duration) - return TRUE - // Attempts to blend the item. /datum/action/cooldown/power/thaumaturge/blend_for_me/proc/will_it_blend(mob/living/user, obj/item/input_item, grinding) // Start cooldown immediately (anti-spam) @@ -168,3 +138,36 @@ buffer_volume = new_buffer_volume reagents = new /datum/reagents(buffer_volume, src) reagents.flags = TRANSPARENT | DRAINABLE + +// Check to see if we're allowed to blend people. +/datum/action/cooldown/power/thaumaturge/blend_for_me/proc/person_blend_conditions(/mob/living/user, atom/target) + return owner.pulling && owner.grab_state <= GRAB_AGGRESSIVE && isliving(owner.pulling) + +// Attemps to blend A PERSON. +// Keep in mind that if you try to blend an undersized person in your hand, it will use will_it_blend instead. +/datum/action/cooldown/power/thaumaturge/blend_for_me/proc/will_a_person_blend(mob/living/user, mob/living/target) + // How many times has our do_while hurt the person? + var/blend_attacks = 0 + owner.visible_message(span_danger("[owner] begins to magically grind [target]'s body to bits!"), span_notice("You begin to grind [target] into a pulp.")) + playsound(user, 'sound/machines/blender.ogg' , 50, TRUE) + do + target.Shake(pixelshiftx = 1, pixelshifty = 0, duration = 10) + if(do_after(owner, 10, target = target) && person_blend_conditions(user, target)) + target.adjustBruteLoss(grab_blend_brute) + // Carbon mobs can receive wounds. + if(iscarbon(target)) + var/mob/living/carbon/thatpoorguy = target + // 50% chance to receive a severe wound + if(prob(50)) + thatpoorguy.cause_wound_of_type_and_severity(WOUND_SLASH, null, WOUND_SEVERITY_SEVERE, WOUND_SEVERITY_SEVERE) + else + thatpoorguy.cause_wound_of_type_and_severity(WOUND_SLASH, null, WOUND_SEVERITY_MODERATE, WOUND_SEVERITY_MODERATE) + // Scream for the first time cause this is HORRIFYING. + if(blend_attacks == 0) + target.emote("scream") + playsound(user, SFX_DESECRATION, 75, TRUE, SILENCED_SOUND_EXTRARANGE) + blend_attacks++ + else + break + while (blend_attacks < grab_blend_duration) + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/magic_barrage.dm index 41847aebbe122a..687982182af72c 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 @@ -6,7 +6,7 @@ /datum/power/thaumaturge/magical_barrage name = "Magical Barrage" desc = "Shoots a volley of magic projectiles equal to your Affinity + 2. You can either fire single shots with a short delay between shots, or shoot all your remaining shots in a barrage. Requires Affinity 3." - value = 4 + value = 5 archetype = POWER_ARCHETYPE_SORCEROUS path = POWER_PATH_THAUMATURGE @@ -19,9 +19,9 @@ button_icon = 'icons/obj/weapons/guns/projectiles.dmi' button_icon_state = "arcane_barrage" - max_charges = 6 + max_charges = 5 required_affinity = 3 - prep_cost = 4 + prep_cost = 5 // The projectile we fire var/obj/projectile/projectile_path = /obj/projectile/resonant/magic_barrage @@ -188,7 +188,7 @@ /obj/projectile/resonant/magic_barrage name = "magic missile" icon_state = "arcane_barrage" - damage = 7.5 + damage = 9 damage_type = BURN armour_penetration = 25 // Great for civilian use, less-so on armored opponents. armor_flag = LASER From dd5afa081683840a9bacedf11f8882834da9c0ed Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 8 Feb 2026 19:26:45 +0100 Subject: [PATCH 047/212] Minor tweak to thaumaturge defines, file structures and made blend for me better --- code/__DEFINES/~doppler_defines/powers.dm | 7 +++++-- .../sorcerous/thaumaturge/_thaumaturge_preperation.dm | 2 +- .../code/powers/sorcerous/thaumaturge/blend_for_me.dm | 2 +- .../code/{powers => }/security/resonant_cuffs.dm | 0 tgstation.dme | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) rename modular_doppler/modular_powers/code/{powers => }/security/resonant_cuffs.dm (100%) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index cc105a7aef1b06..56cc0b301c5724 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -49,7 +49,6 @@ // Bitflags for how dispel should behave #define DISPEL_CASCADE_CARRIED (1<<0) - /** * SORCEROUS * All defines related to the sorcerous archetype. @@ -64,7 +63,11 @@ */ // How much mana you practically can cap out at. -#define THAUMATURGE_MAX_MANA 50 +#define THAUMATURGE_MAX_MANA (MAXIMUM_POWER_POINTS * THAUMATURGE_MANA_MULT ) + +#define THAUMATURGE_MANA_MULT 2 + +// How much /** * SORCEROUS: ENIGMATIST diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm index b6b14d9bae146b..cd2102c2ff7404 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm @@ -67,7 +67,7 @@ if(check_if_can_prepare(power_instance.action_path)) spell_list.Add(power_instance) calculated_mana += power_instance.value - mana = clamp(calculated_mana * 2, 0, max_mana) + mana = clamp(calculated_mana * THAUMATURGE_MANA_MULT, 0, max_mana) // Checks if we can prepare the spell in our spellbook and if so adds it to the spell list. /datum/component/thaumaturge_preparation/proc/check_if_can_prepare(action_type) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm index cda6378ca07912..376dc4be61cab2 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm @@ -24,7 +24,7 @@ prep_cost = 2 // The grab damage per tick. - var/grab_blend_brute = 9 + var/grab_blend_brute = 12.5 // How many cycles can we blend a person. var/grab_blend_duration = 4 diff --git a/modular_doppler/modular_powers/code/powers/security/resonant_cuffs.dm b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm similarity index 100% rename from modular_doppler/modular_powers/code/powers/security/resonant_cuffs.dm rename to modular_doppler/modular_powers/code/security/resonant_cuffs.dm diff --git a/tgstation.dme b/tgstation.dme index f50fce7de5bd91..602c7b8aa1a309 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7429,7 +7429,7 @@ #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" #include "modular_doppler\modular_powers\code\powers_antimagic.dm" -#include "modular_doppler\modular_powers\code\powers\security\resonant_cuffs.dm" +#include "modular_doppler\modular_powers\code\security\resonant_cuffs.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" From 819028de3d649a59c3f1f49abca26981aa3b836e Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 10 Feb 2026 19:54:53 +0100 Subject: [PATCH 048/212] Augments system fully implemented. Needs more augments but someone can do that LATER. --- code/__DEFINES/~doppler_defines/powers.dm | 26 ++- .../mortal/augmented/_augmented_power.dm | 183 ++++++++++++++++++ .../mortal/augmented/simple_augments.dm | 121 ++++++++++++ .../code/powers_prefs_middleware.dm | 146 ++++++++++++++ tgstation.dme | 2 + .../interfaces/PreferencesMenu/PowersMenu.tsx | 78 ++++++-- .../tgui/interfaces/PreferencesMenu/types.ts | 10 + 7 files changed, 539 insertions(+), 27 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 56cc0b301c5724..8762e275d99fea 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -117,15 +117,6 @@ // Trait made as to prevent duplicate smites. #define TRAIT_HAS_SMITING_STRIKE "has_smiting_strike" -/**MORTAL DEFINES -* I'm literally just using this to define Breacher Knuckle right now -* These things, they take time. -* edit: im also using this to def the mad dog style because it will not allow me to make a new file for melee defines -*/ - -#define MARTIALART_BREACHERKNUCKLE "breacher knuckle" -#define MARTIALART_MAD_DOG "the mag dog style" - /** * RESONANT * All defines related to the resonant archetype. @@ -168,3 +159,20 @@ // Standard messages for Psyker Events #define PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE "As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity; as well as a feeling that something is very wrong." + +/**MORTAL DEFINES +* I'm literally just using this to define Breacher Knuckle right now +* These things, they take time. +* edit: im also using this to def the mad dog style because it will not allow me to make a new file for melee defines +*/ + +#define MARTIALART_BREACHERKNUCKLE "breacher knuckle" +#define MARTIALART_MAD_DOG "the mag dog style" + +/** + * MORTAL: Augmented + * All defines related to the augmented powers. + */ + +// Used for the prefs to shorthand tell there's nothing in the right or left arm augment slot. +#define AUGMENTED_NO_AUGMENT "None" diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm new file mode 100644 index 00000000000000..c3d4efee72382a --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm @@ -0,0 +1,183 @@ +/datum/power/augmented + name = "Augmented Power" + desc = "I never asked for this (abstract type to appear. You shouldn't be seeing this.)" + + archetype = POWER_ARCHETYPE_MORTAL + path = POWER_PATH_AUGMENTED + priority = POWER_PRIORITY_BASIC + abstract_parent_type = /datum/power/augmented + + // The augment added in the quirk. + var/augment + + // Should the augment be disabled if they're a prisoner. + var/disable_if_prisoner = TRUE + +// Responsible for adding augments +/datum/power/augmented/add_unique(client/client_source) + var/mob/living/carbon/carbon_holder = power_holder + if(!augment || !power_holder) + return + if(disable_if_prisoner && carbon_holder.mind?.assigned_role.title == JOB_PRISONER) + to_chat(carbon_holder, span_warning("Due to your job, the [name] power has been disabled.")) + return + + // All checks passed, time to actually give the item. + // Yes. We do all this. Just to get people's arms. Having two is infinitely more difficult. + var/obj/item/organ/implant = new augment() + if(implant.zone in GLOB.arm_zones) + var/augment_left = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_left) + var/augment_right = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_right) + var/left_match = augment_matches_pref(augment_left) + var/right_match = augment_matches_pref(augment_right) + if(left_match && right_match) + var/obj/item/organ/left_implant = new augment() + left_implant.zone = BODY_ZONE_L_ARM + left_implant.slot = ORGAN_SLOT_LEFT_ARM_AUG + left_implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED) + + var/obj/item/organ/right_implant = new augment() + right_implant.zone = BODY_ZONE_R_ARM + right_implant.slot = ORGAN_SLOT_RIGHT_ARM_AUG + right_implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED) + return + var/arm_zone = get_assigned_arm_zone(client_source) + if(arm_zone == BODY_ZONE_L_ARM) + implant.zone = BODY_ZONE_L_ARM + implant.slot = ORGAN_SLOT_LEFT_ARM_AUG + else if(arm_zone == BODY_ZONE_R_ARM) + implant.zone = BODY_ZONE_R_ARM + implant.slot = ORGAN_SLOT_RIGHT_ARM_AUG + implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED) + return + +// Used to get the location zones for augment_location_label +/datum/power/augmented/proc/get_augment_location_label() + if(!augment) + return null + var/label + var/obj/item/organ/augment_path = augment + var/zone = initial(augment_path.zone) + var/slot = initial(augment_path.slot) + // I'd love if like, the weird slots like ORGAN_SLOT_BRAIN_CNS didn't return with weird strings like "brain_antistun". + // For UX we basically tell the base slots. We might have issues wtih overlap for the misc. category in the future but uhh. + // Just add it manually here and kick the can further down the road. + var/slot_label = GLOB.organ_slot_labels[slot] + if(slot_label) + return slot_label + if(zone in GLOB.arm_zones) + label = "Arms" + else if(zone in GLOB.leg_zones) + label = "Legs" + else if(zone == BODY_ZONE_HEAD) + label = "Head" + else if(zone == BODY_ZONE_CHEST) + label = "Chest" + else + label = "Misc." + return label + +// Labels for organ slots used in augment UI. +// A lot of these are niche, and I've pre-populated with basically anything I think is relevant in the future (and the appendix lmao). +// If yours is missing, just add it. +GLOBAL_LIST_INIT(organ_slot_labels, list( + ORGAN_SLOT_HUD = "Eye HUD", + ORGAN_SLOT_EYES = "Eyes", + ORGAN_SLOT_EARS = "Ears", + ORGAN_SLOT_BRAIN = "Brain", + ORGAN_SLOT_BRAIN_CEREBELLUM = "Brain (Cerebellum)", + ORGAN_SLOT_BRAIN_CNS = "Brain (CNS)", + ORGAN_SLOT_HEART = "Heart", + ORGAN_SLOT_LUNGS = "Lungs", + ORGAN_SLOT_LIVER = "Liver", + ORGAN_SLOT_STOMACH = "Stomach", + ORGAN_SLOT_TONGUE = "Tongue", + ORGAN_SLOT_VOICE = "Vocal Cords", + ORGAN_SLOT_SPINE = "Spine", + ORGAN_SLOT_APPENDIX = "Appendix", + ORGAN_SLOT_BREATHING_TUBE = "Breathing Tube", + ORGAN_SLOT_HEART_AID = "Heart Aid", + ORGAN_SLOT_STOMACH_AID = "Stomach Aid", + ORGAN_SLOT_THRUSTERS = "Thrusters", +)) + +// Global list of arm augment power names for preference validation. +// ALL THIS EFFORT JUST FOR SOME ARMS. +GLOBAL_LIST_INIT(arm_augment_values, generate_arm_augment_values()) + +// This is to populate the global list above. It only adds augments in powers, so you can't cheat to give yourself an esword arm. +/proc/generate_arm_augment_values() + var/list/values = list() + for(var/datum/power/augmented/power_type as anything in subtypesof(/datum/power/augmented)) + if(initial(power_type.abstract_parent_type) == power_type) + continue + var/obj/item/organ/augment_path = initial(power_type.augment) + if(!augment_path) + continue + var/zone = initial(augment_path.zone) + if(zone in GLOB.arm_zones) + values += initial(power_type.name) + return values + +// This gets the /datum/preference/choiced for left and right augments telling us which arm is where. +/datum/power/augmented/proc/get_assigned_arm_zone(client/client_source) + if(!client_source) + return null + var/augment_left = client_source.prefs?.read_preference(/datum/preference/choiced/augment_left) + var/augment_right = client_source.prefs?.read_preference(/datum/preference/choiced/augment_right) + if(augment_left && augment_left != AUGMENTED_NO_AUGMENT && augment_matches_pref(augment_left)) + return BODY_ZONE_L_ARM + if(augment_right && augment_right != AUGMENTED_NO_AUGMENT && augment_matches_pref(augment_right)) + return BODY_ZONE_R_ARM + return null + +// Bit of validation to make sure the augment is in fact in the user's prefs. +/datum/power/augmented/proc/augment_matches_pref(value) + if(isnull(value) || value == AUGMENTED_NO_AUGMENT || !augment) + return FALSE + if(value == name) + return TRUE + if(istext(value) && value == "[augment]") + return TRUE + return FALSE + +// Global arm loadout: left/right slots store the chosen augment for each arm. +/datum/preference/choiced/augment_left + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "augment_left" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/augment_left/create_default_value() + return AUGMENTED_NO_AUGMENT + +/datum/preference/choiced/augment_left/init_possible_values() + return list(AUGMENTED_NO_AUGMENT) + GLOB.arm_augment_values + +/datum/preference/choiced/augment_left/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return TRUE + +/datum/preference/choiced/augment_left/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/preference/choiced/augment_right + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "augment_right" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/augment_right/create_default_value() + return AUGMENTED_NO_AUGMENT + +/datum/preference/choiced/augment_right/init_possible_values() + return list(AUGMENTED_NO_AUGMENT) + GLOB.arm_augment_values + +/datum/preference/choiced/augment_right/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return TRUE + +/datum/preference/choiced/augment_right/apply_to_human(mob/living/carbon/human/target, value) + return diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm new file mode 100644 index 00000000000000..abdff9c564bc82 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm @@ -0,0 +1,121 @@ +/* A lot of these augments are very simple; rather than making a new .dm file for each we just put them all here.*/ + +/* +ARMS +*/ + +/datum/power/augmented/razor_claws + name = "Razor Claws" + desc = "Grants razor-sharp claws in your arms, which can be extended and retracted at will. \ + Can also be used as wirecutters." + + value = 2 + augment = /obj/item/organ/cyberimp/arm/toolkit/razor_claws + +/datum/power/augmented/botany_toolkit + name = "Hydroponics Toolset Implant" + desc = "A rather simple arm implant containing tools used in gardening and botanical research." + + value = 3 + augment = /obj/item/organ/cyberimp/arm/toolkit/botany + +/datum/power/augmented/sanitation_toolkit + name = "Sanitation Toolset Implant" + desc = "A set of janitorial tools on the user's arm." + + value = 3 + augment = /obj/item/organ/cyberimp/arm/toolkit/janitor + +/datum/power/augmented/surgery_toolkit + name = "Surgical Toolset Implant" + desc = "A set of surgical tools hidden behind a concealed panel on the user's arm." + + value = 5 + augment = /obj/item/organ/cyberimp/arm/toolkit/surgery + +/datum/power/augmented/toolset_toolkit + name = "Integrated Toolset Implant" + desc = "A stripped-down version of the engineering cyborg toolset, designed to be installed on subject's arm. Contain advanced versions of every tool." + + value = 9 + augment = /obj/item/organ/cyberimp/arm/toolkit/toolset + +/datum/power/augmented/drill_arm + name = "Integrated Drill Implant" + desc = "Extending from a stabilization bracer built into the upper forearm, this implant allows for a steel mining drill to extend over the user's hand." + + value = 4 + augment = /obj/item/organ/cyberimp/arm/toolkit/mining_drill + +/datum/power/augmented/strong_arm + name = "Strong Arm Implant" + desc = "When implanted, this cybernetic implant will enhance the muscles of the arm to deliver more power-per-action. Install one in each arm \ + to pry open doors with your bare hands!" + + value = 8 + augment = /obj/item/organ/cyberimp/arm/strongarm + +/* +CHEST +The game sometimes calls this spine. +*/ +/datum/power/augmented/spinal_implant + name = "Herculean Gravitronic Spinal Implant" + desc = "This gravitronic spinal interface is able to improve the athletics of a user, allowing them greater physical ability. \ + Contains a slot which can be upgraded with a gravity anomaly core, improving its performance." + + value = 3 + augment = /obj/item/organ/cyberimp/chest/spine + +/datum/power/augmented/spinal_implant/atlas + name = "Atlas Gravitonic Spinal Implant" + desc = "The upgraded version of the Herculean Gravitronic Spinal Implant. Allows for easier lifting, as well as remaining grounded even in low gravity." + + value = 7 + augment = /obj/item/organ/cyberimp/chest/spine/atlas + +/* +EYE HUDS. +Keep in mind these are HUDS. Not actual eye replacements. +*/ + +/datum/power/augmented/med_hud + name = "Medical HUD Implant" + desc = "These cybernetic eye implants will display a medical HUD over everything you see." + + value = 5 + augment = /obj/item/organ/cyberimp/eyes/hud/medical + disable_if_prisoner = FALSE + +/datum/power/augmented/diagnostic_hud + name = "Diagnostic HUD Implant" + desc = "These cybernetic eye implants will display a diagnostic HUD over everything you see." + + value = 2 + augment = /obj/item/organ/cyberimp/eyes/hud/diagnostic + disable_if_prisoner = FALSE + +/* +EYES. +Not to be confused with HUD eyes above. +*/ + +/datum/power/augmented/flashproof_eyes + name = "Shielded Robotic Eyes" + desc = "These reactive micro-shields will protect you from welders and flashes without obscuring your vision." + + value = 4 + augment = /obj/item/organ/eyes/robotic/shield + disable_if_prisoner = FALSE // don't go ripping out a man's eyes. + +/* +INTERNAL (basically anything that isnt standard slots) +*/ + +/datum/power/augmented/skillchip_connector + name = "CNS Skillchip Connector Implant" + desc = "This cybernetic adds a port to the back of your head, where you can remove or add skillchips at will." + + value = 2 + augment = /obj/item/organ/cyberimp/brain/connector + disable_if_prisoner = FALSE diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index 7b102ed19c06d4..2eea55a4b6fa67 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -15,6 +15,7 @@ action_delegations = list( "give_power" = PROC_REF(give_power), "remove_power" = PROC_REF(remove_power), + "set_augment_arm" = PROC_REF(set_augment_arm), ) /datum/preference_middleware/powers/get_ui_data(mob/user) @@ -83,15 +84,19 @@ word = "Learn" color = "1" + var/augment_info = build_augment_ui_info(power_type, preferences) + 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, + "augment" = augment_info, )) switch(power_type.path) @@ -129,6 +134,41 @@ return data +/datum/preference_middleware/powers/proc/build_augment_ui_info( + datum/power/power_type, + datum/preferences/preferences +) + // Snowflake code for Augments: expose arm assignment + location. + var/augment_location + var/is_arm_augment + var/augment_assignment + var/arm_left_blocked + var/arm_right_blocked + if(ispath(power_type, /datum/power/augmented)) + var/datum/power/augmented/power_instance = new power_type + augment_location = power_instance.get_augment_location_label() + is_arm_augment = (augment_location == "Arms") + qdel(power_instance) + if(is_arm_augment) + var/augment_left = preferences.read_preference(/datum/preference/choiced/augment_left) + var/augment_right = preferences.read_preference(/datum/preference/choiced/augment_right) + arm_left_blocked = (augment_left && augment_left != AUGMENTED_NO_AUGMENT && augment_left != power_type.name) + arm_right_blocked = (augment_right && augment_right != AUGMENTED_NO_AUGMENT && augment_right != power_type.name) + if(augment_left == power_type.name && augment_right == power_type.name) + augment_assignment = "Both" + else if(augment_left == power_type.name) + augment_assignment = "Left" + else if(augment_right == power_type.name) + augment_assignment = "Right" + return list( + "location" = augment_location, + "is_arm" = is_arm_augment, + "assignment" = augment_assignment, + "left_blocked" = arm_left_blocked, + "right_blocked" = arm_right_blocked, + ) + return null + /** * Gives a power to a character using the params list provided by tgui. * Runs through multiple checks to ensure that the power can be learned. @@ -181,9 +221,53 @@ to_chat(user, span_boldwarning("[power_name] costs too much!")) return FALSE + // Augmented specific validation. + if(!validate_augment(power_type, power_name, user)) + return FALSE + preferences.all_powers += power_name return TRUE +// A lot of validation specifically for augmented, given they're very snowflakey in their restrictions. +/datum/preference_middleware/powers/proc/validate_augment(datum/power/power_type, power_name, mob/user) + if(!ispath(power_type, /datum/power/augmented)) + return TRUE + + var/datum/power/augmented/power_instance = new power_type + var/augment_location = power_instance.get_augment_location_label() + qdel(power_instance) + if(augment_location == "Arms") // Arm augment validation + auto-assign missing arm. + var/augment_left = preferences.read_preference(/datum/preference/choiced/augment_left) + var/augment_right = preferences.read_preference(/datum/preference/choiced/augment_right) + var/left_taken = (augment_left && augment_left != AUGMENTED_NO_AUGMENT && augment_left != power_name) + var/right_taken = (augment_right && augment_right != AUGMENTED_NO_AUGMENT && augment_right != power_name) + if(left_taken && right_taken) + to_chat(user, span_boldwarning("Both arms already have augments assigned.")) + return FALSE + if(!right_taken) + to_chat(user, span_notice("[power_name] will be assigned to your right arm.")) + preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], power_name) + else if(!left_taken) + to_chat(user, span_notice("[power_name] will be assigned to your left arm.")) + preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], power_name) + else // Non-arm validation; just goes off of slots and looks if there's any others. + var/obj/item/organ/new_augment_path = initial(power_instance.augment) + if(new_augment_path) + var/new_slot = initial(new_augment_path.slot) + for(var/existing_power_name in preferences.all_powers) + var/datum/power/augmented/existing_power_type = SSpowers.powers[existing_power_name] + if(!ispath(existing_power_type, /datum/power/augmented)) + continue + var/obj/item/organ/existing_augment_path = initial(existing_power_type.augment) + if(!existing_augment_path) + continue + var/existing_slot = initial(existing_augment_path.slot) + if(existing_slot && existing_slot == new_slot) + to_chat(user, span_boldwarning("[power_name] conflicts with [existing_power_name] (same organ slot).")) + return FALSE + + return TRUE + /** * Remove Power * @@ -210,6 +294,68 @@ return FALSE preferences.all_powers -= power_name + if(ispath(power_type, /datum/power/augmented)) + var/augment_left = preferences.read_preference(/datum/preference/choiced/augment_left) + var/augment_right = preferences.read_preference(/datum/preference/choiced/augment_right) + if(augment_left == power_name) + preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], AUGMENTED_NO_AUGMENT) + if(augment_right == power_name) + preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], AUGMENTED_NO_AUGMENT) + return TRUE + +/** + * Assign an arm augment to left/right/both for the global arm loadout. + */ +/datum/preference_middleware/powers/proc/set_augment_arm(list/params, mob/user) + var/power_name = params["power_name"] + var/side = params["side"] + var/datum/power/power_type = SSpowers.powers[power_name] + if(isnull(power_type)) + return FALSE + if(!(power_name in preferences.all_powers)) + to_chat(user, span_boldwarning("You must learn [power_name] before assigning it to an arm.")) + return FALSE + if(!ispath(power_type, /datum/power/augmented)) + return FALSE + + // Verify arm augment + var/datum/power/augmented/power_instance = new power_type + var/augment_location = power_instance.get_augment_location_label() + qdel(power_instance) + if(augment_location != "Arms") + to_chat(user, span_boldwarning("[power_name] is not an arm augment.")) + return FALSE + + var/augment_left = preferences.read_preference(/datum/preference/choiced/augment_left) + var/augment_right = preferences.read_preference(/datum/preference/choiced/augment_right) + var/left_blocked = (augment_left && augment_left != AUGMENTED_NO_AUGMENT && augment_left != power_name) + var/right_blocked = (augment_right && augment_right != AUGMENTED_NO_AUGMENT && augment_right != power_name) + + var/side_lower = lowertext(side) + if(side_lower == "left") + if(left_blocked) + to_chat(user, span_boldwarning("Your left arm already has an augment assigned.")) + return FALSE + preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], power_name) + if(augment_right == power_name) + preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], AUGMENTED_NO_AUGMENT) + else if(side_lower == "right") + if(right_blocked) + to_chat(user, span_boldwarning("Your right arm already has an augment assigned.")) + return FALSE + preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], power_name) + if(augment_left == power_name) + preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], AUGMENTED_NO_AUGMENT) + else if(side_lower == "both") + if(left_blocked || right_blocked) + to_chat(user, span_boldwarning("Both arms must be free to assign this augment to both.")) + return FALSE + preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], power_name) + preferences.write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], power_name) + else + to_chat(user, span_boldwarning("Invalid arm selection.")) + return FALSE + return TRUE /** diff --git a/tgstation.dme b/tgstation.dme index 602c7b8aa1a309..8fa442a6efc01c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7482,6 +7482,8 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\brain_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\resonant_immune.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" #include "modular_doppler\modular_quirks\bouncy\bouncy.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx index db43a7056965ee..1082e66e66a812 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx @@ -1,8 +1,15 @@ -import { Box, Button, Image, Section, Stack } from 'tgui-core/components'; +import { + Box, + Button, + Dropdown, + Image, + Section, + Stack, +} from 'tgui-core/components'; import { resolveAsset } from '../../assets'; import { useBackend } from '../../backend'; -import { PreferencesMenuData } from './types'; +import type { PreferencesMenuData } from './types'; export const Powers = (props) => { const { act } = useBackend(); @@ -19,22 +26,57 @@ export const Powers = (props) => { {'Cost: ' + props.power.cost}
- - + + + + + + {props.power.augment?.is_arm && props.power.has_power ? ( + + + act('set_augment_arm', { + power_name: props.power.name, + side: value, + }) + } + /> + + ) : props.power.augment?.location ? ( + + ({props.power.augment?.location}) + + ) : null} + + ); }; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts index b9b960ab0789b4..e033427d2ef7fb 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts @@ -106,11 +106,19 @@ export type Power = { name: string; icon: string; cost: number; + has_power?: boolean; state: string; word: string; color: string; powertype: (string | null)[]; rootpower: (string | null)[]; + augment?: { + location?: string | null; + is_arm?: boolean; + assignment?: string | null; + left_blocked?: boolean; + right_blocked?: boolean; + } | null; }; // DOPPLER EDIT END @@ -219,6 +227,8 @@ export type PreferencesMenuData = { augmented: Power[]; power_points: number; + augment_location?: string | null; + // DOPPLER EDIT END keybindings: Record; overflow_role: string; From 3ab3238fd7dad2abd971a3101bceda30399260f5 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 11 Feb 2026 10:01:15 +0100 Subject: [PATCH 049/212] Added Zoologist & Creature Tamer. Took out some redundant vars in the powers system for thaumaturge. --- .../powers/mortal/expert/_expert_action.dm | 3 + .../powers/mortal/expert/_expert_power.dm | 8 ++ .../code/powers/mortal/expert/zoologist.dm | 121 ++++++++++++++++++ .../mortal/expert/zoologist_upgrades.dm | 21 +++ .../sorcerous/thaumaturge/blend_for_me.dm | 2 - .../sorcerous/thaumaturge/brazen_bindings.dm | 2 - .../sorcerous/thaumaturge/gale_blast.dm | 10 +- .../sorcerous/thaumaturge/magic_barrage.dm | 7 +- .../sorcerous/thaumaturge/phantasmal_tool.dm | 2 - .../modular_powers/code/powers_action.dm | 18 +-- tgstation.dme | 8 +- 11 files changed, 173 insertions(+), 29 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/_expert_action.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/_expert_power.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/zoologist_upgrades.dm diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_action.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_action.dm new file mode 100644 index 00000000000000..b2fe36bea8430e --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_action.dm @@ -0,0 +1,3 @@ +/datum/action/cooldown/power/expert + name = "abstract expert power action - ahelp this" + resonant = FALSE diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_power.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_power.dm new file mode 100644 index 00000000000000..6cafa2611b7a60 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/_expert_power.dm @@ -0,0 +1,8 @@ +/datum/power/expert + name = "Expert Power" + desc = "I wanna be the very best, like no one ever was. To catch the abstract types is my real test, to report them is my cause!" + + archetype = POWER_ARCHETYPE_MORTAL + path = POWER_PATH_EXPERT + priority = POWER_PRIORITY_BASIC + abstract_parent_type = /datum/power/expert diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm new file mode 100644 index 00000000000000..57921a2b9beec0 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm @@ -0,0 +1,121 @@ +/* + Make friends with just about any simple creature. Doesn't save your friends though. +*/ +/datum/power/expert/zoologist + name = "Zoologist" + desc = "You are capable of befriending just about any creature, given the opportunity. You gain the 'Befriend Creature' ability; using it on a mob in melee range will befriend it and any of it's other nearby cousins. \ + This doesn't prevent them from turning hostile on other creatures. You can befriend just about any creature that can also be revived with a Lazurs Injector. There's no limit to how many creatures you can befriend." + value = 4 + + action_path = /datum/action/cooldown/power/expert/zoologist + +/datum/action/cooldown/power/expert/zoologist + name = "Befriend Creature" + desc = "Befriends a mob in melee range, as well as any of it's other nearby cousins. This doesn't prevent them from turning hostile on other creatures. \ + You can befriend just about any creature that can also be revived with a Lazurs Injector. There's no limit to how many creatures you can befriend." + button_icon = 'icons/mob/simple/pets.dmi' + button_icon_state = "cat_sit" + + target_type = /mob/living + target_range = 1 + click_to_activate = TRUE + cooldown_time = 5 + + // Do we have the taming upgrade? If so we use different behavior. + var/taming = FALSE + // How many creatures may we tame? + var/taming_cap = 0 + // Track current tamed mobs for cap enforcement. + var/list/tamed_mobs = list() + +/datum/action/cooldown/power/expert/zoologist/proc/get_tamed_count() + var/count = 0 + for(var/amount = length(tamed_mobs), amount >= 1, amount--) + var/mob/living/tamed = tamed_mobs[amount] + if(QDELETED(tamed)) + tamed_mobs.Cut(amount, amount + 1) + continue + count++ + return count + + +/datum/action/cooldown/power/expert/zoologist/use_action(mob/living/user, mob/living/target) + // eligibility like Lazarus injector + if(!target?.compare_sentience_type(SENTIENCE_ORGANIC)) + user.balloon_alert(user, "invalid creature!") + return FALSE + if (target.stat == DEAD) + user.balloon_alert(user, "they're dead, they won't make for good friends like this!") + return + + // If taming; tame the creature and give it pet commands. Otherwise, use base power and make everyone friends. + if(taming) + var/datum/component/zoologist_tamed/tame_component = target.GetComponent(/datum/component/zoologist_tamed) + if(tame_component) + tamed_mobs -= target + qdel(target.GetComponent(/datum/component/obeys_commands)) + if(tame_component.original_faction) + target.faction = tame_component.original_faction.Copy() + qdel(tame_component) + target.befriend(user) + return TRUE + + if(taming_cap && get_tamed_count() >= taming_cap) + user.balloon_alert(user, "too many tamed!") + return FALSE + + var/list/pet_commands = set_pet_commands(user, target) + if(length(pet_commands)) + if(!target.AddComponent(/datum/component/obeys_commands, pet_commands)) // this runtimes if it has no ai controller. + user.balloon_alert(user, "no higher brain function!") + return FALSE + + target.ai_controller?.set_blackboard_key(BB_PET_TARGETING_STRATEGY, /datum/targeting_strategy/basic/not_friends) // tells it that anyone you target is free game regardless of faction. + target.AddComponent(/datum/component/zoologist_tamed, target.faction?.Copy()) + tamed_mobs += target + qdel(target.GetComponent(/datum/component/tameable)) // No shared tames. + target.befriend(user) + target.faction = user.faction.Copy() + //shows hearts to all + var/image/heart = image('icons/effects/effects.dmi', loc = target, icon_state = "love_hearts", layer = ABOVE_MOB_LAYER) + flick_overlay_global(heart, GLOB.clients, 25) + return TRUE + else + // sets the range which is basically screen width + var/range_tiles = world.view + + for(var/mob/living/friendshiptarget in view(range_tiles, target)) + // same typepath (exact) or subtype + if(friendshiptarget.type == target.type || istype(friendshiptarget, target.type)) + var/image/heart = image('icons/effects/effects.dmi', loc = friendshiptarget, icon_state = "love_hearts", layer = ABOVE_MOB_LAYER) + friendshiptarget.flick_overlay(heart, list(user.client), 25, ABOVE_MOB_LAYER) + friendshiptarget.befriend(user) + return TRUE + +// Here for future proofing, but I'd love to make it so that you can use special abilities of some mobs. But that'd be a lotta work. +/datum/action/cooldown/power/expert/zoologist/proc/set_pet_commands(mob/living/user, mob/living/target) + var/list/commands = list( + /datum/pet_command/idle, + /datum/pet_command/free, + /datum/pet_command/protect_owner, + /datum/pet_command/follow, + /datum/pet_command/attack, + /datum/pet_command/good_boy, + ) + return commands + +/obj/effect/temp_visual/tame_hearts + name = "hearts" + icon = 'icons/effects/effects.dmi' + icon_state = "love_hearts" + duration = 25 + +/datum/component/zoologist_tamed + /// Original faction list before taming (copied) + var/list/original_faction + +/datum/component/zoologist_tamed/Initialize(list/original_faction) + . = ..() + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + src.original_faction = original_faction diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist_upgrades.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist_upgrades.dm new file mode 100644 index 00000000000000..735ddc4e27e42a --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist_upgrades.dm @@ -0,0 +1,21 @@ +/datum/power/expert/zoologist/creature_tamer + name = "Creature Tamer" + desc = "Creatures you befriend with Zooologist turn neutral and gain the 'pet commands' subsystem, allowing you to verbally command them to take certain actions. \ + Your cap of befriended creatures is reduced to 2, it no longer befriends other nearby creatures, and the creatures you tame will now be considered hostile to it's former peers." + + value = 6 + required_powers = list(/datum/power/expert/zoologist) + action_path = null // So we don't give em another use of the ability. + +/datum/power/expert/zoologist/creature_tamer/post_add() + . = ..() + var/datum/power/expert/zoologist/zoologist = power_holder.get_power(/datum/power/expert/zoologist) + var/datum/action/cooldown/power/expert/zoologist/zoologist_action = zoologist.action_path // I really should find a better way to get the variables of actions. + zoologist_action.taming = TRUE + zoologist_action.taming_cap = 2 + +/datum/power/expert/zoologist/creature_tamer/remove() + var/datum/power/expert/zoologist/zoologist = power_holder.get_power(/datum/power/expert/zoologist) + var/datum/action/cooldown/power/expert/zoologist/zoologist_action = zoologist.action_path // I really should find a better way to get the variables of actions. + zoologist_action.taming = FALSE + zoologist_action.taming_cap = 0 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm index 376dc4be61cab2..94c145c1789196 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm @@ -7,8 +7,6 @@ Requires Affinity 1. Affinity gives a chance to not consume charges." value = 2 - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THAUMATURGE action_path = /datum/action/cooldown/power/thaumaturge/blend_for_me required_powers = list(/datum/power/thaumaturge_root) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm index a5e96056ad581f..c9eaefde2d1b85 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm @@ -7,8 +7,6 @@ desc = "Summons a set of manacles made from brass, capable of dispelling and disabling Resonant powers on the bound target. The magic that made them is fragile, causing them to break once someone escapes. Requires Affinity 1. Additional affinity increases the time it takes to break out." value = 3 - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THAUMATURGE action_path = /datum/action/cooldown/power/thaumaturge/brazen_bindings required_powers = list(/datum/power/thaumaturge_root) 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 2c54714d40ce35..830990df2826c7 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 @@ -11,8 +11,6 @@ desc = "Shoots forth a blast of wind. The blast keeps traveling until it hits a solid structure, extinguishing any fires and dragging along any items with it. If it hits a creature, it knocks them back 3 spaces and extinguishes them. Requires Affinity 3. Extra affinity gives a chance to knockback further." value = 3 - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THAUMATURGE action_path = /datum/action/cooldown/power/thaumaturge/gale_blast required_powers = list(/datum/power/thaumaturge_root) @@ -25,12 +23,14 @@ max_charges = 7 required_affinity = 3 prep_cost = 3 - projectile_type = /obj/projectile/resonant/gale_blast click_to_activate = TRUE + anti_magic_on_click_target = FALSE /datum/action/cooldown/power/thaumaturge/gale_blast/use_action(mob/living/user, atom/target) - . = ..() - playsound(user, 'sound/effects/podwoosh.ogg', 60, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + if(fire_projectile(user, target, /obj/projectile/resonant/gale_blast)) + playsound(user, 'sound/effects/podwoosh.ogg', 60, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + return TRUE + return FALSE // The projectile itself 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 687982182af72c..07cb75c81c76e6 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 @@ -8,8 +8,6 @@ desc = "Shoots a volley of magic projectiles equal to your Affinity + 2. You can either fire single shots with a short delay between shots, or shoot all your remaining shots in a barrage. Requires Affinity 3." value = 5 - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THAUMATURGE action_path = /datum/action/cooldown/power/thaumaturge/magical_barrage required_powers = list(/datum/power/thaumaturge_root) @@ -22,6 +20,7 @@ max_charges = 5 required_affinity = 3 prep_cost = 5 + anti_magic_on_click_target = FALSE // The projectile we fire var/obj/projectile/projectile_path = /obj/projectile/resonant/magic_barrage @@ -126,13 +125,13 @@ // Special proc for shotgunning it. -/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/fire_projectile_shotgun(mob/living/user, atom/target, obj/projectile/projectile_override, pellet_count = 5, cone_degrees = 18, angle_jitter_degrees = 1) +/datum/action/cooldown/power/thaumaturge/magical_barrage/proc/fire_projectile_shotgun(mob/living/user, atom/target, obj/projectile/projectile, pellet_count = 5, cone_degrees = 18, angle_jitter_degrees = 1) SHOULD_CALL_PARENT(TRUE) if(!can_fire_now(user)) return FALSE - var/projectile_path = projectile_override ? projectile_override : projectile_type + var/projectile_path = projectile if(!projectile_path || !user || !target) return FALSE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm index b77cf6511917ad..23f99001277998 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm @@ -6,8 +6,6 @@ desc = "Summons a basic tool of your choice in your hand, that disappears after a duration, or if it is dropped/used to attack a person. Requires Affinity 1 to cast. Affinity gives a chance to not consume charges on cast." value = 3 - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THAUMATURGE action_path = /datum/action/cooldown/power/thaumaturge/phantasmal_tool required_powers = list(/datum/power/thaumaturge_root) diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 3aabe29c16177a..0b730ab2a48409 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -36,10 +36,8 @@ var/target_range /// If set, clicked target MUST be of this type (or subtype). var/target_type - - /// Imitates the effects of action/cooldown/spell/pointed/projectile if set to a projectile by shooting the listed projectile towards the target space. Useful for actions that just shoot stuff without too much nuance. - /// If used with click_to_activate, it will shoot towards the targeted space. Otherwise, it shoots directly forwards. - var/obj/projectile/projectile_type + /// Do we check for anti magic on the target when we click 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_click_target = TRUE // When you press the button // Attempts to actively use the action @@ -48,7 +46,7 @@ if(!can_use(user, target)) return FALSE // Checking for anti-resonance/anti-magic below which really is a pain. - if(!projectile_type && resonant && ismob(target) && target != user) // If it is not a projectile spell, and if the spell is resonance based, and if the target is a mob, and if the target is not us. + if(anti_magic_on_click_target && resonant && ismob(target) && target != user) // If the spell does check for antimagic on click, and if the spell is resonance based, and if the target is a mob, and if the target is not us. var/mob/mob_target = target if(mob_target.can_block_resonance(1)) // Runs the special can_block_resonance function which also handles the anti-magic part. // I would like to deduct resources on spell fail, but that is going to be so utterly complex. TODO for the future chap who wants this. @@ -89,10 +87,6 @@ // Now we do THINGS! // Make sure you return TRUE or FALSE to tell the power that it has succesfully (or unsuccesfully) been used and trigger on_action_success. /datum/action/cooldown/power/proc/use_action(mob/living/user, atom/target) - // Most spells won't use this so they are free to override this, but projectile spells use this step to actually fire ze misiles. - if(projectile_type) - return fire_projectile(user, target) - return TRUE // Anything that should happen as a result of use_action returning TRUE. @@ -167,11 +161,11 @@ Projectile action code down below // Fires the configured or given projectile at the clicked target. // This assumes you are shooting just one projectile. Override if you need multi-shot, spread, special spawn logic, etc. -/datum/action/cooldown/power/proc/fire_projectile(mob/living/user, atom/target, obj/projectile/projectile_override) +// Requires click_to_activate = TRUE to do mouse based targeting. +/datum/action/cooldown/power/proc/fire_projectile(mob/living/user, atom/target, obj/projectile/projectile) SHOULD_CALL_PARENT(TRUE) - // We allow for projectile_override in the event that you want to call fire_projectile outside of its standard use. - var/projectile_path = projectile_override ? projectile_override : projectile_type + var/projectile_path = projectile if(!projectile_path || !user || !target) return FALSE diff --git a/tgstation.dme b/tgstation.dme index 8fa442a6efc01c..2951b1c41196ab 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7429,6 +7429,12 @@ #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" #include "modular_doppler\modular_powers\code\powers_antimagic.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\expert\zoologist_upgrades.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_action.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_power.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\expert\zoologist.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" #include "modular_doppler\modular_powers\code\security\resonant_cuffs.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" @@ -7482,8 +7488,6 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\brain_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\resonant_immune.dm" -#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" -#include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" #include "modular_doppler\modular_quirks\bouncy\bouncy.dm" From 705f8ffb278e4da2c57e637c22868bd299249ef6 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 11 Feb 2026 11:18:39 +0100 Subject: [PATCH 050/212] Overhauled zoologist. Creature taming is now its standalone thing ,can only tame creatures that can already be tamed. --- .../powers/mortal/expert/creature_tamer.dm | 40 ++++++++ .../code/powers/mortal/expert/zoologist.dm | 91 ++----------------- .../mortal/expert/zoologist_upgrades.dm | 21 ----- tgstation.dme | 2 +- 4 files changed, 50 insertions(+), 104 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm delete mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/zoologist_upgrades.dm diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm new file mode 100644 index 00000000000000..5d68684bd88b84 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm @@ -0,0 +1,40 @@ +/datum/power/expert/creature_tamer + name = "Creature Tamer" + desc = "You're always met with success when taming creatures. Grants you the 'Tame Creature' ability, allowing you to automatically tame any normally tameable creatures. Now you too can have your very own space carp pet." + + value = 3 + required_powers = list(/datum/power/expert/zoologist) + action_path = /datum/action/cooldown/power/expert/creature_tamer + +/datum/action/cooldown/power/expert/creature_tamer + name = "Tame Creature" + desc = "Tame a creature that is already tameable, granting all the bonuses that you would've gained from taming it normally." + button_icon = 'icons/obj/clothing/neck.dmi' + button_icon_state = "petcollar" + + target_type = /mob/living + target_range = 1 + click_to_activate = TRUE + cooldown_time = 5 + +/datum/action/cooldown/power/expert/creature_tamer/use_action(mob/living/user, mob/living/target) + if(!target?.compare_sentience_type(SENTIENCE_ORGANIC)) + user.balloon_alert(user, "invalid creature!") + return FALSE + if (target.stat == DEAD) + user.balloon_alert(user, "they're dead, they won't make for good friends like this!") + return FALSE + + var/datum/component/tameable/tameable_component = target.GetComponent(/datum/component/tameable) + if(!tameable_component) + user.balloon_alert(user, "can't be tamed!") + return FALSE + + // We actually unfriend them to prevent an ai issue. + target.unfriend(user) + SEND_SIGNAL(target, COMSIG_SIMPLEMOB_SENTIENCEPOTION, user) // This basically tells it to instantly succeed at being tamed. + + //shows hearts to all + var/image/heart = image('icons/effects/effects.dmi', loc = target, icon_state = "love_hearts", layer = ABOVE_MOB_LAYER) + flick_overlay_global(heart, GLOB.clients, 25) + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm index 57921a2b9beec0..6986dae6fd9cfb 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm @@ -21,24 +21,6 @@ click_to_activate = TRUE cooldown_time = 5 - // Do we have the taming upgrade? If so we use different behavior. - var/taming = FALSE - // How many creatures may we tame? - var/taming_cap = 0 - // Track current tamed mobs for cap enforcement. - var/list/tamed_mobs = list() - -/datum/action/cooldown/power/expert/zoologist/proc/get_tamed_count() - var/count = 0 - for(var/amount = length(tamed_mobs), amount >= 1, amount--) - var/mob/living/tamed = tamed_mobs[amount] - if(QDELETED(tamed)) - tamed_mobs.Cut(amount, amount + 1) - continue - count++ - return count - - /datum/action/cooldown/power/expert/zoologist/use_action(mob/living/user, mob/living/target) // eligibility like Lazarus injector if(!target?.compare_sentience_type(SENTIENCE_ORGANIC)) @@ -48,74 +30,19 @@ user.balloon_alert(user, "they're dead, they won't make for good friends like this!") return - // If taming; tame the creature and give it pet commands. Otherwise, use base power and make everyone friends. - if(taming) - var/datum/component/zoologist_tamed/tame_component = target.GetComponent(/datum/component/zoologist_tamed) - if(tame_component) - tamed_mobs -= target - qdel(target.GetComponent(/datum/component/obeys_commands)) - if(tame_component.original_faction) - target.faction = tame_component.original_faction.Copy() - qdel(tame_component) - target.befriend(user) - return TRUE - - if(taming_cap && get_tamed_count() >= taming_cap) - user.balloon_alert(user, "too many tamed!") - return FALSE - - var/list/pet_commands = set_pet_commands(user, target) - if(length(pet_commands)) - if(!target.AddComponent(/datum/component/obeys_commands, pet_commands)) // this runtimes if it has no ai controller. - user.balloon_alert(user, "no higher brain function!") - return FALSE + // sets the range which is basically screen width + var/range_tiles = world.view - target.ai_controller?.set_blackboard_key(BB_PET_TARGETING_STRATEGY, /datum/targeting_strategy/basic/not_friends) // tells it that anyone you target is free game regardless of faction. - target.AddComponent(/datum/component/zoologist_tamed, target.faction?.Copy()) - tamed_mobs += target - qdel(target.GetComponent(/datum/component/tameable)) // No shared tames. - target.befriend(user) - target.faction = user.faction.Copy() - //shows hearts to all - var/image/heart = image('icons/effects/effects.dmi', loc = target, icon_state = "love_hearts", layer = ABOVE_MOB_LAYER) - flick_overlay_global(heart, GLOB.clients, 25) - return TRUE - else - // sets the range which is basically screen width - var/range_tiles = world.view - - for(var/mob/living/friendshiptarget in view(range_tiles, target)) - // same typepath (exact) or subtype - if(friendshiptarget.type == target.type || istype(friendshiptarget, target.type)) - var/image/heart = image('icons/effects/effects.dmi', loc = friendshiptarget, icon_state = "love_hearts", layer = ABOVE_MOB_LAYER) - friendshiptarget.flick_overlay(heart, list(user.client), 25, ABOVE_MOB_LAYER) - friendshiptarget.befriend(user) - return TRUE - -// Here for future proofing, but I'd love to make it so that you can use special abilities of some mobs. But that'd be a lotta work. -/datum/action/cooldown/power/expert/zoologist/proc/set_pet_commands(mob/living/user, mob/living/target) - var/list/commands = list( - /datum/pet_command/idle, - /datum/pet_command/free, - /datum/pet_command/protect_owner, - /datum/pet_command/follow, - /datum/pet_command/attack, - /datum/pet_command/good_boy, - ) - return commands + for(var/mob/living/friendshiptarget in view(range_tiles, target)) + // same typepath (exact) or subtype + if(friendshiptarget.type == target.type || istype(friendshiptarget, target.type)) + var/image/heart = image('icons/effects/effects.dmi', loc = friendshiptarget, icon_state = "love_hearts", layer = ABOVE_MOB_LAYER) + friendshiptarget.flick_overlay(heart, list(user.client), 25, ABOVE_MOB_LAYER) + friendshiptarget.befriend(user) + return TRUE /obj/effect/temp_visual/tame_hearts name = "hearts" icon = 'icons/effects/effects.dmi' icon_state = "love_hearts" duration = 25 - -/datum/component/zoologist_tamed - /// Original faction list before taming (copied) - var/list/original_faction - -/datum/component/zoologist_tamed/Initialize(list/original_faction) - . = ..() - if(!isliving(parent)) - return COMPONENT_INCOMPATIBLE - src.original_faction = original_faction diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist_upgrades.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist_upgrades.dm deleted file mode 100644 index 735ddc4e27e42a..00000000000000 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist_upgrades.dm +++ /dev/null @@ -1,21 +0,0 @@ -/datum/power/expert/zoologist/creature_tamer - name = "Creature Tamer" - desc = "Creatures you befriend with Zooologist turn neutral and gain the 'pet commands' subsystem, allowing you to verbally command them to take certain actions. \ - Your cap of befriended creatures is reduced to 2, it no longer befriends other nearby creatures, and the creatures you tame will now be considered hostile to it's former peers." - - value = 6 - required_powers = list(/datum/power/expert/zoologist) - action_path = null // So we don't give em another use of the ability. - -/datum/power/expert/zoologist/creature_tamer/post_add() - . = ..() - var/datum/power/expert/zoologist/zoologist = power_holder.get_power(/datum/power/expert/zoologist) - var/datum/action/cooldown/power/expert/zoologist/zoologist_action = zoologist.action_path // I really should find a better way to get the variables of actions. - zoologist_action.taming = TRUE - zoologist_action.taming_cap = 2 - -/datum/power/expert/zoologist/creature_tamer/remove() - var/datum/power/expert/zoologist/zoologist = power_holder.get_power(/datum/power/expert/zoologist) - var/datum/action/cooldown/power/expert/zoologist/zoologist_action = zoologist.action_path // I really should find a better way to get the variables of actions. - zoologist_action.taming = FALSE - zoologist_action.taming_cap = 0 diff --git a/tgstation.dme b/tgstation.dme index 2951b1c41196ab..bdc1c1c7aa63b9 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7429,10 +7429,10 @@ #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" #include "modular_doppler\modular_powers\code\powers_antimagic.dm" -#include "modular_doppler\modular_powers\code\powers\mortal\expert\zoologist_upgrades.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_action.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\zoologist.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\expert\creature_tamer.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" #include "modular_doppler\modular_powers\code\security\resonant_cuffs.dm" From 91656eec84d5a5e2a410540ec32c4c7921949ebd Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 11 Feb 2026 11:31:03 +0100 Subject: [PATCH 051/212] Got rid of an unnecessary if statement. Lowered price down to 2. --- .../code/powers/mortal/expert/creature_tamer.dm | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm index 5d68684bd88b84..ca8273504f96ed 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm @@ -2,7 +2,7 @@ name = "Creature Tamer" desc = "You're always met with success when taming creatures. Grants you the 'Tame Creature' ability, allowing you to automatically tame any normally tameable creatures. Now you too can have your very own space carp pet." - value = 3 + value = 2 required_powers = list(/datum/power/expert/zoologist) action_path = /datum/action/cooldown/power/expert/creature_tamer @@ -18,9 +18,6 @@ cooldown_time = 5 /datum/action/cooldown/power/expert/creature_tamer/use_action(mob/living/user, mob/living/target) - if(!target?.compare_sentience_type(SENTIENCE_ORGANIC)) - user.balloon_alert(user, "invalid creature!") - return FALSE if (target.stat == DEAD) user.balloon_alert(user, "they're dead, they won't make for good friends like this!") return FALSE From 733c794d1cc4f86ace371e60cdced02ccb4fbdb4 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 11 Feb 2026 11:58:33 +0100 Subject: [PATCH 052/212] Tweaked piety to check for ckey on targets to prevent harvesting. --- .../theologist/_theologist_root_revered.dm | 15 +++++++++------ .../theologist/_theologist_root_shared.dm | 3 ++- .../theologist/_theologist_root_twisted.dm | 9 ++++++--- .../powers/sorcerous/theologist/pious_prayer.dm | 2 ++ 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index 250cf82103432d..367ea90b53533c 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -40,11 +40,14 @@ . = ..() to_chat(owner, span_notice("You ready yourself to relieve the burden of others!
Left-click a creature next to you to target them!")) -/datum/action/cooldown/power/theologist/theologist_root/revered/proc/effect_expired(amount) - adjust_piety(amount) - if(amount >= 1 && !healing_self) - to_chat(owner, span_notice("Your previous Burden Revered has expired! You gained [amount] piety!")) - owner.playsound_local(owner, 'sound/effects/pray.ogg', 50, FALSE) +/datum/action/cooldown/power/theologist/theologist_root/revered/proc/effect_expired(mob/living/target, amount) + if(target.ckey) // Don't get piety from healing nobodies. + adjust_piety(amount) + if(amount >= 1 && !healing_self) + to_chat(owner, span_notice("Your previous Burden Revered has expired! You gained [amount] piety!")) + owner.playsound_local(owner, 'sound/effects/pray.ogg', 50, FALSE) + else + to_chat(owner, span_notice("Your previous Burden Revered has expired!")) else to_chat(owner, span_notice("Your previous Burden Revered has expired!")) @@ -162,7 +165,7 @@ var/piety_gained = max(0, floor(healing_done * THEOLOGIAN_PIETY_HEALING_COEFFICIENT)) // TODO: defines // Report back BEFORE deletion starts if(burden_power) - burden_power.effect_expired(piety_gained) + burden_power.effect_expired(owner, piety_gained) already_expired = TRUE src.Destroy() // There might be something better, but QDEL triggers the qdel loop warning. diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index 68debf00d312e9..945aab806d0fd6 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -264,7 +264,8 @@ taker.adjustOxyLoss(amount) // Piety buildup increases/deductions - if(taker == owner) + // you can't gain piety from taking burdens from a ckey-less creature (sorry pets), but you can lose piety from dumping onto a ckey-less creature. + if(taker == owner && giver.ckey) piety_buildup += amount * THEOLOGIAN_PIETY_HEALING_COEFFICIENT else if(giver == owner) piety_buildup -= amount * THEOLOGIAN_PIETY_HEALING_COEFFICIENT diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index cd126f95c81acd..543aafa3e38572 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -95,9 +95,12 @@ // resets for next time healing_done = 0 damage_done = 0 - adjust_piety(piety_gained) - if(piety_gained >= 1) - to_chat(owner, span_notice("You Burden Twisted yielded [piety_gained] piety!")) + if(target.ckey) + adjust_piety(piety_gained) + if(piety_gained >= 1) + to_chat(owner, span_notice("You Burden Twisted yielded [piety_gained] piety!")) + else + to_chat(owner, span_notice("Your Burden Twisted yielded no piety!")) else to_chat(owner, span_notice("Your Burden Twisted yielded no piety!")) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm index fce916b48c2d7c..d14f40092f0e05 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm @@ -32,9 +32,11 @@ // Are you the Chaplain? if(is_chaplain_job(owner.mind?.assigned_role)) prayer_cap = 15 + return // Do you have the religious quirk? if(HAS_TRAIT(owner, TRAIT_SPIRITUAL)) prayer_cap = 10 + return /datum/action/cooldown/power/theologist/pious_prayer/use_action(mob/living/user, atom/target) //Tells the do_while loop to keep_going From 853461915ad077e7a12b15c10e429c3705b396fb Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 11 Feb 2026 12:39:45 +0100 Subject: [PATCH 053/212] Base root for anomaly, a bunch of base aberrant stuff. --- .../resonant/aberrant/_aberrant_action.dm | 2 ++ .../resonant/aberrant/_aberrant_power.dm | 8 +++++ .../resonant/aberrant/_aberrant_root.dm | 9 ++++++ .../aberrant/_aberrant_root_anomalous.dm | 29 +++++++++++++++++++ .../resonant/aberrant/resonant_immune.dm | 7 +++-- tgstation.dme | 4 +++ 6 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_action.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_power.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_action.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_action.dm new file mode 100644 index 00000000000000..083a2e9a993ec9 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_action.dm @@ -0,0 +1,2 @@ +/datum/action/cooldown/power/aberant + name = "abstract aberrant power action - ahelp this" diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_power.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_power.dm new file mode 100644 index 00000000000000..af7a0f008faa14 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_power.dm @@ -0,0 +1,8 @@ +/datum/power/aberrant + name = "Aberrant Power" + desc = "Oh my god how horrifying; an abstract parent type! Such an abomination. Not meant for your mortal eyes." + + archetype = POWER_ARCHETYPE_RESONANT + path = POWER_PATH_ABERRANT + priority = POWER_PRIORITY_BASIC + abstract_parent_type = /datum/power/aberrant diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root.dm new file mode 100644 index 00000000000000..0efffe2ebf4277 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root.dm @@ -0,0 +1,9 @@ +/datum/power/aberrant_root + name = "Abstract aberrant root" + desc = "You've pierced beyond the veil. Gaining dark powers you were not meant to have. Repent, sinner, by telling the developers how you got this, or be faced with the developer inquisition." + abstract_parent_type = /datum/power/aberrant_root + + mob_trait = TRAIT_ARCHETYPE_RESONANT + archetype = POWER_ARCHETYPE_RESONANT + path = POWER_PATH_ABERRANT + priority = POWER_PRIORITY_ROOT diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm new file mode 100644 index 00000000000000..4c2e7145808bd2 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm @@ -0,0 +1,29 @@ +/* + Anomalous root. The anomaly root is largely a ribbon style power, but can be neat at times. +*/ + +/datum/power/aberrant_root/anomalous + name = "Anomalous Origin" + desc = "Things just don't add up with you. You can interact with anomalies to close them, as if you were using an anomaly neutralizer." + value = 1 + +/datum/power/aberrant_root/anomalous/add(client/client_source) + RegisterSignal(power_holder, COMSIG_LIVING_UNARMED_ATTACK, PROC_REF(on_unarmed_attack)) + +/datum/power/aberrant_root/anomalous/remove() + UnregisterSignal(power_holder, COMSIG_LIVING_UNARMED_ATTACK) + +/datum/power/aberrant_root/anomalous/proc/on_unarmed_attack(mob/living/source, atom/target, proximity, modifiers) + SIGNAL_HANDLER + + if(!proximity || !istype(target, /obj/effect/anomaly)) + return NONE + + if(HAS_TRAIT(target, TRAIT_ILLUSORY_EFFECT)) + to_chat(source, span_notice("You pass your hand through [target], but nothing seems to happen. Is it really even there?")) + return COMPONENT_CANCEL_ATTACK_CHAIN + + var/obj/effect/anomaly/anomaly_target = target + to_chat(source, span_notice("You reach out and touch [anomaly_target], disrupting the anomaly!")) + anomaly_target.anomalyNeutralize() + return COMPONENT_CANCEL_ATTACK_CHAIN diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm index 6fc29b3b429193..7d325ca6cbbf10 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm @@ -1,18 +1,19 @@ /* You're immune to resonant antics! But also you're permanently silenced. */ -/datum/power/aberrant/anomaly/counter_resonance +/datum/power/aberrant/counter_resonance name = "Counter-Resonance Anomaly" desc = "You have a counteractive effect on resonance-based phenomena. You are immune to resonance-based effects (but not the highly advanced magics wielded by some antagonistic forces), and you cannot use any resonance-based powers." value = 9 + required_powers = list(/datum/power/aberrant_root/anomalous) archetype = POWER_ARCHETYPE_RESONANT path = POWER_PATH_ABERRANT -/datum/power/aberrant/anomaly/counter_resonance/add() +/datum/power/aberrant/counter_resonance/add() ADD_TRAIT(power_holder, TRAIT_ANTIRESONANCE, src) ADD_TRAIT(power_holder, TRAIT_RESONANCE_SILENCED, src) -/datum/power/aberrant/anomaly/counter_resonance/remove() +/datum/power/aberrant/counter_resonance/remove() REMOVE_TRAIT(power_holder, TRAIT_ANTIRESONANCE, src) REMOVE_TRAIT(power_holder, TRAIT_RESONANCE_SILENCED, src) diff --git a/tgstation.dme b/tgstation.dme index bdc1c1c7aa63b9..3761d8ddb2c297 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7435,6 +7435,10 @@ #include "modular_doppler\modular_powers\code\powers\mortal\expert\creature_tamer.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_action.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_power.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root_anomalous.dm" #include "modular_doppler\modular_powers\code\security\resonant_cuffs.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" From 022fd215136b6a5e452e77c2a9ad33e32559f728 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 11 Feb 2026 20:21:30 +0100 Subject: [PATCH 054/212] Adds eye for ingredients, heavy lifter, ofuscate voice, strider and master surgeon. Fixed a bug with the add/remove power button in vv. --- .../mortal/expert/eye_for_ingredients.dm | 14 +++++ .../code/powers/mortal/expert/heavy_lifter.dm | 31 ++++++++++ .../powers/mortal/expert/master_surgeon.dm | 25 ++++++++ .../powers/mortal/expert/obfuscate_voice.dm | 59 +++++++++++++++++++ .../code/powers/mortal/expert/strider.dm | 22 +++++++ .../modular_powers/code/powers_vv.dm | 5 +- tgstation.dme | 5 ++ 7 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/eye_for_ingredients.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/heavy_lifter.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/master_surgeon.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/eye_for_ingredients.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/eye_for_ingredients.dm new file mode 100644 index 00000000000000..efc778e03ba318 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/eye_for_ingredients.dm @@ -0,0 +1,14 @@ +/* + Gives TRAIT_REAGENT_SCANNER which is basically what science goggles do. +*/ + +/datum/power/expert/eye_for_ingredients + name = "Eye for Ingredients" + desc = "You've interacted with food, drinks and/or chemicals so often, you can see at a glance if something's off with it. You can see the precise reagent contents of all containers by simply examining it." + value = 3 + +/datum/power/expert/eye_for_ingredients/add() + ADD_TRAIT(power_holder, TRAIT_REAGENT_SCANNER, src) + +/datum/power/expert/eye_for_ingredients/remove() + REMOVE_TRAIT(power_holder, TRAIT_REAGENT_SCANNER, src) diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/heavy_lifter.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/heavy_lifter.dm new file mode 100644 index 00000000000000..bc444723913d64 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/heavy_lifter.dm @@ -0,0 +1,31 @@ +/* + Allows you to drag some heavy objects and people more efficiently. Also athletics boost. +*/ + +/datum/power/expert/heavy_lifter + name = "Heavy Lifter" + desc = "A strong back does a lot when it comes to carrying closets. You ignore the slowdown from dragging objects and having creatures grabbed/ and/or carried. You also start off as a Journeyman in the Athletics skill. \ + All other slowdowns such as stamina, items, damage, etc. still apply as normal." + value = 5 + // how much xp we start with on average. + var/starting_xp_base = SKILL_EXP_JOURNEYMAN + // tracks how much was given for removal later. + var/xp_given = 0 + +/datum/power/expert/heavy_lifter/add() + // Grab slowdowns all share the same movespeed id. + power_holder.add_movespeed_mod_immunities(src, MOVESPEED_ID_MOB_GRAB_STATE) + power_holder.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/bulky_drag) + // Fireman carry slowdown. + power_holder.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/human_carry) + + // We give a degree of randomness to the amount of xp given. + var/xp_mult = rand(100, 150) / 100 + xp_given = starting_xp_base * xp_mult + power_holder.mind?.adjust_experience(/datum/skill/athletics, xp_given) + +/datum/power/expert/heavy_lifter/remove() + power_holder.remove_movespeed_mod_immunities(src, MOVESPEED_ID_MOB_GRAB_STATE) + power_holder.remove_movespeed_mod_immunities(src, (/datum/movespeed_modifier/bulky_drag)) + power_holder.remove_movespeed_mod_immunities(src, (/datum/movespeed_modifier/human_carry)) + power_holder.mind?.adjust_experience(/datum/skill/athletics, -xp_given) diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/master_surgeon.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/master_surgeon.dm new file mode 100644 index 00000000000000..4c84643b14db40 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/master_surgeon.dm @@ -0,0 +1,25 @@ +/* + 1.5x speed/action success chance on surgery. + Fun fact fail_prob_index is flat amounts so we are actually giving a -50 flat which is hella busted, but also imho surgery failure chance doesn't exist outside of ghetto. +*/ + +/datum/power/expert/master_surgeon + name = "Master Surgeon" + desc = " Surgery takes composure and skill which you have aplenty. Increases your success rate and action speed with surgery by a factor of 1.5x." + value = 4 + /// 1.5x faster => multiply time by 1/1.5 + var/surgery_speed_mult = 1 / 1.5 + /// Flat reduction to failure chance (percentage points) + var/surgery_fail_reduction = 50 + +/datum/power/expert/master_surgeon/add() + RegisterSignal(power_holder, COMSIG_LIVING_INITIATE_SURGERY_STEP, PROC_REF(apply_surgery_bonuses)) + +/datum/power/expert/master_surgeon/remove() + UnregisterSignal(power_holder, COMSIG_LIVING_INITIATE_SURGERY_STEP) + +/datum/power/expert/master_surgeon/proc/apply_surgery_bonuses(mob/living/_source, mob/living/user, mob/living/target, target_zone, obj/item/tool, datum/surgery/surgery, datum/surgery_step/step, list/modifiers) + SIGNAL_HANDLER + modifiers[FAIL_PROB_INDEX] -= surgery_fail_reduction + modifiers[SPEED_MOD_INDEX] *= surgery_speed_mult + diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm new file mode 100644 index 00000000000000..19f229b477d3ae --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm @@ -0,0 +1,59 @@ +/* +Hides your voice as unknown while active. Act out the machivalean you always wanted to be, for good or bad. +*/ + +/datum/power/expert/obfuscate_voice + name = "Obfuscate Voice" + desc = "Like an actor, the sheer range in your voice is enough, with a little effort, to sound like someone entirely unfamiliar. Grants the 'Obfuscate Voice' action, making your voice unrecognizeable while active." + value = 5 + + action_path = /datum/action/cooldown/power/expert/obfuscate_voice + +/datum/action/cooldown/power/expert/obfuscate_voice + name = "Obfuscate Voice" + desc = "Makes your voice unrecognizeable while active." + button_icon = 'icons/mob/actions/actions_items.dmi' + button_icon_state = "bci_say" + + // The current in use status effect + var/datum/status_effect/power/obfuscate_voice/active_effect + + +/datum/action/cooldown/power/expert/obfuscate_voice/use_action(mob/living/user, atom/target) + if(active_effect) + qdel(active_effect) + active_effect = null + active = FALSE + return TRUE + + active_effect = user.apply_status_effect(/datum/status_effect/power/obfuscate_voice, src) + active = TRUE + return TRUE + +// We pass it on to a status effect both as a convenient handler, and also user feedback that its active with the alert pop-up. +/datum/status_effect/power/obfuscate_voice + id = "obfuscate_voice" + duration = STATUS_EFFECT_PERMANENT + alert_type = /atom/movable/screen/alert/status_effect/obfuscate_voice + var/datum/action/cooldown/power/expert/obfuscate_voice/source_action + +/datum/status_effect/power/obfuscate_voice/on_creation(mob/living/new_owner, datum/action/cooldown/power/expert/obfuscate_voice/passed_action) + . = ..() + source_action = passed_action + +/datum/status_effect/power/obfuscate_voice/on_apply() + var/mob/living/carbon/human/H = owner + if(istype(H)) + H.SetSpecialVoice("Unknown") + return TRUE + +/datum/status_effect/power/obfuscate_voice/on_remove() + var/mob/living/carbon/human/H = owner + if(istype(H)) + H.UnsetSpecialVoice() + return + +/atom/movable/screen/alert/status_effect/obfuscate_voice + name = "Obfuscate Voice" + desc = "Your voice is masked and will appear as 'Unknown' when speaking. Toggle the power again to disable." + icon_state = "mute" // swap if you have a better icon diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm new file mode 100644 index 00000000000000..f5129185d81bb2 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm @@ -0,0 +1,22 @@ +/* + No item slowdowns. Basically always having your stuff coated in slime speed pots. +*/ + +/datum/power/expert/strider + name = "Strider" + desc = "You're one hell of a hunk of a man. You ignore all slowdowns from held & worn items. \ + You also start out at Master proficiency athletics." + value = 6 + required_powers = list(/datum/power/expert/heavy_lifter) + + // how much xp we start with on average. Since the prerequisite skill gives journeyman, we subtract that. + var/starting_xp_base = SKILL_EXP_MASTER - SKILL_EXP_JOURNEYMAN + +/datum/power/expert/strider/add() + power_holder.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/equipment_speedmod) + power_holder.mind?.adjust_experience(/datum/skill/athletics, starting_xp_base) + +/datum/power/expert/strider/remove() + power_holder.remove_movespeed_mod_immunities(src, (/datum/movespeed_modifier/equipment_speedmod)) + power_holder.mind?.adjust_experience(/datum/skill/athletics, -starting_xp_base) + diff --git a/modular_doppler/modular_powers/code/powers_vv.dm b/modular_doppler/modular_powers/code/powers_vv.dm index 67b0dc858dbded..3e7a2d2053485b 100644 --- a/modular_doppler/modular_powers/code/powers_vv.dm +++ b/modular_doppler/modular_powers/code/powers_vv.dm @@ -54,7 +54,8 @@ // Removes a power. /mob/living/carbon/proc/remove_power(powertype) for(var/datum/power/power in powers) - if(!power.type == powertype) - return FALSE + if(power.type != powertype) + continue qdel(power) return TRUE + return FALSE diff --git a/tgstation.dme b/tgstation.dme index 3761d8ddb2c297..f2d836947f606a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7431,6 +7431,11 @@ #include "modular_doppler\modular_powers\code\powers_antimagic.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_action.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_power.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\expert\eye_for_ingredients.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\expert\heavy_lifter.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\strider.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\zoologist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\creature_tamer.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" From 33b9851385289ee18a5b036cc8d8c77801685330 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 12 Feb 2026 15:18:39 +0100 Subject: [PATCH 055/212] fixes a bug with the brain trauma psyker event --- .../resonant/psyker/psyker_events/catastrophic/brain_trauma.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm index e0697394424593..47eab50fa034a9 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/brain_trauma.dm @@ -3,7 +3,7 @@ weight = PSYKER_EVENT_RARITY_UNCOMMON /datum/psyker_event/catastrophic/brain_trauma/execute(mob/living/carbon/human/psyker) - if(psyker.gain_trauma_type(BRAIN_TRAUMA_SEVERE, TRAUMA_RESILIENCE_LOBOTOMY)) + if(!psyker.gain_trauma_type(BRAIN_TRAUMA_SEVERE, TRAUMA_RESILIENCE_LOBOTOMY)) // If we somehow fail to give them the trauma return FALSE //Standard message for catastrophic for when we don't explicitly want to tell them what is going to happen to them. From db625e6f790b86313a009720eff06c7645ed5ed0 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 12 Feb 2026 18:12:06 +0100 Subject: [PATCH 056/212] added vitalize flora + conjure rain. thaum spell prep now caps out at 6 regardless of how many spells you got. --- code/__DEFINES/~doppler_defines/powers.dm | 11 +- .../thaumaturge/_thaumaturge_action.dm | 2 +- .../thaumaturge/_thaumaturge_root.dm | 4 +- .../sorcerous/thaumaturge/blend_for_me.dm | 3 +- .../sorcerous/thaumaturge/brazen_bindings.dm | 1 - .../sorcerous/thaumaturge/conjure_rain.dm | 102 +++++++++++++++++ .../sorcerous/thaumaturge/gale_blast.dm | 1 - .../sorcerous/thaumaturge/magic_barrage.dm | 1 - .../sorcerous/thaumaturge/phantasmal_tool.dm | 3 +- .../sorcerous/thaumaturge/vitalize_flora.dm | 103 ++++++++++++++++++ tgstation.dme | 18 +-- 11 files changed, 231 insertions(+), 18 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 8762e275d99fea..913c8dc97c353a 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -65,9 +65,18 @@ // How much mana you practically can cap out at. #define THAUMATURGE_MAX_MANA (MAXIMUM_POWER_POINTS * THAUMATURGE_MANA_MULT ) +// The factor with which we multiply our power points to get our mana. #define THAUMATURGE_MANA_MULT 2 -// How much +// How many spells of a type can you prepare max? +#define THAUMATURGE_MAX_CHARGES_BASE 6 + +// For refund abilities, how much refund chance does each level/degree add. +#define THAUMATURGE_REFUND_MULT_BASE 35 +#define THAUMATURGE_REFUND_MULT_AFFINITY 5 + +// hard cap on refund powers. +#define THAUMATURGE_REFUND_MAX 75 /** * SORCEROUS: 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 2020fdf657d85c..a65756d91f876d 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 @@ -9,7 +9,7 @@ // Unlike normal spells, we have charges. More of that explained below at check_if_valid() var/charges = 0 // The cap on charges; you can't prepare more than these. If you leave this null, the spell will not interact with the charges system. - var/max_charges + var/max_charges = THAUMATURGE_MAX_CHARGES_BASE // How many charges does it consume on use? var/charges_to_use = 1 // How much 'mana' does it cost to prepare this per charge? 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 d8c913ffbdbec5..71a56abc565d63 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 @@ -5,7 +5,7 @@ action_path = /datum/action/cooldown/power/thaumaturge/thaumaturge_root - value = 5 + value = 3 mob_trait = TRAIT_ARCHETYPE_SORCEROUS archetype = POWER_ARCHETYPE_SORCEROUS path = POWER_PATH_THAUMATURGE @@ -29,6 +29,8 @@ button_icon = 'icons/obj/storage/book.dmi' button_icon_state = "ithaqua" + // Makes it not interact with the charges system. + max_charges = null // Lets you tweak it while you sleep. disabled_by_incapacitate = FALSE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm index 94c145c1789196..6876992b2edc8d 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm @@ -17,7 +17,6 @@ button_icon_state = "juicer" cooldown_time = 50 // we don't want people spamming the blender noise. that's it. that's the whole reason why we force a 5 second cooldown. - max_charges = 8 required_affinity = 1 prep_cost = 2 @@ -110,7 +109,7 @@ // To potentially refund it, we run a small check. /datum/action/cooldown/power/thaumaturge/blend_for_me/on_action_success(mob/living/user, atom/target, override_charges) - var/chance_to_refund = clamp(11 * affinity + 30, 20, 100) // Caps out at 85 for T5. + var/chance_to_refund = clamp(THAUMATURGE_REFUND_MULT_AFFINITY * affinity + THAUMATURGE_REFUND_MULT_BASE, 0, THAUMATURGE_REFUND_MAX) if(prob(chance_to_refund)) override_charges = 0 to_chat(owner, span_notice("Your [name] spell did not consume a charge!")) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm index c9eaefde2d1b85..fe709cc8abe29e 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm @@ -16,7 +16,6 @@ button_icon = 'icons/obj/weapons/restraints.dmi' button_icon_state = "brass_manacles" - max_charges = 7 required_affinity = 1 prep_cost = 3 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm new file mode 100644 index 00000000000000..70116154bd8570 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm @@ -0,0 +1,102 @@ +// bless my rains down with reagents. +/datum/power/thaumaturge/conjure_rain + name = "Conjure Rain" + desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u of water (for mobs, this is being splashed with that amount instead). \ + Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent. Replacing all the water in a cast will prevent slippery tiles. \ + Requires Affinity 3. Higher affinity improves the reagent conversion ratio (10% per affinity)." + value = 4 + + action_path = /datum/action/cooldown/power/thaumaturge/conjure_rain + required_powers = list(/datum/power/thaumaturge_root) + +/datum/action/cooldown/power/thaumaturge/conjure_rain + name = "Conjure Rain" + desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u water (for mobs, this is being splashed with that amount instead). \ + Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent. Replacing all the water in a cast will prevent slippery tiles. \ " + button_icon = 'icons/effects/weather_effects.dmi' + button_icon_state = "rain_low" + + required_affinity = 3 + prep_cost = 4 + click_to_activate = TRUE + anti_magic_on_click_target = FALSE + + // the chem that the base rain uses + var/rain_chem = /datum/reagent/water + // the base conversion ratio of the chem. + var/base_chem_ratio = 1 + // how much the ratio increases per affinity level + var/affinity_chem_bonus = 0.1 + +/datum/action/cooldown/power/thaumaturge/conjure_rain/use_action(mob/living/user, atom/target) + var/turf/target_turf = get_turf(target) + if(!target_turf) + return FALSE + + // creatures the reagent buffer and adds water + var/obj/effect/abstract/thaum_rain_buffer/buffer = new(target_turf, 20) + buffer.reagents.add_reagent(rain_chem, buffer.buffer_volume) + + // If we have a held container, convert some of the rain into that reagent. + var/obj/item/reagent_containers/held_container = user.get_active_held_item() + if(istype(held_container) && held_container.reagents?.total_volume) + var/drain_amount = min(buffer.buffer_volume, held_container.reagents.total_volume) + if(drain_amount > 0) + buffer.reagents.remove_reagent(rain_chem, drain_amount) // 1:1 water consumption + var/chem_ratio = base_chem_ratio + (affinity_chem_bonus * (affinity - required_affinity)) + if(chem_ratio < 0) + chem_ratio = 0 + held_container.reagents.trans_to(buffer.reagents, drain_amount, chem_ratio, transferred_by = user) + + // sets the rain color and plays the noise + var/rain_color = mix_color_from_reagents(buffer.reagents.reagent_list) + playsound(target, 'sound/effects/splat.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + + // every tile in range... + for(var/turf/area_turf in range(1, target_turf)) + // splash it onto the space. + buffer.reagents.expose(area_turf, TOUCH) + // applies it to every reagent container in the area + for(var/obj/item/reagent_containers/target_container in area_turf) + if(target_container.reagents) + buffer.reagents.trans_to(target_container, buffer.buffer_volume, transferred_by = user, copy_only = TRUE) + // splashes it onto every mob in the area + for(var/mob/living/area_mob in area_turf) + buffer.reagents.expose(area_mob, TOUCH) + + // rain fx + new /obj/effect/temp_visual/thaum_rain(area_turf, rain_color) + + qdel(buffer) + return TRUE + + +// We create a temporary buffer for holding the reagents. +/obj/effect/abstract/thaum_rain_buffer + name = "resonant beaker" + desc = "You caught me doing it again; I did it once with the blender, now I am doing it again. YES. This is NECESSARY for Reagents. Don't you judge the coder! You aren't even meant to see this, peasant!" + invisibility = INVISIBILITY_ABSTRACT + anchored = TRUE + density = FALSE + + var/datum/reagents/reagent_buffer + // also affects how much our rain produces + var/buffer_volume = 20 + +/obj/effect/abstract/thaum_rain_buffer/Initialize(mapload, new_buffer_volume) + . = ..() + if(isnum(new_buffer_volume) && new_buffer_volume > 0) + buffer_volume = new_buffer_volume + reagents = new /datum/reagents(buffer_volume, src) + reagents.flags = TRANSPARENT | DRAINABLE + +/obj/effect/temp_visual/thaum_rain + name = "magical rain" + icon = 'icons/effects/weather_effects.dmi' + icon_state = "rain_high" + duration = 1 SECONDS + +/obj/effect/temp_visual/thaum_rain/Initialize(mapload, set_color) + if(set_color) + add_atom_colour(set_color, FIXED_COLOUR_PRIORITY) + return ..() 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 830990df2826c7..5b3adf7da893a7 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 @@ -20,7 +20,6 @@ button_icon = 'icons/effects/effects.dmi' button_icon_state = "smoke" - max_charges = 7 required_affinity = 3 prep_cost = 3 click_to_activate = TRUE 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 07cb75c81c76e6..b1a43625f063ed 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 @@ -17,7 +17,6 @@ button_icon = 'icons/obj/weapons/guns/projectiles.dmi' button_icon_state = "arcane_barrage" - max_charges = 5 required_affinity = 3 prep_cost = 5 anti_magic_on_click_target = FALSE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm index 23f99001277998..075c2a4ef0d3c5 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm @@ -15,7 +15,6 @@ button_icon = 'icons/obj/weapons/club.dmi' button_icon_state = "hypertool" - max_charges = 7 required_affinity = 1 prep_cost = 3 @@ -46,7 +45,7 @@ // To potentially refund it, we run a small check. /datum/action/cooldown/power/thaumaturge/phantasmal_tool/on_action_success(mob/living/user, atom/target, override_charges) - var/chance_to_refund = clamp(11 * affinity + 30, 20, 100) // Caps out at 85 for T5. + var/chance_to_refund = clamp(THAUMATURGE_REFUND_MULT_AFFINITY * affinity + THAUMATURGE_REFUND_MULT_BASE, 0, THAUMATURGE_REFUND_MAX) if(prob(chance_to_refund)) override_charges = 0 to_chat(owner, span_notice("Your [name] spell did not consume a charge!")) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm new file mode 100644 index 00000000000000..9879db3605e844 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm @@ -0,0 +1,103 @@ +// Does a lot of things to plants. Makes em grow, makes em produce, makes em healthy. +// This applies to A LOT of plants. Interpet as you will. + +/datum/power/thaumaturge/vitalize_flora + name = "Vitalize Flora" + desc = "Breathes life into the plants around you. This heals any and all plants (including plant creatures), makes them grow if they're still in the growth phase, and speeds up the time until the next harvest. \ + Requires Affinity 1. Affinity gives a chance to not consume charges." + value = 2 + + action_path = /datum/action/cooldown/power/thaumaturge/vitalize_flora + required_powers = list(/datum/power/thaumaturge_root) + +/datum/action/cooldown/power/thaumaturge/vitalize_flora + name = "Vitalize Flora" + desc = "Breathes life into the plants around you. This heals any and all plants (including plant creatures), makes them grow if they're still in the growth phase, and speeds up the time until the next harvest." + button_icon = 'icons/obj/fluff/flora/plants.dmi' + button_icon_state = "plant-03" + + required_affinity = 1 + prep_cost = 2 + + + // the amount to heal mob plants by + var/mob_heal_amount = 10 + // the amount to heal non-mob plants by + var/obj_heal_amount = 10 + // How many seconds to make it grow by. + var/grow_amount = (15 SECONDS) / (HYDROTRAY_CYCLE_DELAY) + +/datum/action/cooldown/power/thaumaturge/vitalize_flora/use_action(mob/living/user, atom/target) + var/turf/user_turf = get_turf(user) + if(!user_turf) + return FALSE + + // affected anything at all + var/affected_anything = FALSE + // affected something this cycle + var/affected_anything_this_cycle = FALSE + // Get everyhing in a 3x3 area + for(var/turf/area_turf in range(1, user_turf)) + affected_anything_this_cycle = FALSE + // If hydro tray: Heals the plant inside it. + for(var/obj/machinery/hydroponics/hydro_tray in area_turf) + if(!hydro_tray.myseed || hydro_tray.plant_status == HYDROTRAY_PLANT_DEAD) + continue + // heals the plant + hydro_tray.adjust_plant_health(obj_heal_amount) + // if its not fully aged yet; make it age. + if(hydro_tray.age < hydro_tray.myseed.maturation) + hydro_tray.age += grow_amount + hydro_tray.lastproduce = hydro_tray.age + // if it is mature, advance progress toward next harvest + else + hydro_tray.lastproduce = max(hydro_tray.lastproduce - grow_amount, 0) + hydro_tray.update_appearance() + affected_anything = TRUE + affected_anything_this_cycle = TRUE + + // As above, but instead of hydotray its other flora objects. + for(var/obj/structure/flora/area_flora in area_turf) + if(area_flora.get_integrity() < area_flora.max_integrity) + area_flora.repair_damage(obj_heal_amount) + if(area_flora.harvested && prob(30)) // Because of how area flora is coded this is best we can do to speed it up in a way that isn't always success. + area_flora.regrow() + affected_anything = TRUE + affected_anything_this_cycle = TRUE + + // Heals plant mobs in the area for either burn, brute or tox. + for(var/mob/living/area_mob in area_turf) + if(!(area_mob.mob_biotypes & MOB_PLANT)) + continue + // prevents charge consumption for podpeople when theyre at full hp + if(area_mob.health >= area_mob.maxHealth) + continue + area_mob.heal_ordered_damage(mob_heal_amount, list(BRUTE, BURN, TOX)) + affected_anything = TRUE + affected_anything_this_cycle = TRUE + + //glowy particles to tell people somethings happening on that space. + if(affected_anything_this_cycle) + new /obj/effect/temp_visual/plant_growth(area_turf) + + if(!affected_anything) + user.balloon_alert(user, "no valid targets in range!") + return FALSE + playsound(user, 'sound/effects/magic/charge.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + return TRUE + +// Refund chance, similar to phantasmal tool & blend for me. +/datum/action/cooldown/power/thaumaturge/vitalize_flora/on_action_success(mob/living/user, atom/target, override_charges) + var/chance_to_refund = clamp(THAUMATURGE_REFUND_MULT_AFFINITY * affinity + THAUMATURGE_REFUND_MULT_BASE, 0, THAUMATURGE_REFUND_MAX) + if(prob(chance_to_refund)) + override_charges = 0 + to_chat(owner, span_notice("Your [name] spell did not consume a charge!")) + else if(chance_to_refund >= 51) // At this point it's more common that it does not consume a charge, so we invert them and tell them when it does consume a charge! + to_chat(owner, span_warning("Your [name] spell consumed a charge!")) + return ..(user, target, override_charges) + +// visual effect for plant growth +/obj/effect/temp_visual/plant_growth + icon_state = "blessed" + color = "#24da3c" + duration = 1 SECONDS diff --git a/tgstation.dme b/tgstation.dme index f2d836947f606a..9b8f7f80f627ab 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7444,14 +7444,6 @@ #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\security\resonant_cuffs.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_root.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_spell.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\lodestone_legends.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_action.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_power.dm" @@ -7463,6 +7455,16 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\gale_blast.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\magic_barrage.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\brazen_bindings.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\conjure_rain.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\vitalize_flora.dm" +#include "modular_doppler\modular_powers\code\security\resonant_cuffs.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_root.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_spell.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\lodestone_legends.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_action.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" From 0cdd2e8fc0a074da4601114d7c33d71cf7512b92 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 12 Feb 2026 19:06:53 +0100 Subject: [PATCH 057/212] adds cast timers, fixes some thaumaturge bugs, made conjure rain chems be restricted to synthesizable chems --- .../thaumaturge/_thaumaturge_preperation.dm | 4 +++ .../sorcerous/thaumaturge/conjure_rain.dm | 29 ++++++++++++++++--- .../modular_powers/code/powers_action.dm | 25 ++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm index cd2102c2ff7404..3277f5ab77dc23 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm @@ -45,6 +45,9 @@ /datum/component/thaumaturge_preparation/proc/on_sleep_set(mob/living/source, amount) SIGNAL_HANDLER + // Only trigger on entering sleep (not waking, shortening, or extending existing sleep). + if(amount <= 0 || source.IsSleeping()) + return // Do we have queqed changes and is the flag that it passed validation on? if(applied_prepared_charges && recharge_when_sleep) //Do we have the focus on our person? @@ -96,6 +99,7 @@ if(first_time_preperation) if(apply_spell_charges()) first_time_preperation = FALSE + recharge_when_sleep = TRUE to_chat(attached_mob, span_notice("Your spell preperation has been applied!")) else to_chat(attached_mob, span_warning("Something went wrong when applying spell charges; this shouldn't happen! Yell at a dev!")) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm index 70116154bd8570..4cb81c8f5c3f93 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm @@ -2,7 +2,7 @@ /datum/power/thaumaturge/conjure_rain name = "Conjure Rain" desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u of water (for mobs, this is being splashed with that amount instead). \ - Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent. Replacing all the water in a cast will prevent slippery tiles. \ + Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ Requires Affinity 3. Higher affinity improves the reagent conversion ratio (10% per affinity)." value = 4 @@ -12,7 +12,7 @@ /datum/action/cooldown/power/thaumaturge/conjure_rain name = "Conjure Rain" desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u water (for mobs, this is being splashed with that amount instead). \ - Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent. Replacing all the water in a cast will prevent slippery tiles. \ " + Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ " button_icon = 'icons/effects/weather_effects.dmi' button_icon_state = "rain_low" @@ -21,6 +21,10 @@ click_to_activate = TRUE anti_magic_on_click_target = FALSE + use_time_overlay_type = /obj/effect/temp_visual/conjure_rain + use_time = 5 + use_time_flags = IGNORE_USER_LOC_CHANGE + // the chem that the base rain uses var/rain_chem = /datum/reagent/water // the base conversion ratio of the chem. @@ -40,13 +44,22 @@ // If we have a held container, convert some of the rain into that reagent. var/obj/item/reagent_containers/held_container = user.get_active_held_item() if(istype(held_container) && held_container.reagents?.total_volume) - var/drain_amount = min(buffer.buffer_volume, held_container.reagents.total_volume) + var/synth_volume = 0 + for(var/datum/reagent/reagent as anything in held_container.reagents.reagent_list) + if(reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED)// Prevents us from duping SPECIAL CHEMS. + synth_volume += reagent.volume + var/drain_amount = min(buffer.buffer_volume, synth_volume) if(drain_amount > 0) buffer.reagents.remove_reagent(rain_chem, drain_amount) // 1:1 water consumption var/chem_ratio = base_chem_ratio + (affinity_chem_bonus * (affinity - required_affinity)) + // in some alt universe you get negative chem ratio if(chem_ratio < 0) chem_ratio = 0 - held_container.reagents.trans_to(buffer.reagents, drain_amount, chem_ratio, transferred_by = user) + var/part = drain_amount / synth_volume + for(var/datum/reagent/reagent as anything in held_container.reagents.reagent_list) + var/transfer_amount = reagent.volume * part + if(transfer_amount > 0) + held_container.reagents.trans_to(buffer.reagents, transfer_amount, chem_ratio, target_id = reagent.type, transferred_by = user) // sets the rain color and plays the noise var/rain_color = mix_color_from_reagents(buffer.reagents.reagent_list) @@ -70,6 +83,8 @@ qdel(buffer) return TRUE +// Adds a cast effect, just to make it clear to EVEROYNE we're about to rain some shit down on them. + // We create a temporary buffer for holding the reagents. /obj/effect/abstract/thaum_rain_buffer @@ -100,3 +115,9 @@ if(set_color) add_atom_colour(set_color, FIXED_COLOUR_PRIORITY) return ..() + +// visual effect on the caster for casting rain +/obj/effect/temp_visual/conjure_rain + icon_state = "blessed" + color = "#243fda" + duration = 1 SECONDS diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 0b730ab2a48409..7f16cb64ee287a 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -32,6 +32,13 @@ // Do we need our hands free? var/need_hands_free = TRUE + /// If set, we must wait this long before use_action executes. Cast time basically. + var/use_time = 0 + /// Flags passed to do_after during use_time (e.g. IGNORE_HELD_ITEM, IGNORE_USER_LOC_CHANGE). + var/use_time_flags = NONE + /// Optional overlay to show on the user during use_time. + var/use_time_overlay_type + /// Maximum targeting range (in tiles) for click_to_activate powers. Set to 0 or null for no range limit. var/target_range /// If set, clicked target MUST be of this type (or subtype). @@ -51,6 +58,8 @@ if(mob_target.can_block_resonance(1)) // Runs the special can_block_resonance function which also handles the anti-magic part. // I would like to deduct resources on spell fail, but that is going to be so utterly complex. TODO for the future chap who wants this. return FALSE + if(!do_use_time(user, target)) + return FALSE if(use_action(user, target)) on_action_success() return TRUE @@ -89,6 +98,22 @@ /datum/action/cooldown/power/proc/use_action(mob/living/user, atom/target) return TRUE +// Handles optional channel time before the action goes off. +/datum/action/cooldown/power/proc/do_use_time(mob/living/user, atom/target) + if(use_time <= 0) + return TRUE + var/atom/use_target = target ? target : user + var/mutable_appearance/use_overlay + if(use_time_overlay_type) + var/atom/overlay_obj = new use_time_overlay_type(null) + use_overlay = new /mutable_appearance(overlay_obj) + qdel(overlay_obj) + user.add_overlay(use_overlay) + var/success = do_after(user, use_time, target = use_target, timed_action_flags = use_time_flags) + if(use_overlay && !QDELETED(user)) + user.cut_overlay(use_overlay) + return success + // Anything that should happen as a result of use_action returning TRUE. // Cost systems for archetypes to name an example. /datum/action/cooldown/power/proc/on_action_success(mob/living/user, atom/target) From a41a5c8de2b0e3760a2ccd3fe5c238bb249ae557 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 15 Feb 2026 09:14:52 +0100 Subject: [PATCH 058/212] Lots of bugfixes and tweaks. Null rod now dispells on hit (cascading). Fixed the active_overlay_icon_state not appearing. Fixed chaplain not getting bonuses. Vitalize flora now propogates kudzu and also you can grow the little tree on turtles :). --- code/datums/actions/action.dm | 7 ++++ .../sorcerous/thaumaturge/vitalize_flora.dm | 21 ++++++++++-- .../sorcerous/theologist/pious_prayer.dm | 4 +-- .../sorcerous/theologist/smiting_strike.dm | 10 ++++-- .../theologist/smiting_strike_upgrades.dm | 4 +-- .../modular_powers/code/powers_action.dm | 24 +++++++++++++- .../modular_powers/code/powers_antimagic.dm | 32 +++++++++++++++++++ 7 files changed, 92 insertions(+), 10 deletions(-) diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm index 65c314d4fc976b..9b836f6909a22b 100644 --- a/code/datums/actions/action.dm +++ b/code/datums/actions/action.dm @@ -312,6 +312,13 @@ */ /datum/action/proc/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force = FALSE) + var/active = is_action_active(current_button) + var/base_state = (istype(src, /datum/action/cooldown) ? src:base_overlay_icon_state : null) + var/active_state = (istype(src, /datum/action/cooldown) ? src:active_overlay_icon_state : null) + + if(overlay_icon && overlay_icon_state) + to_chat(owner, span_notice("[src] overlay apply: active?=[active] base=[base_state] active_state=[active_state] current_active=[current_button.active_overlay_icon_state] force=[force]")) + SEND_SIGNAL(src, COMSIG_ACTION_OVERLAY_APPLY, current_button, force) if(!overlay_icon || !overlay_icon_state || (current_button.active_overlay_icon_state == overlay_icon_state && !force)) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm index 9879db3605e844..609218d394a967 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm @@ -21,7 +21,7 @@ // the amount to heal mob plants by - var/mob_heal_amount = 10 + var/mob_heal_amount = 15 // the amount to heal non-mob plants by var/obj_heal_amount = 10 // How many seconds to make it grow by. @@ -36,6 +36,10 @@ var/affected_anything = FALSE // affected something this cycle var/affected_anything_this_cycle = FALSE + // nearby vine to propagate from (if any) + var/obj/structure/spacevine/nearby_vine = locate(/obj/structure/spacevine) in range(1, user_turf) + var/datum/spacevine_controller/vine_master = nearby_vine?.master + // Get everyhing in a 3x3 area for(var/turf/area_turf in range(1, user_turf)) affected_anything_this_cycle = FALSE @@ -65,14 +69,25 @@ affected_anything = TRUE affected_anything_this_cycle = TRUE - // Heals plant mobs in the area for either burn, brute or tox. + // Kudzu growth (spacevines) around the caster, only if vines are nearby + if(vine_master && !isspaceturf(area_turf) && !locate(/obj/structure/spacevine) in area_turf) + vine_master.spawn_spacevine_piece(area_turf, nearby_vine, list()) + affected_anything = TRUE + affected_anything_this_cycle = TRUE + + // Heals plant mobs in the area for either burn, brute or tox. Also does things to turtles. for(var/mob/living/area_mob in area_turf) if(!(area_mob.mob_biotypes & MOB_PLANT)) continue - // prevents charge consumption for podpeople when theyre at full hp + // prevents charge consumption for platn creatures when theyre at full hp if(area_mob.health >= area_mob.maxHealth) continue + // heals plant creatures area_mob.heal_ordered_damage(mob_heal_amount, list(BRUTE, BURN, TOX)) + // so there's these cute turtles that can grow plants on themselves and clearly we should be able to grow that too. + if(istype(area_mob, /mob/living/basic/turtle)) + var/mob/living/basic/turtle/plant_turtle = area_mob + plant_turtle.set_plant_growth(plant_turtle.retrieve_destined_path(), grow_amount) affected_anything = TRUE affected_anything_this_cycle = TRUE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm index d14f40092f0e05..dd7c22c45057d3 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm @@ -30,11 +30,11 @@ /datum/action/cooldown/power/theologist/pious_prayer/New() // Increase prayer cap based on various factors. // Are you the Chaplain? - if(is_chaplain_job(owner.mind?.assigned_role)) + if(is_chaplain_job(usr.mind?.assigned_role)) prayer_cap = 15 return // Do you have the religious quirk? - if(HAS_TRAIT(owner, TRAIT_SPIRITUAL)) + if(HAS_TRAIT(usr, TRAIT_SPIRITUAL)) prayer_cap = 10 return diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm index 38ea3ec673cffa..7420b820596f49 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm @@ -3,7 +3,7 @@ desc = "Channel energy into the item you are currently holding. Your next attack that hits with it against a creature deals 15 additional burn damage and sends them flying backwards 4 spaces. \ This knockback cannot stun or damage on impact. Costs 5 Piety to use. This effect ends if the item leaves your hands." action_path = /datum/action/cooldown/power/theologist/smiting_strike - value = 5 + value = 4 archetype = POWER_ARCHETYPE_SORCEROUS path = POWER_PATH_THEOLOGIST @@ -15,7 +15,7 @@ This knockback cannot stun or damage on impact. Costs 5 Piety to use. This effect ends if the item leaves your hands." button_icon = 'icons/mob/actions/actions_cult.dmi' button_icon_state = "sword_fling" - cooldown_time = 150 + cooldown_time = 60 cost = 5 // How much damage the smite element will do @@ -36,6 +36,10 @@ to_chat(owner, span_warning("You aren't holding anything that can be imbued!")) return FALSE + // To prevent you from smiting with something that doesn't normally want you to attack wtih it. + if(potential_smite.force <= 0) + to_chat(owner, span_warning("Item is too weak")) + return FALSE // In order to detect our buff, we pass along a trait to the host item. if(HAS_TRAIT(potential_smite, TRAIT_HAS_SMITING_STRIKE)) to_chat(owner, span_warning("The item is already imbued!")) @@ -68,6 +72,8 @@ // Whilst I originally considered adding just the knockback element, we kind-of want more control over when the smite fades. /datum/element/theologist_smite + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 /// extra damage the smite does var/smite_damage /// distance the atom will be thrown diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm index 4c73927390145a..24a25cf6b264aa 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm @@ -4,14 +4,14 @@ Most of the effects are already baked into the existing power for convenience. /datum/power/theologist/smiting_strike/imbue_armaments name = "Imbue Armaments" desc = "Changes Smiting Strike to no longer be removed when it passes hands, and allows you to have an unlimited amount of items blessed. Reduces the smite effect's knockback by 2 and damage by 5." - value = 5 + value = 3 archetype = POWER_ARCHETYPE_SORCEROUS path = POWER_PATH_THEOLOGIST required_powers = list(/datum/power/theologist/smiting_strike) action_path = null // So we don't give em another use of the ability. -/datum/power/theologist/smiting_strike/imbue_armaments/add() +/datum/power/theologist/smiting_strike/imbue_armaments/post_add() . = ..() var/datum/power/theologist/smiting_strike/smiting_strike = power_holder.get_power(/datum/power/theologist/smiting_strike) var/datum/action/cooldown/power/theologist/smiting_strike/smite_action = smiting_strike.action_path // I really should find a better way to get the variables of actions. diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 7f16cb64ee287a..9ea97101a8e492 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -169,8 +169,10 @@ Handles all the logic involved in using a targeted, click-based action. // If the power can't be used, refuse the click and keep intercept state as-is. if(!try_use(clicker, target)) + // fixes the overlay from cast time getting stuck. + if(clicker?.click_intercept == src) + unset_click_ability(clicker, refund_cooldown = TRUE) return FALSE - StartCooldown() // Successful click. @@ -180,6 +182,26 @@ Handles all the logic involved in using a targeted, click-based action. clicker.next_click = world.time + click_cd_override return TRUE +// We override the click abilities to fix an issue with the active_overlay_icon_state not appearing when it should. +/datum/action/cooldown/power/set_click_ability(mob/on_who) + . = ..() + if(.) + build_all_button_icons(UPDATE_BUTTON_STATUS | UPDATE_BUTTON_OVERLAY) + return . + +/datum/action/cooldown/power/unset_click_ability(mob/on_who, refund_cooldown = TRUE) + . = ..() + if(.) + for(var/datum/action/A as anything in on_who.actions) + for(var/datum/hud/H as anything in A.viewers) + var/atom/movable/screen/movable/action_button/B = A.viewers[H] + if(B) + B.cut_overlay(B.button_overlay) + B.button_overlay = null + B.active_overlay_icon_state = null + on_who.update_mob_action_buttons(UPDATE_BUTTON_OVERLAY, TRUE) + return . + /* Projectile action code down below */ diff --git a/modular_doppler/modular_powers/code/powers_antimagic.dm b/modular_doppler/modular_powers/code/powers_antimagic.dm index 44ce0dc72b7ad2..b3709ec8a526e6 100644 --- a/modular_doppler/modular_powers/code/powers_antimagic.dm +++ b/modular_doppler/modular_powers/code/powers_antimagic.dm @@ -70,6 +70,38 @@ Dispel proc handler playsound(target, 'sound/effects/magic/smoke.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) return was_dispersed +/* + Adds dispel on hit for the null rod. +*/ +/obj/item/nullrod/Initialize(mapload) + . = ..() + AddElement(/datum/element/resonant_dispel_hit, TRUE) + +/* + Component to make something dispel on smack. +*/ + +/datum/element/resonant_dispel_hit + element_flags = ELEMENT_BESPOKE + argument_hash_start_idx = 2 + // does it cascade it's dispel (Everything on the target) + var/cascade_dispels + +/datum/element/resonant_dispel_hit/Attach(datum/target, cascade = FALSE) + . = ..() + target.AddElementTrait(TRAIT_ON_HIT_EFFECT, REF(src), /datum/element/on_hit_effect) + RegisterSignal(target, COMSIG_ON_HIT_EFFECT, PROC_REF(dispel_on_hit)) + if(cascade) + cascade_dispels = TRUE + +/datum/element/resonant_dispel_hit/Detach(datum/source) + UnregisterSignal(source, COMSIG_ON_HIT_EFFECT) + REMOVE_TRAIT(source, TRAIT_ON_HIT_EFFECT, REF(src)) + return ..() + +/datum/element/resonant_dispel_hit/proc/dispel_on_hit(datum/source, atom/attacker, atom/damage_target, hit_zone, throw_hit) + SIGNAL_HANDLER + dispel(damage_target, attacker, cascade_dispels ? DISPEL_CASCADE_CARRIED : null) /* Very simple wiz spell to test dispel functionality, plus for admeme purposes. From d14a52ca0becf3d441e249e9659e94d761c6aab4 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 15 Feb 2026 13:16:34 +0100 Subject: [PATCH 059/212] Basics for warfighter, adds the command category. Fixes a few bugs to do with the action system. --- code/__DEFINES/~doppler_defines/powers.dm | 17 ++++ code/datums/actions/action.dm | 8 -- .../mortal/warfighter/_command_action.dm | 92 +++++++++++++++++++ .../mortal/warfighter/_warfighter_action.dm | 4 + .../mortal/warfighter/_warfighter_power.dm | 8 ++ .../powers/mortal/warfighter/command_grit.dm | 52 +++++++++++ .../mortal/warfighter/command_recover.dm | 31 +++++++ .../thaumaturge/_thaumaturge_action.dm | 4 +- .../modular_powers/code/powers_action.dm | 2 +- tgstation.dme | 5 + 10 files changed, 213 insertions(+), 10 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/_command_action.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_action.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_power.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 913c8dc97c353a..ce2fecbbd3e590 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -178,6 +178,23 @@ #define MARTIALART_BREACHERKNUCKLE "breacher knuckle" #define MARTIALART_MAD_DOG "the mag dog style" +/** + * MORTAL: WARFIGHTER + * All defines related to the augmented powers. + */ + +// The amount to multiple the effects of all commander powers by. +#define WARFIGHTER_COMMANDER_BASE_MULT 1 + +// The multiplier bonus for sharing a department with the target as a commander +#define WARFIGHTER_COMMANDER_DEPARTMENT_BONUS 0.3 + +// The multiplier bonus for being a head of staff as a commander +#define WARFIGHTER_COMMANDER_HEAD_BONUS 0.3 + +// The global GCD for Warfigher powers +#define WARFIGHTER_COMMANDER_SHARED_COOLDOWN 2 SECONDS + /** * MORTAL: Augmented * All defines related to the augmented powers. diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm index 9b836f6909a22b..88913edcf1e723 100644 --- a/code/datums/actions/action.dm +++ b/code/datums/actions/action.dm @@ -311,14 +311,6 @@ * force - whether an update is forced regardless of existing status */ /datum/action/proc/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force = FALSE) - - var/active = is_action_active(current_button) - var/base_state = (istype(src, /datum/action/cooldown) ? src:base_overlay_icon_state : null) - var/active_state = (istype(src, /datum/action/cooldown) ? src:active_overlay_icon_state : null) - - if(overlay_icon && overlay_icon_state) - to_chat(owner, span_notice("[src] overlay apply: active?=[active] base=[base_state] active_state=[active_state] current_active=[current_button.active_overlay_icon_state] force=[force]")) - SEND_SIGNAL(src, COMSIG_ACTION_OVERLAY_APPLY, current_button, force) if(!overlay_icon || !overlay_icon_state || (current_button.active_overlay_icon_state == overlay_icon_state && !force)) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/_command_action.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_command_action.dm new file mode 100644 index 00000000000000..518e4b89639448 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_command_action.dm @@ -0,0 +1,92 @@ + +/* Commands work in their own little way so they get a different typepath. */ +/datum/action/cooldown/power/warfighter/command + name = "COMMAND abstract parent type" + desc = "This is what the Karens in management think they have. Great power. But really this doesn't do anything; this is just an abstract type. Demand to speak to the manager of the server and that they fix this." + + click_to_activate = TRUE + target_self = FALSE + // we validate hands free differently given you can give commands with your voice and all that. + need_hands_free = FALSE + // silicons I hate to break it to you but you aren't included. + target_type = /mob/living/carbon + + // is the user a command staff + var/command_bonus = FALSE + // is the target part of the user's department? + var/department_bonus = FALSE + + // the total effectiveness modifier for commander powers + var/commander_modifier = WARFIGHTER_COMMANDER_BASE_MULT + + // the symbol displayed over the target's head when using the action + var/action_symbol = "point" + +/datum/action/cooldown/power/warfighter/command/proc/is_command_staff(mob/living/user) + var/datum/job/assigned = user?.mind?.assigned_role + if(!assigned) + return FALSE + return (assigned.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) || (assigned.job_flags & JOB_HEAD_OF_STAFF) + +/datum/action/cooldown/power/warfighter/command/proc/is_same_department(mob/living/user, mob/living/target) + var/datum/job/user_job = user?.mind?.assigned_role + var/datum/job/target_job = target?.mind?.assigned_role + if(!user_job || !target_job) + return FALSE + return (user_job.departments_bitflags & target_job.departments_bitflags) + +/datum/action/cooldown/power/warfighter/command/can_use(mob/living/user, mob/living/target) + . = ..() + // If the target can't hear or see you + if(!target.can_hear() && !can_see(target, user)) + owner.balloon_alert(user, "target can't perceive you!") + return FALSE + // If we can't talk nor use our hands. + if(!user.can_speak() && HAS_TRAIT(user, TRAIT_HANDS_BLOCKED)) + owner.balloon_alert(user, "you're unable to relay your commands!") + return FALSE + +// We do the department checks in here. We force a parent call because commands kind-of are build around this system. +/datum/action/cooldown/power/warfighter/command/use_action(mob/living/user, mob/living/target) + SHOULD_CALL_PARENT(TRUE) + . = ..() + commander_modifier = WARFIGHTER_COMMANDER_BASE_MULT + command_bonus = is_command_staff(user) + department_bonus = is_same_department(user, target) + if(department_bonus) + commander_modifier += WARFIGHTER_COMMANDER_DEPARTMENT_BONUS + if(command_bonus) + commander_modifier += WARFIGHTER_COMMANDER_HEAD_BONUS + +/datum/action/cooldown/power/warfighter/command/on_action_success(mob/living/user, mob/living/target) + . = ..() + var/mutable_appearance/user_symbol = mutable_appearance('icons/effects/callouts.dmi', "danger") + user_symbol.pixel_y = 16 + user_symbol.color = "#cc3d3d" + SET_PLANE_EXPLICIT(user_symbol, ABOVE_LIGHTING_PLANE, user) + var/mutable_appearance/target_symbol = mutable_appearance('icons/effects/callouts.dmi', action_symbol) + target_symbol.pixel_y = 16 + target_symbol.color = "#cc3d3d" + SET_PLANE_EXPLICIT(target_symbol, ABOVE_LIGHTING_PLANE, target) + // applies the status effect overlay + user.flick_overlay_static(user_symbol, 2 SECONDS) + target.flick_overlay_static(target_symbol, 2 SECONDS) + + // plays the sound to only the target and the user given that it's kind-of obnoxious. + var/turf/origin = get_turf(user) + var/sound_file = 'sound/items/whistle/whistle.ogg' + user.playsound_local(origin, sound_file, 40, TRUE) + target.playsound_local(origin, sound_file, 40, TRUE) + + // starts the gcd + start_command_gcd(user) + +// starts a gcd for all warifghter poewrs. +/datum/action/cooldown/power/warfighter/command/proc/start_command_gcd(mob/living/user) + for(var/datum/action/A as anything in user.actions) + if(!istype(A, /datum/action/cooldown/power/warfighter/command)) + continue + var/datum/action/cooldown/power/warfighter/command/C = A + if(C.next_use_time <= world.time) + C.StartCooldownSelf(2 SECONDS) + diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_action.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_action.dm new file mode 100644 index 00000000000000..c90015bdda64b4 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_action.dm @@ -0,0 +1,4 @@ +/datum/action/cooldown/power/warfighter + name = "abstract expert warfighter action - ahelp this" + resonant = FALSE + background_icon_state = "bg_cult" diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_power.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_power.dm new file mode 100644 index 00000000000000..3fad79071cbb4b --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/_warfighter_power.dm @@ -0,0 +1,8 @@ +/datum/power/warfighter + name = "Warfighter Power" + desc = "Odysseus wouldn't want you to see what's inside the Trojan Horse now would they? Report this abstract type, or suffer ill consequence." + + archetype = POWER_ARCHETYPE_MORTAL + path = POWER_PATH_WARFIGHTER + priority = POWER_PRIORITY_BASIC + abstract_parent_type = /datum/power/warfighter diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm new file mode 100644 index 00000000000000..08887b7a94ec23 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm @@ -0,0 +1,52 @@ +/datum/power/warfighter/command_grit + name = "Command: Grit" + desc = "Whilst active, the target ignores pain for 15 seconds, as well as slowdown from damage and stamina loss. Has a long cooldown. Increased effect lenghtens duration." + + value = 5 + required_powers + action_path = /datum/action/cooldown/power/warfighter/command/grit + required_powers = list(/datum/power/warfighter/command_recover) + +/datum/action/cooldown/power/warfighter/command/grit + name = "Command: Grit" + desc = "Whilst active, the target ignores pain for 15 seconds, as well as slowdown from damage and stamina loss. Has a long cooldown. Increased effect lenghtens duration." + + cooldown_time = 600 + button_icon = 'icons/hud/guardian.dmi' + button_icon_state = "protector" + action_symbol = "guard" + +/datum/action/cooldown/power/warfighter/command/grit/use_action(mob/living/user, mob/living/carbon/target) + . = ..() + target.apply_status_effect(/datum/status_effect/power/command_grit, commander_modifier) + +// Status effect that Burden Revered applies +/datum/status_effect/power/command_grit + id = "command_grit" + show_duration = TRUE + duration = 15 SECONDS // baseline + tick_interval = -1 + alert_type = /atom/movable/screen/alert/status_effect/command_grit + +/datum/status_effect/power/command_grit/on_creation(mob/living/new_owner, commander_modifier) + if(isnum(commander_modifier)) + duration = 15 SECONDS * commander_modifier + . = ..() + +/datum/status_effect/power/command_grit/on_apply() + ADD_TRAIT(owner, TRAIT_ANALGESIA, type) + owner.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/damage_slowdown) + owner.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/basic_stamina_slowdown) + return TRUE + +/datum/status_effect/power/command_grit/on_remove() + REMOVE_TRAIT(owner, TRAIT_ANALGESIA, type) + owner.remove_movespeed_mod_immunities(src, /datum/movespeed_modifier/damage_slowdown) + owner.remove_movespeed_mod_immunities(src, /datum/movespeed_modifier/basic_stamina_slowdown) + return + +/atom/movable/screen/alert/status_effect/command_grit + name = "Grit" + desc = "You ignore pain for a duration, including the slowdowns from damage and stamina!" + icon = 'icons/hud/guardian.dmi' + icon_state = "standard" diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm new file mode 100644 index 00000000000000..1793501289f9f8 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm @@ -0,0 +1,31 @@ +/datum/power/warfighter/command_recover + name = "Commander" + desc = "There's many facets to a good leader, but being able to delegate and manage people under pressure is an art of it's own. \ + You gain the 'Command: Recover' ability. Using it on someone will cause them to recover from stuns faster (as if shook on help intent). Has a moderate cooldown. \ + For any and all command abilities in this category, the effect is increased if you are in the same department as the target, and even further if you are a head of staff (regardless of department). \ + Command abilities can never be used on yourself, and require the target to be able to see or hear you." + + value = 4 + required_powers + action_path = /datum/action/cooldown/power/warfighter/command/recover + +/datum/action/cooldown/power/warfighter/command/recover + name = "Command: Recover" + desc = "Command a target to recover, with an effect similar to shaking them with help intent several times." + + cooldown_time = 200 + button_icon = 'icons/hud/guardian.dmi' + button_icon_state = "dextrous" + action_symbol = "move" + +/datum/action/cooldown/power/warfighter/command/recover/use_action(mob/living/user, mob/living/carbon/target) + . = ..() + // Basically the same amounts as shaking up twice multiplied by commander modifiers. + target.AdjustStun(-6 SECONDS * (commander_modifier + 1)) + target.AdjustKnockdown(-6 SECONDS * (commander_modifier + 1)) + target.AdjustUnconscious(-6 SECONDS * (commander_modifier + 1)) + target.AdjustSleeping(-10 SECONDS * (commander_modifier + 1)) + target.AdjustParalyzed(-6 SECONDS * (commander_modifier + 1)) + target.AdjustImmobilized(-6 SECONDS * (commander_modifier + 1)) + target.shake_up_animation() // visual + return TRUE 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 a65756d91f876d..30f54a093b855f 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 @@ -4,8 +4,10 @@ overlay_icon_state = "bg_default_border" button_icon = 'icons/mob/actions/backgrounds.dmi' - // We generally don't dabble with cooldowns but a GCD of 0.5 seconds is kinda handy to prevent you from blowing your load on all your charges by accident. + // We generally don't dabble with cooldowns but a cooldown of 0.5 seconds is kinda handy to prevent you from blowing your load on all your charges by accident. cooldown_time = 5 + // hides the cooldown text cause we contest the ui element location. + text_cooldown = FALSE // Unlike normal spells, we have charges. More of that explained below at check_if_valid() var/charges = 0 // The cap on charges; you can't prepare more than these. If you leave this null, the spell will not interact with the charges system. diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 9ea97101a8e492..685994f24c4660 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -61,7 +61,7 @@ if(!do_use_time(user, target)) return FALSE if(use_action(user, target)) - on_action_success() + on_action_success(user, target) return TRUE return FALSE diff --git a/tgstation.dme b/tgstation.dme index 9b8f7f80f627ab..12ea96b962bdb3 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7440,6 +7440,11 @@ #include "modular_doppler\modular_powers\code\powers\mortal\expert\creature_tamer.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_command_action.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_action.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_power.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_grit.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_recover.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root.dm" From 7bb09ee261b12b8803243b581de0c1c112f71ab1 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 15 Feb 2026 14:30:16 +0100 Subject: [PATCH 060/212] Adds tackling, krav maga, adds some comments here and there. Adds support for unarmed damage to carbons (for cultivator) --- .../mob/living/carbon/carbon_defines.dm | 3 ++ .../mob/living/carbon/human/_species.dm | 1 + .../powers/mortal/warfighter/command_grit.dm | 3 ++ .../mortal/warfighter/command_recover.dm | 4 +++ .../mortal/warfighter/greater_tackler.dm | 31 +++++++++++++++++ .../powers/mortal/warfighter/krav_maga.dm | 20 +++++++++++ .../mortal/warfighter/martial_artist.dm | 20 +++++++++++ .../code/powers/mortal/warfighter/tackler.dm | 34 +++++++++++++++++++ tgstation.dme | 4 +++ 9 files changed, 120 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/greater_tackler.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/martial_artist.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm diff --git a/code/modules/mob/living/carbon/carbon_defines.dm b/code/modules/mob/living/carbon/carbon_defines.dm index 90896042dbea7d..d26aa086f02de2 100644 --- a/code/modules/mob/living/carbon/carbon_defines.dm +++ b/code/modules/mob/living/carbon/carbon_defines.dm @@ -125,6 +125,9 @@ /// A bitfield of "bodyshapes", updated by /obj/item/bodypart/proc/synchronize_bodyshapes() var/bodyshape = BODYSHAPE_HUMANOID + // DOPPLER ADDITION - Used by the Powers system to add unarmed damage without modifying the arms. + var/unarmed_damage_bonus = 0 + COOLDOWN_DECLARE(bleeding_message_cd) /// Obscured hide flags (hideflags that can't be seen AND can't be interacted with) diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm index 978e3369b63a99..ba6783153d9412 100644 --- a/code/modules/mob/living/carbon/human/_species.dm +++ b/code/modules/mob/living/carbon/human/_species.dm @@ -917,6 +917,7 @@ GLOBAL_LIST_EMPTY(features_by_species) var/grappled = (target.pulledby && target.pulledby.grab_state >= GRAB_AGGRESSIVE) var/damage = rand(attacking_bodypart.unarmed_damage_low, attacking_bodypart.unarmed_damage_high) + damage += user.unarmed_damage_bonus // DOPPLER ADDITION - Used by the Powers system to add unarmed damage without modifying the arms. var/limb_accuracy = attacking_bodypart.unarmed_effectiveness var/limb_sharpness = attacking_bodypart.unarmed_sharpness diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm index 08887b7a94ec23..dc1526b0ff5273 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm @@ -1,3 +1,6 @@ +/* + Gives pain negation as well as stam-damage immunity. +*/ /datum/power/warfighter/command_grit name = "Command: Grit" desc = "Whilst active, the target ignores pain for 15 seconds, as well as slowdown from damage and stamina loss. Has a long cooldown. Increased effect lenghtens duration." diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm index 1793501289f9f8..ae16056b2bb9fd 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm @@ -1,3 +1,7 @@ +/* + Gets someone up as if shook a couple of times. Also contains the lore dump on how commander powers work and their overarching mechanics. + Gateway power for all the commander stuff +*/ /datum/power/warfighter/command_recover name = "Commander" desc = "There's many facets to a good leader, but being able to delegate and manage people under pressure is an art of it's own. \ diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/greater_tackler.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/greater_tackler.dm new file mode 100644 index 00000000000000..9533885b8b9633 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/greater_tackler.dm @@ -0,0 +1,31 @@ +/* + +3 to skill mod, +2 to range, 0.5s to knockdown duration. +*/ +/datum/power/warfighter/tackler/greater_tackler + name = "Greater Tackler" + desc = "Your chances of landing a succesful tackle are greatly increased, as are your range and the duration you knockdown tackled foes." + value = 5 + + required_powers = list(/datum/power/warfighter/tackler) + + // bonuses to success chance + var/skill_mod_bonus = 3 + // bonuses to range + var/tackle_range_bonus = 2 + // bonuses to knockdown duration + var/knockdown_bonus = 0.5 SECONDS + +/datum/power/warfighter/tackler/greater_tackler/post_add() + . = ..() + var/datum/component/tackler/component = power_holder.GetComponent(/datum/component/tackler) + if(component) + component.skill_mod += skill_mod_bonus + component.range += tackle_range_bonus + component.base_knockdown += knockdown_bonus + +/datum/power/warfighter/tackler/greater_tackler/remove() + var/datum/component/tackler/component = power_holder.GetComponent(/datum/component/tackler) + if(component) + component.skill_mod -= skill_mod_bonus + component.range -= tackle_range_bonus + component.base_knockdown -= knockdown_bonus diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm new file mode 100644 index 00000000000000..dea52705c08d32 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm @@ -0,0 +1,20 @@ +/* + Lets you do KRAV MAGA. +*/ +/datum/power/warfighter/krav_maga + name = "Krav Maga" + desc = "Trained in various disarming moves, you can wield the martial arts of Krav Maga without any external assistance." + value = 6 + required_powers = list(/datum/power/warfighter/martial_artist) + /// Mindbound martial art component so the style follows mind transfers + var/datum/component/mindbound_martial_arts/krav_component + +/datum/power/warfighter/krav_maga/add() + if(!power_holder?.mind) + return + krav_component = power_holder.mind.AddComponent(/datum/component/mindbound_martial_arts, /datum/martial_art/krav_maga) + +/datum/power/warfighter/krav_maga/remove() + if(krav_component) + qdel(krav_component) + krav_component = null diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/martial_artist.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/martial_artist.dm new file mode 100644 index 00000000000000..ba919d817f0ff2 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/martial_artist.dm @@ -0,0 +1,20 @@ +/* + +5 to punch. Gateway to most of the martial arts stuff, just not a hard-root due to Mortal's design philosophy. +*/ +/datum/power/warfighter/martial_artist + name = "Martial Artist" + desc = "Trained in specialized combat maneuvers, you know where to best strike your opponents. Your punches deal extra damage." + value = 2 + +/datum/power/warfighter/martial_artist/add() + if(!iscarbon(power_holder)) + return + var/mob/living/carbon/power_guy = power_holder + power_guy.unarmed_damage_bonus += 5 + +/datum/power/warfighter/martial_artist/remove() + if(!iscarbon(power_holder)) + return + var/mob/living/carbon/power_guy = power_holder + power_guy.unarmed_damage_bonus -= 5 + diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm new file mode 100644 index 00000000000000..d162c1123b2b6f --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm @@ -0,0 +1,34 @@ +/* + Grants the tackle subsystem and makes you better at tackling. + Stats wise this is about the same strenght as the tackler gloves just with a +1 to the skill_mod +*/ +/datum/power/warfighter/tackler + name = "Tackler" + desc = "You know how to throw a well-trained tackle. Allows you to perform tackles without assistive items and allows you to perform them better." + value = 4 + + required_powers = list(/datum/power/warfighter/martial_artist) + + // the datum that the tackle system is in + var/datum/component/tackler + +/datum/power/warfighter/tackler/add() + // Taking these over from tackle gloves just for clarity. They're in here becuase I don't want to clog the upgrade vars with these. + /// See: [/datum/component/tackler/var/stamina_cost] + var/tackle_stam_cost = 25 + /// See: [/datum/component/tackler/var/base_knockdown] + var/base_knockdown = 1 SECONDS + /// See: [/datum/component/tackler/var/range] + var/tackle_range = 4 + /// See: [/datum/component/tackler/var/min_distance] + var/min_distance = 0 + /// See: [/datum/component/tackler/var/speed] + var/tackle_speed = 1 + /// See: [/datum/component/tackler/var/skill_mod] + var/skill_mod = 2 + + tackler = power_holder.AddComponent(/datum/component/tackler, stamina_cost=tackle_stam_cost, base_knockdown = base_knockdown, range = tackle_range, speed = tackle_speed, skill_mod = skill_mod, min_distance = min_distance) + +/datum/power/warfighter/tackler/remove() + power_holder.RemoveComponentSource(src, /datum/component/tackler) + diff --git a/tgstation.dme b/tgstation.dme index 12ea96b962bdb3..ec880f43f3fc8e 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7445,6 +7445,10 @@ #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_grit.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_recover.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\greater_tackler.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\krav_maga.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\martial_artist.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\tackler.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root.dm" From 15bda41f7a778e63e4af950560284ba2639f1151 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 15 Feb 2026 16:00:52 +0100 Subject: [PATCH 061/212] Adds quick draw, letting you acclimate to and auto-grab items of it's type. --- .../powers/mortal/warfighter/quick_draw.dm | 227 ++++++++++++++++++ tgstation.dme | 1 + 2 files changed, 228 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm new file mode 100644 index 00000000000000..88c81d96293c93 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm @@ -0,0 +1,227 @@ +/* + Allows you to bind with a specific item and draw any of its type on demand. Keyword type; so if you like consumable items a la flashbangs & bolas, you'll love this one. +*/ +/datum/power/warfighter/quick_draw + name = "Equipment Specialist" + desc = "Some folks have studied warfare in their own specialized way for years, putting them on an equal ground with many others. This category includes things such as swords, shields and more. \ + The power itself grants you the 'Quick Draw' ability, letting you 'acclimate' with an item of your choice. \ + Whilst acclimated, you can use the power to instantly draw that type of item to your hand, as long as it is anywhere on your person, or within melee range of you. \ + You can even use this to snag it back from your enemies." + value = 3 + action_path = /datum/action/cooldown/power/warfighter/quick_draw + +/datum/action/cooldown/power/warfighter/quick_draw + name = "Quick Draw" + desc = "Acclimate to a held item type, then draw that item from sensible storage or nearby hands automatically into your active hand." + button_icon = 'icons/mob/actions/actions_slime.dmi' // placeholders out of the wazoo + button_icon_state = "slimeeject" + + cooldown_time = 20 + /// Cached overlay so we can cleanly update it. + var/mutable_appearance/bonded_overlay + /// Type path of the bonded item. + var/bonded_type + /// Display name for user feedback. + var/bonded_name + /// Icon file for bonded item overlay. + var/bonded_icon + /// Icon state for bonded item overlay. + var/bonded_icon_state + +/datum/action/cooldown/power/warfighter/quick_draw/use_action(mob/living/user, atom/target) + var/obj/item/held_item = user.get_active_held_item() + + // Bind if we don't have a bonded type yet. + if(!bonded_type) + if(!held_item) + user.balloon_alert(user, "hold an item to acclimate") + return FALSE + if(!can_bond_item(held_item, user)) + user.balloon_alert(user, "can't acclimate to that") + return FALSE + bonded_type = held_item.type + bonded_name = held_item.name + bonded_icon = held_item.icon + bonded_icon_state = held_item.icon_state + user.balloon_alert(user, "acclimated to [held_item]") + build_all_button_icons(UPDATE_BUTTON_OVERLAY) + return TRUE + + // Rebind if holding a different item type. + if(held_item && !istype(held_item, bonded_type)) + if(!can_bond_item(held_item, user)) + user.balloon_alert(user, "can't acclimate to that") + return FALSE + bonded_type = held_item.type + bonded_name = held_item.name + bonded_icon = held_item.icon + bonded_icon_state = held_item.icon_state + user.balloon_alert(user, "reacclimated to [held_item]") + build_all_button_icons(UPDATE_BUTTON_OVERLAY) + return TRUE + + if(user.get_active_held_item() && user.get_inactive_held_item()) + user.balloon_alert(user, "hands full") + return FALSE + + var/obj/item/target_item = find_drawable_item(user) + if(!target_item) + var/label_name = bonded_name ? bonded_name : "item" + user.balloon_alert(user, "no [label_name]") + return FALSE + + if(!draw_item_to_hand(user, target_item)) + user.balloon_alert(user, "can't draw it") + return FALSE + + user.visible_message( + span_notice("[user] draws [target_item]!"), + span_notice("You draw [target_item]."), + ) + return TRUE + +// Adds an overlay to the power button so that the user knows what their bonded item is. +/datum/action/cooldown/power/warfighter/quick_draw/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force = FALSE) + ..() + + if(!bonded_icon || !bonded_icon_state) + if(bonded_overlay) + current_button.cut_overlay(bonded_overlay) + bonded_overlay = null + return + + if(bonded_overlay) + current_button.cut_overlay(bonded_overlay) + + bonded_overlay = mutable_appearance(icon = bonded_icon, icon_state = bonded_icon_state) + current_button.add_overlay(bonded_overlay) + +/// Checks if an item can be bonded to. +/datum/action/cooldown/power/warfighter/quick_draw/proc/can_bond_item(obj/item/held_item, mob/living/user) + if(!held_item || !user) + return FALSE + if(held_item.item_flags & ABSTRACT) + return FALSE + return TRUE + +/// Checks if an item is eligible to be quick-drawn from the user's gear. +/datum/action/cooldown/power/warfighter/quick_draw/proc/can_quickdraw_item(obj/item/candidate_item, mob/living/user, list/equipped_items) + if(!candidate_item || !user) + return FALSE + if(candidate_item.item_flags & ABSTRACT) + return FALSE + // Normally you can't draw equipped items unless they're in a container (anti-cheese), but it works if its either in the pockets, the belts, the suit slot or the id. + var/allow_equipped_slot = FALSE + if(ishuman(user)) + var/mob/living/carbon/human/human_user = user + var/slot_id = human_user.get_slot_by_item(candidate_item) + if(slot_id == ITEM_SLOT_LPOCKET || slot_id == ITEM_SLOT_RPOCKET || slot_id == ITEM_SLOT_BELT || slot_id == ITEM_SLOT_SUITSTORE || slot_id == ITEM_SLOT_ID) + allow_equipped_slot = TRUE + + if(candidate_item in equipped_items) + if(!allow_equipped_slot) + return FALSE + if(candidate_item.loc == user && !allow_equipped_slot) + return FALSE + + // Reject implants/organs and abstract containers in the loc chain. + var/atom/current_container = candidate_item.loc + while(current_container && !ismob(current_container)) + if(istype(current_container, /obj/item/implant) || istype(current_container, /obj/item/organ)) + return FALSE + if(isobj(current_container)) + var/obj/item/container_item = current_container + if(container_item.item_flags & ABSTRACT) + return FALSE + if(container_item.atom_storage?.locked) + return FALSE + current_container = current_container.loc + + // Must be inside a storage container to count as "on your person". + if(!allow_equipped_slot && !isobj(candidate_item.loc)) + return FALSE + var/obj/item/candidate_container = candidate_item.loc + if(!allow_equipped_slot && !candidate_container.atom_storage) + return FALSE + + return TRUE + +/// Finds a suitable bonded item to draw. +/datum/action/cooldown/power/warfighter/quick_draw/proc/find_drawable_item(mob/living/user) + if(!bonded_type || !user) + return null + + var/list/equipped_items = user.get_equipped_items(INCLUDE_POCKETS | INCLUDE_HELD | INCLUDE_ACCESSORIES) + var/list/gear_items = user.get_all_gear(accessories = TRUE, recursive = TRUE) + + for(var/obj/item/candidate_item in gear_items) + if(!istype(candidate_item, bonded_type)) + continue + if(!can_quickdraw_item(candidate_item, user, equipped_items)) + continue + return candidate_item + + // Check adjacent ground items (melee range). + for(var/obj/item/ground_item in view(1, user)) + if(!istype(ground_item, bonded_type)) + continue + if(ground_item.item_flags & ABSTRACT) + continue + if(!isturf(ground_item.loc)) + continue + return ground_item + + // Check adjacent enemies' hands only. + for(var/mob/living/nearby_mob in view(1, user)) + if(nearby_mob == user) + continue + var/obj/item/active_item = nearby_mob.get_active_held_item() + if(istype(active_item, bonded_type) && can_take_from_other(nearby_mob, active_item)) + return active_item + var/obj/item/inactive_item = nearby_mob.get_inactive_held_item() + if(istype(inactive_item, bonded_type) && can_take_from_other(nearby_mob, inactive_item)) + return inactive_item + + return null + +/// Checks if we can take an item from another mob's hand. +/datum/action/cooldown/power/warfighter/quick_draw/proc/can_take_from_other(mob/living/other_mob, obj/item/held_item) + if(!other_mob || !held_item) + return FALSE + if(held_item.item_flags & ABSTRACT) + return FALSE + if(!other_mob.canUnEquip(held_item, FALSE)) + return FALSE + return TRUE + +/// Moves the target item into the user's hands, if possible. +/datum/action/cooldown/power/warfighter/quick_draw/proc/draw_item_to_hand(mob/living/user, obj/item/target_item) + if(!user || !target_item) + return FALSE + + // stole from a mob + if(ismob(target_item.loc)) + var/mob/living/holder_mob = target_item.loc + if(!holder_mob.canUnEquip(target_item, FALSE)) + return FALSE + if(!holder_mob.transferItemToLoc(target_item, user, force = FALSE)) + return FALSE + user.balloon_alert(user, "snagged") + holder_mob.balloon_alert(holder_mob, "snagged") + return user.put_in_hands(target_item) + + // took it from our person + if(isobj(target_item.loc)) + var/obj/item/storage_container = target_item.loc + if(storage_container.atom_storage) + if(storage_container.atom_storage.locked) + return FALSE + if(!storage_container.atom_storage.remove_single(user, target_item, user)) + return FALSE + return user.put_in_hands(target_item) + + // took it from the ground + if(isturf(target_item.loc)) + return user.put_in_hands(target_item) + + return FALSE diff --git a/tgstation.dme b/tgstation.dme index ec880f43f3fc8e..e4a86d8aa4c01d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7448,6 +7448,7 @@ #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\greater_tackler.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\krav_maga.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\martial_artist.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\quick_draw.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\tackler.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_power.dm" From 97dd1c3a57a1086d3f64bf32929e62e2d493de30 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 15 Feb 2026 16:11:35 +0100 Subject: [PATCH 062/212] Undoes earlier martial arts addition, adds a general hook for on unarmed hit instead that it uses. --- code/__DEFINES/~doppler_defines/powers.dm | 4 ++++ .../mob/living/carbon/carbon_defines.dm | 3 --- .../mob/living/carbon/human/_species.dm | 4 +++- .../mortal/warfighter/martial_artist.dm | 19 ++++++++++++------- 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index ce2fecbbd3e590..c2efc9ec2d6622 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -43,6 +43,10 @@ // Listener for dispelling #define COMSIG_ATOM_DISPEL "atom_dispel" +/// Fired after a successful unarmed hit (i.e. not missed/blocked), right before damage is applied. +/// Args: (mob/living/carbon/attacker, mob/living/carbon/target, obj/item/bodypart/affecting, damage, armor_block, limb_accuracy, limb_sharpness) +#define COMSIG_HUMAN_UNARMED_HIT "living_unarmed_hit" + // Bitflag return value(s) from handlers: #define DISPEL_RESULT_DISPELLED (1<<0) diff --git a/code/modules/mob/living/carbon/carbon_defines.dm b/code/modules/mob/living/carbon/carbon_defines.dm index d26aa086f02de2..90896042dbea7d 100644 --- a/code/modules/mob/living/carbon/carbon_defines.dm +++ b/code/modules/mob/living/carbon/carbon_defines.dm @@ -125,9 +125,6 @@ /// A bitfield of "bodyshapes", updated by /obj/item/bodypart/proc/synchronize_bodyshapes() var/bodyshape = BODYSHAPE_HUMANOID - // DOPPLER ADDITION - Used by the Powers system to add unarmed damage without modifying the arms. - var/unarmed_damage_bonus = 0 - COOLDOWN_DECLARE(bleeding_message_cd) /// Obscured hide flags (hideflags that can't be seen AND can't be interacted with) diff --git a/code/modules/mob/living/carbon/human/_species.dm b/code/modules/mob/living/carbon/human/_species.dm index ba6783153d9412..a03fb6877c180d 100644 --- a/code/modules/mob/living/carbon/human/_species.dm +++ b/code/modules/mob/living/carbon/human/_species.dm @@ -917,7 +917,6 @@ GLOBAL_LIST_EMPTY(features_by_species) var/grappled = (target.pulledby && target.pulledby.grab_state >= GRAB_AGGRESSIVE) var/damage = rand(attacking_bodypart.unarmed_damage_low, attacking_bodypart.unarmed_damage_high) - damage += user.unarmed_damage_bonus // DOPPLER ADDITION - Used by the Powers system to add unarmed damage without modifying the arms. var/limb_accuracy = attacking_bodypart.unarmed_effectiveness var/limb_sharpness = attacking_bodypart.unarmed_sharpness @@ -1009,6 +1008,9 @@ GLOBAL_LIST_EMPTY(features_by_species) var/attack_type = attacking_bodypart.attack_type var/kicking = (atk_effect == ATTACK_EFFECT_KICK) var/final_armor_block = armor_block + + SEND_SIGNAL(user, COMSIG_HUMAN_UNARMED_HIT, target, affecting, damage, armor_block, limb_accuracy, limb_sharpness) // DOPPLER ADDITION - Adds a signaler for the power system so that we can track if we land punches. + if(kicking || grappled) //kicks and punches when grappling bypass armor slightly. if(damage >= 9) target.force_say() 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 ba919d817f0ff2..6196f3ba9be123 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 @@ -6,15 +6,20 @@ desc = "Trained in specialized combat maneuvers, you know where to best strike your opponents. Your punches deal extra damage." value = 2 + // how much EEEEXTRA DEEEAAMEEEEG we do with our punches. + var/bonus_damage = 5 + /datum/power/warfighter/martial_artist/add() - if(!iscarbon(power_holder)) - return - var/mob/living/carbon/power_guy = power_holder - power_guy.unarmed_damage_bonus += 5 + RegisterSignal(power_holder, COMSIG_HUMAN_UNARMED_HIT, PROC_REF(on_unarmed_hit)) /datum/power/warfighter/martial_artist/remove() - if(!iscarbon(power_holder)) + UnregisterSignal(power_holder, COMSIG_HUMAN_UNARMED_HIT) + +// Sends a signal to the new signaler for unarmed punches. +// Will probably be used a lot more with cultivator. +/datum/power/warfighter/martial_artist/proc/on_unarmed_hit(mob/living/user, mob/living/target, obj/item/bodypart/affecting, damage, armor_block, limb_accuracy, limb_sharpness) + SIGNAL_HANDLER + if(!target || bonus_damage <= 0) return - var/mob/living/carbon/power_guy = power_holder - power_guy.unarmed_damage_bonus -= 5 + target.apply_damage(bonus_damage, BRUTE, affecting, armor_block, sharpness = limb_sharpness) From c86c986034d4a9f4614686533753769aa3411816 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 15 Feb 2026 18:32:09 +0100 Subject: [PATCH 063/212] Tweaks some powers flags stuff I forgot to do. --- code/__DEFINES/~doppler_defines/powers.dm | 3 +++ .../code/powers/mortal/augmented/_augmented_power.dm | 1 + .../code/powers/mortal/warfighter/martial_artist.dm | 1 + .../modular_powers/code/powers/mortal/warfighter/quick_draw.dm | 2 +- .../modular_powers/code/powers/resonant/psyker/_psyker_root.dm | 1 + tgstation.dme | 2 ++ 6 files changed, 9 insertions(+), 1 deletion(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index c2efc9ec2d6622..d90530040d64ce 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -199,6 +199,9 @@ // The global GCD for Warfigher powers #define WARFIGHTER_COMMANDER_SHARED_COOLDOWN 2 SECONDS +// Trait for the Explosives Specialist power +#define TRAIT_POWER_EXPLOSIVES_SPECIALIST "power_explosives_specialist" + /** * MORTAL: Augmented * All defines related to the augmented powers. diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm index c3d4efee72382a..097f1be33af889 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm @@ -2,6 +2,7 @@ name = "Augmented Power" desc = "I never asked for this (abstract type to appear. You shouldn't be seeing this.)" + power_flags = POWER_HUMAN_ONLY archetype = POWER_ARCHETYPE_MORTAL path = POWER_PATH_AUGMENTED priority = POWER_PRIORITY_BASIC 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 6196f3ba9be123..29b5ee0a83980e 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 @@ -6,6 +6,7 @@ desc = "Trained in specialized combat maneuvers, you know where to best strike your opponents. Your punches deal extra damage." value = 2 + power_flags = POWER_HUMAN_ONLY // how much EEEEXTRA DEEEAAMEEEEG we do with our punches. var/bonus_damage = 5 diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm index 88c81d96293c93..4bce9eac31392b 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm @@ -16,7 +16,7 @@ button_icon = 'icons/mob/actions/actions_slime.dmi' // placeholders out of the wazoo button_icon_state = "slimeeject" - cooldown_time = 20 + cooldown_time = 5 /// Cached overlay so we can cleanly update it. var/mutable_appearance/bonded_overlay /// Type path of the bonded item. 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 47d9f0b56b2acc..eb611d59c61c29 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 @@ -6,6 +6,7 @@ The catalyst for psychic abilities; but beware overexerting it." value = 3 + power_flags = POWER_HUMAN_ONLY mob_trait = TRAIT_ARCHETYPE_RESONANT archetype = POWER_ARCHETYPE_RESONANT path = POWER_PATH_PSYKER diff --git a/tgstation.dme b/tgstation.dme index e4a86d8aa4c01d..cb455ec7b5a4ca 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7445,11 +7445,13 @@ #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_grit.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_recover.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\explosives_specialist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\greater_tackler.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\krav_maga.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\martial_artist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\quick_draw.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\tackler.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\components\explosives_specialist_countdown.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root.dm" From 76227efbfd4bd1067f7b29421b5d074f4582364b Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 15 Feb 2026 18:33:17 +0100 Subject: [PATCH 064/212] fixed some stray includes in the dme --- tgstation.dme | 2 -- 1 file changed, 2 deletions(-) diff --git a/tgstation.dme b/tgstation.dme index cb455ec7b5a4ca..e4a86d8aa4c01d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7445,13 +7445,11 @@ #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_grit.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_recover.dm" -#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\explosives_specialist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\greater_tackler.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\krav_maga.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\martial_artist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\quick_draw.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\tackler.dm" -#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\components\explosives_specialist_countdown.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root.dm" From 050b617c480fddc423e871d27720cac4b772f68a Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 16 Feb 2026 09:02:35 +0100 Subject: [PATCH 065/212] Few tweaks. Adds explosives specialist (doesnt work with c4 yet). --- code/game/objects/items/grenades/_grenade.dm | 2 + .../mortal/warfighter/command_recover.dm | 1 - .../components/grenade_components.dm | 291 ++++++++++++++++++ .../warfighter/explosives_specialist.dm | 9 + .../powers/mortal/warfighter/quick_draw.dm | 7 +- tgstation.dme | 2 + 6 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/explosives_specialist.dm diff --git a/code/game/objects/items/grenades/_grenade.dm b/code/game/objects/items/grenades/_grenade.dm index 262a493cc75366..4f8f1cd153e708 100644 --- a/code/game/objects/items/grenades/_grenade.dm +++ b/code/game/objects/items/grenades/_grenade.dm @@ -62,6 +62,8 @@ /obj/item/grenade/Initialize(mapload) . = ..() ADD_TRAIT(src, TRAIT_ODD_CUSTOMIZABLE_FOOD_INGREDIENT, type) + AddComponent(/datum/component/grenade_timer_hud) // DOPPLER ADDITION - Display timers for Explosives Specialists (and ghosts) + AddComponent(/datum/component/grenade_timer_registry) // DOPPLER ADDITION - Register grenades for global specialist view RegisterSignal(src, COMSIG_ITEM_USED_AS_INGREDIENT, PROC_REF(on_used_as_ingredient)) /obj/item/grenade/suicide_act(mob/living/carbon/user) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm index ae16056b2bb9fd..27f3f963210a32 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm @@ -10,7 +10,6 @@ Command abilities can never be used on yourself, and require the target to be able to see or hear you." value = 4 - required_powers action_path = /datum/action/cooldown/power/warfighter/command/recover /datum/action/cooldown/power/warfighter/command/recover diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm new file mode 100644 index 00000000000000..6d59f07c511f79 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm @@ -0,0 +1,291 @@ +/** + * Shows a live detonation countdown on the grenade's hand HUD icon. + * Visible to explosives specialists and to observers watching the holder. + */ +/datum/component/grenade_timer_hud + var/obj/item/grenade/parent_grenade + var/mob/holder + var/atom/movable/screen/timer_hud + var/explodes_at = 0 + var/timer_id + var/list/current_viewers = list() + +/datum/component/grenade_timer_hud/Initialize() + if(!istype(parent, /obj/item/grenade)) + return COMPONENT_INCOMPATIBLE + parent_grenade = parent + +/datum/component/grenade_timer_hud/RegisterWithParent() + RegisterSignal(parent, COMSIG_GRENADE_ARMED, PROC_REF(on_armed)) + RegisterSignal(parent, COMSIG_GRENADE_DETONATE, PROC_REF(on_detonate)) + RegisterSignal(parent, COMSIG_ITEM_PICKUP, PROC_REF(on_pickup)) + RegisterSignal(parent, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equipped)) + RegisterSignal(parent, COMSIG_ITEM_DROPPED, PROC_REF(on_drop)) + +/datum/component/grenade_timer_hud/UnregisterFromParent() + UnregisterSignal(parent, list( + COMSIG_GRENADE_ARMED, + COMSIG_GRENADE_DETONATE, + COMSIG_ITEM_PICKUP, + COMSIG_ITEM_EQUIPPED, + COMSIG_ITEM_DROPPED, + )) + stop_timer() + remove_hud() + +/datum/component/grenade_timer_hud/proc/on_armed(datum/source, det_time, delayoverride) + SIGNAL_HANDLER + var/delay = isnull(delayoverride) ? det_time : delayoverride + explodes_at = world.time + delay + if(!holder && ismob(parent_grenade.loc)) + holder = parent_grenade.loc + update_viewers() + start_timer() + +/datum/component/grenade_timer_hud/proc/on_detonate(datum/source, lanced_by) + SIGNAL_HANDLER + stop_timer() + remove_hud() + +/datum/component/grenade_timer_hud/proc/on_pickup(datum/source, mob/living/user) + SIGNAL_HANDLER + holder = user + update_viewers() + +/datum/component/grenade_timer_hud/proc/on_equipped(datum/source, mob/living/user, slot) + SIGNAL_HANDLER + holder = user + update_viewers() + +/datum/component/grenade_timer_hud/proc/on_drop(datum/source, mob/living/user) + SIGNAL_HANDLER + if(holder == user) + holder = null + remove_hud() + +/datum/component/grenade_timer_hud/proc/start_timer() + if(timer_id) + return + timer_id = addtimer(CALLBACK(src, PROC_REF(tick)), 1 DECISECONDS, TIMER_LOOP | TIMER_STOPPABLE) + +/datum/component/grenade_timer_hud/proc/stop_timer() + if(timer_id) + deltimer(timer_id) + timer_id = null + +/datum/component/grenade_timer_hud/proc/tick() + if(!parent_grenade?.active) + stop_timer() + remove_hud() + return + + update_viewers() + if(!timer_hud) + return + + var/remaining = max(explodes_at - world.time, 0) + var/remaining_seconds = max(CEILING(remaining / 10, 1), 0) + timer_hud.maptext = "[remaining_seconds]" + timer_hud.screen_loc = parent_grenade.screen_loc + +/datum/component/grenade_timer_hud/proc/get_viewers() + var/list/viewers = list() + if(holder?.client && HAS_TRAIT(holder, TRAIT_POWER_EXPLOSIVES_SPECIALIST)) + viewers += holder + if(holder?.observers?.len) + for(var/mob/dead/observer/O in holder.observers) + if(O?.client && O.client.eye == holder) + viewers += O + return viewers + +/datum/component/grenade_timer_hud/proc/update_viewers() + if(!holder || !parent_grenade.active || parent_grenade.loc != holder) + remove_hud() + return + + var/list/new_viewers = get_viewers() + if(!new_viewers.len) + remove_hud() + return + + show_hud() + + for(var/mob/M in current_viewers) + if(!(M in new_viewers)) + M.client?.screen -= timer_hud + + for(var/mob/M in new_viewers) + if(!(M in current_viewers)) + M.client?.screen += timer_hud + + current_viewers = new_viewers + +/datum/component/grenade_timer_hud/proc/show_hud() + if(timer_hud) + return + timer_hud = new /atom/movable/screen + timer_hud.layer = ABOVE_HUD_PLANE + timer_hud.plane = HUD_PLANE + timer_hud.maptext_width = 32 + timer_hud.maptext_height = 16 + timer_hud.maptext = "?" + +/datum/component/grenade_timer_hud/proc/remove_hud() + if(timer_hud) + for(var/mob/M in current_viewers) + M?.client?.screen -= timer_hud + current_viewers = list() + QDEL_NULL(timer_hud) + + +/** + * The part 2 that's respnsible for on the ground timers. + * Because showing text overlays to select characters isn't easy, and ghosts get the easy pass with invisibility flags. + */ + +// Global countdowns for specialists/observers looking at any armed grenade. +GLOBAL_DATUM_INIT(grenade_timer_manager, /datum/grenade_timer_manager, new) + +/datum/grenade_timer_manager + var/list/armed_grenades = list() // /obj/item/grenade -> explode_at (world.time) + var/list/viewer_images = list() // mob -> (grenade -> image) + var/timer_id + +/datum/grenade_timer_manager/proc/register_grenade(obj/item/grenade/G, det_time, delayoverride) + if(QDELETED(G)) + return + if(armed_grenades[G]) + return + var/delay = isnull(delayoverride) ? det_time : delayoverride + armed_grenades[G] = world.time + delay + RegisterSignal(G, COMSIG_GRENADE_DETONATE, PROC_REF(on_grenade_detonate)) + RegisterSignal(G, COMSIG_QDELETING, PROC_REF(on_grenade_deleted)) + ensure_timer() + +/datum/grenade_timer_manager/proc/unregister_grenade(obj/item/grenade/G) + if(!armed_grenades[G]) + return + armed_grenades -= G + UnregisterSignal(G, list(COMSIG_GRENADE_DETONATE, COMSIG_QDELETING)) + remove_grenade_images(G) + if(!armed_grenades.len) + stop_timer() + +/datum/grenade_timer_manager/proc/on_grenade_detonate(datum/source, lanced_by) + SIGNAL_HANDLER + unregister_grenade(source) + +/datum/grenade_timer_manager/proc/on_grenade_deleted(datum/source) + SIGNAL_HANDLER + unregister_grenade(source) + +/datum/grenade_timer_manager/proc/ensure_timer() + if(timer_id) + return + timer_id = addtimer(CALLBACK(src, PROC_REF(tick)), 1 DECISECONDS, TIMER_LOOP | TIMER_STOPPABLE) + +/datum/grenade_timer_manager/proc/stop_timer() + if(timer_id) + deltimer(timer_id) + timer_id = null + +/datum/grenade_timer_manager/proc/tick() + if(!armed_grenades.len) + stop_timer() + return + + var/list/eligible_viewers = get_eligible_viewers() + + // Remove viewers who are no longer eligible + for(var/mob/M in viewer_images) + if(!(M in eligible_viewers)) + remove_all_images_from(M) + viewer_images -= M + + // Update grenade images per eligible viewer + for(var/obj/item/grenade/G as anything in armed_grenades) + if(QDELETED(G) || !G.active) + unregister_grenade(G) + continue + + var/remaining = max(armed_grenades[G] - world.time, 0) + var/remaining_seconds = max(CEILING(remaining / 10, 1), 0) + + for(var/mob/M in eligible_viewers) + if(can_view_grenade(M, G)) + update_image(M, G, remaining_seconds) + else + remove_image(M, G) + +/datum/grenade_timer_manager/proc/get_eligible_viewers() + var/list/viewers = list() + for(var/mob/M in GLOB.player_list) + if(!M?.client) + continue + if(HAS_TRAIT(M, TRAIT_POWER_EXPLOSIVES_SPECIALIST) || isobserver(M)) + viewers += M + return viewers + +/datum/grenade_timer_manager/proc/can_view_grenade(mob/M, obj/item/grenade/G) + var/atom/eye = M.client?.eye || M + if(!eye || eye.z != G.z) + return FALSE + var/list/view_range = getviewsize(M.client?.view) + if(!view_range || view_range.len < 2) + return FALSE + var/range = max(view_range[1], view_range[2]) + return get_dist(eye, G) <= range + +/datum/grenade_timer_manager/proc/update_image(mob/M, obj/item/grenade/G, remaining_seconds) + if(!viewer_images[M]) + viewer_images[M] = list() + + var/image/I = viewer_images[M][G] + if(!I) + I = image('icons/blanks/32x32.dmi', loc = G, icon_state = "nothing") + I.plane = ABOVE_LIGHTING_PLANE + I.layer = FLOAT_LAYER + I.dir = SOUTH + I.maptext_width = 32 + I.maptext_height = 16 + I.appearance_flags |= RESET_TRANSFORM|RESET_COLOR|KEEP_APART + viewer_images[M][G] = I + M.client.images += I + + I.maptext = "[remaining_seconds]" + +/datum/grenade_timer_manager/proc/remove_image(mob/M, obj/item/grenade/G) + var/list/images = viewer_images[M] + if(!images) + return + var/image/I = images[G] + if(I) + M.client?.images -= I + images -= G + +/datum/grenade_timer_manager/proc/remove_all_images_from(mob/M) + var/list/images = viewer_images[M] + if(!images) + return + for(var/image/I in images) + M.client?.images -= I + images.Cut() + +/datum/grenade_timer_manager/proc/remove_grenade_images(obj/item/grenade/G) + for(var/mob/M in viewer_images) + remove_image(M, G) + + +/** + * Registers armed grenades with the global timer manager. + */ +/datum/component/grenade_timer_registry +/datum/component/grenade_timer_registry/RegisterWithParent() + RegisterSignal(parent, COMSIG_GRENADE_ARMED, PROC_REF(on_armed)) + +/datum/component/grenade_timer_registry/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_GRENADE_ARMED) + +/datum/component/grenade_timer_registry/proc/on_armed(datum/source, det_time, delayoverride) + SIGNAL_HANDLER + GLOB.grenade_timer_manager.register_grenade(source, det_time, delayoverride) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/explosives_specialist.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/explosives_specialist.dm new file mode 100644 index 00000000000000..7795bf73fa6008 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/explosives_specialist.dm @@ -0,0 +1,9 @@ +/datum/power/warfighter/explosives_specialist + name = "Explosives Specialist" + desc = "Bombs and grenades are your forte. You can see the countdown on grenades (and bombs, but practically all bombs already come with a display for DRAMATIC FLAIR)." + value = 4 + required_powers = list(/datum/power/warfighter/quick_draw) + mob_trait = TRAIT_POWER_EXPLOSIVES_SPECIALIST + +// See modular_doppler\modular_powers\code\powers\mortal\warfighter\components\grenade_components.dm for how we add the timers +// TODO: Make it work with c4 as well. diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm index 4bce9eac31392b..7cf6c0bf48ccf0 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm @@ -199,15 +199,16 @@ if(!user || !target_item) return FALSE - // stole from a mob + // taken from a mob if(ismob(target_item.loc)) var/mob/living/holder_mob = target_item.loc if(!holder_mob.canUnEquip(target_item, FALSE)) return FALSE if(!holder_mob.transferItemToLoc(target_item, user, force = FALSE)) return FALSE - user.balloon_alert(user, "snagged") - holder_mob.balloon_alert(holder_mob, "snagged") + if(!holder_mob == user) // tell the person we're stealing from that we stole from them. + user.balloon_alert(user, "snagged") + holder_mob.balloon_alert(holder_mob, "[target_item.name] was snagged!") return user.put_in_hands(target_item) // took it from our person diff --git a/tgstation.dme b/tgstation.dme index e4a86d8aa4c01d..de3d903904f116 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7445,11 +7445,13 @@ #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_grit.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_recover.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\explosives_specialist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\greater_tackler.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\krav_maga.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\martial_artist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\quick_draw.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\tackler.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\components\grenade_components.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root.dm" From f1ae9558640ed4d5d979984a6fb2002d7264067c Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 17 Feb 2026 07:20:25 +0100 Subject: [PATCH 066/212] Adds dual wielding. Tweaks costs on quick draw and krav maga. --- .../powers/mortal/warfighter/dual_wielder.dm | 108 ++++++++++++++++++ .../powers/mortal/warfighter/krav_maga.dm | 2 +- .../powers/mortal/warfighter/quick_draw.dm | 3 +- tgstation.dme | 1 + 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm new file mode 100644 index 00000000000000..e981c586b3c288 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm @@ -0,0 +1,108 @@ +/* + Allows toggling a dual-wield stance. + When active, a melee attack with one weapon immediately follows with an off-hand strike. + Both strikes have an independent flat miss chance. +*/ + +#define DUAL_WIELD_OFFHAND "dual_wield_offhand" +#define DUAL_WIELD_ATTACK_ITEM "dual_wield_attack_item" +#define DUAL_WIELD_HAS_FORCED_MISS "dual_wield_has_forced_miss" +#define DUAL_WIELD_FORCED_MISS "dual_wield_forced_miss" + +/datum/power/warfighter/dual_wielder + name = "Dual Wielder" + desc = "You can toggle a dual-wield stance. While active, striking with a melee weapon immediately follows with an off-hand strike. Both strikes have a 30% chance to miss." + value = 5 + + required_powers = list(/datum/power/warfighter/quick_draw) + action_path = /datum/action/cooldown/power/warfighter/dual_wielder + +/datum/action/cooldown/power/warfighter/dual_wielder + name = "Dual Wield" + desc = "Toggle dual-wielding. While active, melee attacks immediately follow with an off-hand strike (each strike has a 30% miss chance)." + button_icon = 'icons/mob/actions/actions_cult.dmi' + button_icon_state = "equip" + + // Chance that we miss a swing + var/dual_wield_miss_chance = 30 + +/datum/action/cooldown/power/warfighter/dual_wielder/use_action(mob/living/user, atom/target) + active = !active + user.balloon_alert(user, active ? "dual wield on" : "dual wield off") + build_all_button_icons(UPDATE_BUTTON_STATUS) + return TRUE + +/datum/action/cooldown/power/warfighter/dual_wielder/Grant(mob/granted_to) + . = ..() + RegisterSignal(granted_to, COMSIG_MOB_ITEM_ATTACK, PROC_REF(on_melee_attack)) + +/datum/action/cooldown/power/warfighter/dual_wielder/Remove(mob/removed_from) + . = ..() + UnregisterSignal(removed_from, COMSIG_MOB_ITEM_ATTACK) + +/datum/action/cooldown/power/warfighter/dual_wielder/proc/on_melee_attack(mob/living/source, atom/target, mob/living/user, list/modifiers, list/attack_modifiers) + SIGNAL_HANDLER + + if(source != owner) + return + if(!active) + return + + var/is_offhand = LAZYACCESS(attack_modifiers, DUAL_WIELD_OFFHAND) + var/obj/item/attacking_item = LAZYACCESS(attack_modifiers, DUAL_WIELD_ATTACK_ITEM) || source.get_active_held_item() + if(!is_valid_melee_item(attacking_item)) + return + + var/forced_miss = FALSE + var/has_forced_miss = LAZYACCESS(attack_modifiers, DUAL_WIELD_HAS_FORCED_MISS) + if(has_forced_miss) + forced_miss = LAZYACCESS(attack_modifiers, DUAL_WIELD_FORCED_MISS) + var/main_miss = has_forced_miss ? forced_miss : prob(dual_wield_miss_chance) + + var/offhand_attempted = FALSE + var/offhand_miss = FALSE + if(!is_offhand) + offhand_miss = prob(dual_wield_miss_chance) + offhand_attempted = try_offhand_attack(source, target, modifiers, offhand_miss) + + if(main_miss) + if(offhand_attempted && offhand_miss) // if you miss both + user.do_attack_animation(target, used_item = attacking_item) + user.visible_message(span_warning("[user] misses with both weapons!"), span_danger("You miss with both weapons!")) + playsound(owner, 'sound/items/weapons/etherealmiss.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + + return COMPONENT_CANCEL_ATTACK_CHAIN + + if(!is_offhand && offhand_attempted && !offhand_miss) // if you hit both + target.visible_message(span_warning("[user] lands a hit with both weapons!"), span_danger("You land a hit with both weapons!")) + playsound(owner, 'sound/items/weapons/etherealhit.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + +/datum/action/cooldown/power/warfighter/dual_wielder/proc/is_valid_melee_item(obj/item/item) + if(!item) + return FALSE + if(istype(item, /obj/item/offhand)) + return FALSE + if(item.item_flags & NOBLUDGEON) + return FALSE + if(istype(item, /obj/item/gun)) + return FALSE + if(item.force <= 0) + return FALSE + return TRUE + +/datum/action/cooldown/power/warfighter/dual_wielder/proc/try_offhand_attack(mob/living/source, atom/target, list/modifiers, offhand_miss) + var/obj/item/offhand = source.get_inactive_held_item() + if(!is_valid_melee_item(offhand)) + return FALSE + if(offhand == source.get_active_held_item()) + return FALSE + if(!source.Adjacent(target)) + return FALSE + + INVOKE_ASYNC(offhand, TYPE_PROC_REF(/obj/item, melee_attack_chain), source, target, modifiers, list(DUAL_WIELD_OFFHAND = TRUE, DUAL_WIELD_ATTACK_ITEM = offhand, DUAL_WIELD_HAS_FORCED_MISS = TRUE, DUAL_WIELD_FORCED_MISS = offhand_miss)) + return TRUE + +#undef DUAL_WIELD_OFFHAND +#undef DUAL_WIELD_ATTACK_ITEM +#undef DUAL_WIELD_HAS_FORCED_MISS +#undef DUAL_WIELD_FORCED_MISS diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm index dea52705c08d32..1753a94f0248b3 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm @@ -4,7 +4,7 @@ /datum/power/warfighter/krav_maga name = "Krav Maga" desc = "Trained in various disarming moves, you can wield the martial arts of Krav Maga without any external assistance." - value = 6 + value = 7 required_powers = list(/datum/power/warfighter/martial_artist) /// Mindbound martial art component so the style follows mind transfers var/datum/component/mindbound_martial_arts/krav_component diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm index 7cf6c0bf48ccf0..eb7c4241abe8de 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm @@ -7,7 +7,7 @@ The power itself grants you the 'Quick Draw' ability, letting you 'acclimate' with an item of your choice. \ Whilst acclimated, you can use the power to instantly draw that type of item to your hand, as long as it is anywhere on your person, or within melee range of you. \ You can even use this to snag it back from your enemies." - value = 3 + value = 4 action_path = /datum/action/cooldown/power/warfighter/quick_draw /datum/action/cooldown/power/warfighter/quick_draw @@ -16,7 +16,6 @@ button_icon = 'icons/mob/actions/actions_slime.dmi' // placeholders out of the wazoo button_icon_state = "slimeeject" - cooldown_time = 5 /// Cached overlay so we can cleanly update it. var/mutable_appearance/bonded_overlay /// Type path of the bonded item. diff --git a/tgstation.dme b/tgstation.dme index de3d903904f116..f09614fa4fbb5c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7445,6 +7445,7 @@ #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_grit.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_recover.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\dual_wielder.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\explosives_specialist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\greater_tackler.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\krav_maga.dm" From 80680321ec39dda1137898fec0d7432aee1ae86c Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 17 Feb 2026 09:08:43 +0100 Subject: [PATCH 067/212] Fixed dual wield acting weirdly when holding one item. Added Focused Block. Added Rich & Filthy Rich. --- .../code/powers/mortal/expert/filthy_rich.dm | 21 ++++ .../code/powers/mortal/expert/rich.dm | 19 ++++ .../powers/mortal/warfighter/dual_wielder.dm | 9 +- .../powers/mortal/warfighter/focused_block.dm | 96 +++++++++++++++++++ .../code/powers/mortal/warfighter/tackler.dm | 2 +- tgstation.dme | 3 + 6 files changed, 146 insertions(+), 4 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/filthy_rich.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/filthy_rich.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/filthy_rich.dm new file mode 100644 index 00000000000000..18ea06eaad139b --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/filthy_rich.dm @@ -0,0 +1,21 @@ +/* + Screw the rules I have EVEN MORE money. + Why are you even on this ship if you make this much bank. +*/ + +/datum/power/expert/filthy_rich + name = "Filthy Rich" + desc = "With this much disposable money it's even a question as to why you even work anymore. You start with 10000 extra credits (includes the amount from being Rich already). And probably tons more in off-shore savings accounts." + value = 8 + required_powers = list(/datum/power/expert/rich) + + // we just make it the same as rich but reduced because we are lazy. + var/riches = 7500 + +/datum/power/expert/filthy_rich/add_unique(client/client_source) + var/mob/living/carbon/human/human_holder = power_holder + if(!human_holder.account_id) + return + var/datum/bank_account/account = SSeconomy.bank_accounts_by_id["[human_holder.account_id]"] + account.account_balance += riches + diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm new file mode 100644 index 00000000000000..a84e5900c006ed --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm @@ -0,0 +1,19 @@ +/* + Screw the rules I have money. +*/ + +/datum/power/expert/rich + name = "Rich" + desc = "Whether through good savings, connections or just nepotism; you have way more spendable cash on hand than your peers. You start the shift with 2500 extra credits in your account." + value = 5 + + // how rich are we? + var/riches = 2500 + +/datum/power/expert/rich/add_unique(client/client_source) + var/mob/living/carbon/human/human_holder = power_holder + if(!human_holder.account_id) + return + var/datum/bank_account/account = SSeconomy.bank_accounts_by_id["[human_holder.account_id]"] + account.account_balance += riches + diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm index e981c586b3c288..9479b38af6e758 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm @@ -49,9 +49,12 @@ return var/is_offhand = LAZYACCESS(attack_modifiers, DUAL_WIELD_OFFHAND) - var/obj/item/attacking_item = LAZYACCESS(attack_modifiers, DUAL_WIELD_ATTACK_ITEM) || source.get_active_held_item() - if(!is_valid_melee_item(attacking_item)) + var/obj/item/main_item = source.get_active_held_item() + var/obj/item/off_item = source.get_inactive_held_item() + // Only apply dual-wield logic if both hands are valid melee weapons (force > 0). + if(!is_valid_melee_item(main_item) || !is_valid_melee_item(off_item)) return + var/obj/item/attacking_item = LAZYACCESS(attack_modifiers, DUAL_WIELD_ATTACK_ITEM) || main_item var/forced_miss = FALSE var/has_forced_miss = LAZYACCESS(attack_modifiers, DUAL_WIELD_HAS_FORCED_MISS) @@ -74,7 +77,7 @@ return COMPONENT_CANCEL_ATTACK_CHAIN if(!is_offhand && offhand_attempted && !offhand_miss) // if you hit both - target.visible_message(span_warning("[user] lands a hit with both weapons!"), span_danger("You land a hit with both weapons!")) + target.visible_message(span_warning("[user] lands a hit with both weapons!"), span_userdanger("You were hit by both of [user]'s weapons!")) playsound(owner, 'sound/items/weapons/etherealhit.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) /datum/action/cooldown/power/warfighter/dual_wielder/proc/is_valid_melee_item(obj/item/item) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm new file mode 100644 index 00000000000000..eafa1d44713623 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm @@ -0,0 +1,96 @@ +/datum/power/warfighter/focused_block + name = "Focused Block" + desc = "Using what you have on you, you raise your block chance by 50 for 1.5 seconds, as long as you are holding a bulky-sized item or an item with a block chance. \ + This stacks on-top of any existing block you may have, guaranteeing blocks with most shields. Has a short cooldown." + value = 6 + + action_path = /datum/action/cooldown/power/warfighter/focused_block + required_powers = list(/datum/power/warfighter/quick_draw) + +/datum/action/cooldown/power/warfighter/focused_block + name = "Focused Block" + desc = "You raise your block chance by 50 for 1.5 seconds, as long as you are holding a bulky-sized item or an item with a block chance. This stacks on-top of any existing block you may have." + button_icon = 'icons/obj/weapons/shields.dmi' + button_icon_state = "kite" + cooldown_time = 120 + +// Status effect handles most of the actual effects; we check for requirements here +/datum/action/cooldown/power/warfighter/focused_block/use_action(mob/living/user, atom/target) + var/obj/item/active_item = user.get_active_held_item() + var/obj/item/inactive_item = user.get_inactive_held_item() + var/has_valid_item = FALSE + + if(active_item && (active_item.w_class >= WEIGHT_CLASS_BULKY || active_item.block_chance > 0)) + has_valid_item = TRUE + else if(inactive_item && (inactive_item.w_class >= WEIGHT_CLASS_BULKY || inactive_item.block_chance > 0)) + has_valid_item = TRUE + + if(!has_valid_item) + user.balloon_alert(user, "need bulky or blocking item") + return FALSE + + // apply status effect + var/datum/status_effect/power/focused_block/applied = user.apply_status_effect(/datum/status_effect/power/focused_block) + return !!applied + +// 1.5 seconds of hieghtened block +/datum/status_effect/power/focused_block + id = "focused_block" + duration = 1.5 SECONDS + tick_interval = STATUS_EFFECT_NO_TICK + alert_type = null + var/block_bonus = 50 + +/datum/status_effect/power/focused_block/on_apply() + if(!owner) + return FALSE + var/image/flash_overlay = new('icons/effects/effects.dmi', owner, "shield-flash", dir = pick(GLOB.cardinals)) + owner.flick_overlay_view(flash_overlay, 30) + RegisterSignal(owner, COMSIG_LIVING_CHECK_BLOCK, PROC_REF(check_block)) + return TRUE + +/datum/status_effect/power/focused_block/on_remove() + if(owner) + UnregisterSignal(owner, COMSIG_LIVING_CHECK_BLOCK) + +// We use the COMSIG_LIVING_CHECK_BLOCK signal to check artifically for block. +/datum/status_effect/power/focused_block/proc/check_block(mob/living/blocking_user, atom/movable/hitby, damage, attack_text, attack_type, armour_penetration, damage_type) + SIGNAL_HANDLER + + var/has_valid_item = FALSE + var/best_block = 0 + for(var/obj/item/held_item in blocking_user.held_items) + if(!held_item) + continue + if(held_item.w_class >= WEIGHT_CLASS_BULKY || held_item.block_chance > 0) + has_valid_item = TRUE + if(held_item.block_chance > best_block) + best_block = held_item.block_chance + + if(!has_valid_item) + return NONE + + // guaranteed block chance + best_block = max(best_block, 0) + var/target_block = min(100, best_block + block_bonus) + if(target_block >= 100) + block_effect(blocking_user, attack_text) + return SUCCESSFUL_BLOCK + + // random chance for block + var/bonus_chance = 100 - ((100 - target_block) * 100 / (100 - best_block)) + bonus_chance = clamp(bonus_chance, 0, 100) + if(!prob(bonus_chance)) + return NONE + block_effect(blocking_user, attack_text) + return SUCCESSFUL_BLOCK + +// we have to mimmick the block effects cause they're not baked into COMSIG_LIVING_CHECK_BLOCK by default. +/datum/status_effect/power/focused_block/proc/block_effect(mob/living/blocking_user, attack_text) + blocking_user.visible_message( + span_danger("[blocking_user] blocks [attack_text]!"), + span_userdanger("You block [attack_text]!"), + ) + var/owner_turf = get_turf(blocking_user) + new /obj/effect/temp_visual/block(owner_turf, COLOR_YELLOW) + playsound(blocking_user, 'sound/items/weapons/parry.ogg', BLOCK_SOUND_VOLUME, vary = TRUE) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm index d162c1123b2b6f..3de3c0683c4b4a 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm @@ -13,7 +13,7 @@ var/datum/component/tackler /datum/power/warfighter/tackler/add() - // Taking these over from tackle gloves just for clarity. They're in here becuase I don't want to clog the upgrade vars with these. + // Taking these over from tackle gloves just for clarity. They're in here becuase I don't want to clog the upgrade vars with these + the component inherits these values so having them tweakable in vv doesnt make sense. /// See: [/datum/component/tackler/var/stamina_cost] var/tackle_stam_cost = 25 /// See: [/datum/component/tackler/var/base_knockdown] diff --git a/tgstation.dme b/tgstation.dme index f09614fa4fbb5c..7b0b54ad20f63b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7432,9 +7432,11 @@ #include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_action.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\eye_for_ingredients.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\master_surgeon.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\obfuscate_voice.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\expert\rich.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\strider.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\zoologist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\creature_tamer.dm" @@ -7447,6 +7449,7 @@ #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\command_recover.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\dual_wielder.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\explosives_specialist.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\focused_block.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\greater_tackler.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\krav_maga.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\martial_artist.dm" From 59f9d221cbb1d214f23bc97d8558205b8760f554 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 17 Feb 2026 11:22:49 +0100 Subject: [PATCH 068/212] Adds heavy slam. This is so satisfying to use oml I wasted a hour running around the station breaking shit. --- .../powers/mortal/warfighter/heavy_slam.dm | 164 ++++++++++++++++++ .../powers/mortal/warfighter/krav_maga.dm | 2 +- tgstation.dme | 1 + 3 files changed, 166 insertions(+), 1 deletion(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm new file mode 100644 index 00000000000000..8400e08990e3bd --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm @@ -0,0 +1,164 @@ +/* + Hits everything in a 2x3 melee aoe in the targeted direction. Damages creatures, and objects, if in combat mode. +*/ + +/datum/power/warfighter/heavy_slam + name = "Heavy Slam" + desc = "You perform a massive, arcing strike that hits a large area. You strike the 2x3 area adjacent to you in the target direction, hitting everyone in the area (and everything, if in combat mode). \ + A creature can only be hit once by this power, but large creatures take double damage. Requires you to actively be wielding a two-handed weapon." + value = 4 + + action_path = /datum/action/cooldown/power/warfighter/heavy_slam + required_powers = list(/datum/power/warfighter/quick_draw) + +/datum/action/cooldown/power/warfighter/heavy_slam + name = "Heavy Slam" + desc = "You strike the 2x3 area adjacent to you in the target direction, hitting everyone in the area (and everything, if in combat mode). \ + A creature can only be hit once by this power, but large creatures take double damage. Requires you to actively be wielding a two-handed weapon." + button_icon = 'icons/obj/weapons/hammer.dmi' + button_icon_state = "hammeron" + cooldown_time = 120 + click_to_activate = TRUE + target_self = FALSE + +/datum/action/cooldown/power/warfighter/heavy_slam/use_action(mob/living/user, atom/target) + var/obj/item/active_item = user.get_active_held_item() + if(!active_item) + user.balloon_alert(user, "requires a two-handed weapon!") + return FALSE + + var/datum/component/two_handed/twohanded = active_item.GetComponent(/datum/component/two_handed) + if(!twohanded || !HAS_TRAIT(active_item, TRAIT_WIELDED)) + user.balloon_alert(user, "requires a two-handed weapon!") + return FALSE + + // gets what way we should swing. + var/dir_to_use = get_cardinal_dir(user, target) + if(!dir_to_use) + return FALSE + // we turn towards where we swinging. + user.dir = dir_to_use + + //gets the area that we are going to be slamming + var/turf/origin = get_turf(user) + if(!origin) + return FALSE + + var/list/strike_turfs = list() + var/dir_left = turn(dir_to_use, 90) + var/dir_right = turn(dir_to_use, -90) + + var/turf/row1 = get_step(origin, dir_to_use) + var/turf/row2 = null + + add_slam_row(strike_turfs, row1, dir_left, dir_right) + if(row1 && !is_blocked_turf(row1)) // check if we are allowed to smash through the first row. + row2 = get_step(row1, dir_to_use) + add_slam_row(strike_turfs, row2, dir_left, dir_right) + + // applies the slam vfx + for(var/turf/strike_turf in strike_turfs) + new /obj/effect/temp_visual/dir_setting/warfighter_heavy_slam(strike_turf, dir_to_use) + + var/turf/impact_turf = row1 ? row1 : origin + playsound(impact_turf, 'sound/effects/meteorimpact.ogg', 80, TRUE) + + var/list/shaken_mobs = list() + for(var/turf/strike_turf in strike_turfs) + for(var/mob/living/shake_mob in view(2, strike_turf)) + if(shake_mob in shaken_mobs) + continue + shaken_mobs += shake_mob + shake_camera(shake_mob, 2, 1) + + RegisterSignal(active_item, COMSIG_ITEM_ATTACK_ANIMATION, PROC_REF(suppress_attack_animation)) + + // handles hitting mobs + var/list/hit_mobs = list() + for(var/turf/strike_turf in strike_turfs) + for(var/mob/living/hit_mob in strike_turf) + if(hit_mob == user) + continue + if(hit_mob in hit_mobs) + continue + hit_mobs += hit_mob + + var/list/attack_modifiers = list() + if(is_multi_tile_object(hit_mob) || hit_mob.mob_size >= MOB_SIZE_LARGE) + attack_modifiers[FORCE_MULTIPLIER] = 2 + + active_item.melee_attack_chain(user, hit_mob, null, attack_modifiers) + + // handles hitting objects + if(user.combat_mode) + var/list/hit_objs = list() + for(var/turf/strike_turf in strike_turfs) + for(var/obj/target_obj in strike_turf) + if(target_obj == active_item) + continue + if(!(isstructure(target_obj) || ismachinery(target_obj))) + continue + if(target_obj in hit_objs) + continue + hit_objs += target_obj + target_obj.attackby(active_item, user, null, null) + + UnregisterSignal(active_item, COMSIG_ITEM_ATTACK_ANIMATION) + + // short cd on hit + melee_cooldown_time = active_item.attack_speed + return TRUE + +// Handles the aoe row by row. Basically gets the left and right turfs of the direction. +/datum/action/cooldown/power/warfighter/heavy_slam/proc/add_slam_row(list/strike_turfs, turf/row_turf, dir_left, dir_right) + if(!row_turf) + return + if(!(row_turf in strike_turfs)) + strike_turfs += row_turf + + var/turf/left_turf = get_step(row_turf, dir_left) + if(left_turf && !(left_turf in strike_turfs)) + strike_turfs += left_turf + + var/turf/right_turf = get_step(row_turf, dir_right) + if(right_turf && !(right_turf in strike_turfs)) + strike_turfs += right_turf + +// We check if we can smash through the first row. +/datum/action/cooldown/power/warfighter/heavy_slam/proc/is_blocked_turf(turf/row_turf) + if(!row_turf) + return TRUE + if(row_turf.density) + return TRUE + for(var/obj/blocked_obj in row_turf) + if(blocked_obj.density) + return TRUE + return FALSE + +// Okay look it sounds menacing but this basically just gets the direction that we're swinging towards based on where we click. +/datum/action/cooldown/power/warfighter/heavy_slam/proc/get_cardinal_dir(mob/living/user, atom/target) + if(!user) + return 0 + var/dir_to_use = target ? get_dir(user, target) : user.dir + if(!dir_to_use) + return 0 + if((dir_to_use & (NORTH | SOUTH)) && (dir_to_use & (EAST | WEST))) + var/dx = target ? (target.x - user.x) : 0 + var/dy = target ? (target.y - user.y) : 0 + if(abs(dx) >= abs(dy)) + dir_to_use = (dx >= 0) ? EAST : WEST + else + dir_to_use = (dy >= 0) ? NORTH : SOUTH + return dir_to_use + +// This is a pretty crigne way of doing it but uhh I am out of ideas on how to do this better. +// Prevents everyone in the area from being visible struck by the weapon. +/datum/action/cooldown/power/warfighter/heavy_slam/proc/suppress_attack_animation(obj/item/source, atom/movable/attacker, atom/attacked_atom, animation_type, list/image_override, list/animation_override, list/angle_override) + SIGNAL_HANDLER + image_override += image(icon = 'icons/effects/effects.dmi', icon_state = "nothing") + +// Effect of the slam +/obj/effect/temp_visual/dir_setting/warfighter_heavy_slam + icon = 'icons/effects/effects.dmi' + icon_state = "smash" + duration = 3 diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm index 1753a94f0248b3..bc0af0fe013ce1 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm @@ -4,7 +4,7 @@ /datum/power/warfighter/krav_maga name = "Krav Maga" desc = "Trained in various disarming moves, you can wield the martial arts of Krav Maga without any external assistance." - value = 7 + value = 9 required_powers = list(/datum/power/warfighter/martial_artist) /// Mindbound martial art component so the style follows mind transfers var/datum/component/mindbound_martial_arts/krav_component diff --git a/tgstation.dme b/tgstation.dme index 7b0b54ad20f63b..a228400c994cf4 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7451,6 +7451,7 @@ #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\explosives_specialist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\focused_block.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\greater_tackler.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\warfighter\heavy_slam.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\krav_maga.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\martial_artist.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\quick_draw.dm" From 1501ff5a3a71701af81442a0bb5d5f5353c0c0d1 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 19 Feb 2026 16:22:13 +0100 Subject: [PATCH 069/212] tweaks quick draw cost --- .../code/powers/mortal/warfighter/quick_draw.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm index eb7c4241abe8de..9b1a26fb3a5268 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm @@ -7,7 +7,7 @@ The power itself grants you the 'Quick Draw' ability, letting you 'acclimate' with an item of your choice. \ Whilst acclimated, you can use the power to instantly draw that type of item to your hand, as long as it is anywhere on your person, or within melee range of you. \ You can even use this to snag it back from your enemies." - value = 4 + value = 3 action_path = /datum/action/cooldown/power/warfighter/quick_draw /datum/action/cooldown/power/warfighter/quick_draw @@ -114,7 +114,7 @@ if(ishuman(user)) var/mob/living/carbon/human/human_user = user var/slot_id = human_user.get_slot_by_item(candidate_item) - if(slot_id == ITEM_SLOT_LPOCKET || slot_id == ITEM_SLOT_RPOCKET || slot_id == ITEM_SLOT_BELT || slot_id == ITEM_SLOT_SUITSTORE || slot_id == ITEM_SLOT_ID) + if(slot_id == ITEM_SLOT_LPOCKET || slot_id == ITEM_SLOT_RPOCKET || slot_id == ITEM_SLOT_BELT || slot_id == ITEM_SLOT_BACK || slot_id == ITEM_SLOT_SUITSTORE || slot_id == ITEM_SLOT_ID) allow_equipped_slot = TRUE if(candidate_item in equipped_items) From d7eaefbb6d256d3bad16efc4f32ccaa644e94f7a Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 19 Feb 2026 19:26:38 +0100 Subject: [PATCH 070/212] Polishes psyker backend. --- code/__DEFINES/~doppler_defines/powers.dm | 3 +++ .../code/powers/resonant/meditate.dm | 2 +- .../powers/resonant/psyker/_psyker_action.dm | 26 ++++++++++++++----- .../powers/resonant/psyker/_psyker_organ.dm | 20 ++++++-------- .../code/powers/resonant/psyker/levitate.dm | 3 ++- .../powers/resonant/psyker/telekinesis.dm | 8 +++--- 6 files changed, 38 insertions(+), 24 deletions(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index d90530040d64ce..19983e21faf2d1 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -40,6 +40,9 @@ // Trait for when you are immune to resonant powers #define TRAIT_ANTIRESONANCE "TRAIT_ANTIRESONANCE" +// Trait for when you are immune to resonant powers that reveal any information about you. +#define TRAIT_ANTIRESONANCE_SCRYING "TRAIT_ANTIRESONANCE_SCRYING" + // Listener for dispelling #define COMSIG_ATOM_DISPEL "atom_dispel" diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index b802eaf85c6bf0..a8f2e2c68156f5 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -30,7 +30,7 @@ Reduces stress for psykers and restores Dantian for cultivators to_chat(owner, span_notice("I have nothing to meditate on!")) keep_going = FALSE if(psyker_organ) - psyker_organ.remove_stress(PSYKER_STRESS_MEDITATION_POWER) + psyker_organ.modify_stress(-PSYKER_STRESS_MEDITATION_POWER) if(psyker_organ.stress <= 0) to_chat(owner, span_notice("I no longer feel any stress")) keep_going = FALSE 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 dc7467fcaad3c5..b0b7871316eead 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 @@ -10,6 +10,9 @@ // Disabled by the trait var/disabled_by_mental_immunity = TRUE + //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 + /datum/action/cooldown/power/psyker/New() . = ..() ValidateOrgan() @@ -25,11 +28,8 @@ // This doesn't actually add the stress itself; it merely tells the organ to add the stress. // Why not handle it here? Because why give them an organ if we're not going to use it?! // Validation is handled on the organ side. -/datum/action/cooldown/power/psyker/proc/add_stress(amount) - psyker_organ.add_stress(amount) - -/datum/action/cooldown/power/psyker/proc/remove_stress(amount) - psyker_organ.remove_stress(amount) +/datum/action/cooldown/power/psyker/proc/modify_stress(amount) + psyker_organ.modify_stress(amount) // We added checking for organs on try_use, as well as making sure that if we are wearing a tinfoil cap, we can't just wield our psychic powers. /datum/action/cooldown/power/psyker/try_use(mob/living/user, mob/living/target) @@ -37,10 +37,22 @@ owner.balloon_alert(owner, "No paracausal gland!") return FALSE if(isliving(target)) + if(mental && !can_affect_mental(target)) + modify_stress(PSYKER_STRESS_MINOR) // This actively stresses you out. + owner.balloon_alert(owner, "Something interveres with your powers!") + return FALSE if(disabled_by_mental_immunity && target.can_block_magic(MAGIC_RESISTANCE_MIND)) - add_stress(PSYKER_STRESS_MINOR) // This actively stresses you out. + modify_stress(PSYKER_STRESS_MINOR) // This actively stresses you out. owner.balloon_alert(owner, "Something interveres with your powers!") return FALSE . = .. () - +// Checks if the target can be affected by psyker stuff, since it has its own litle list of unique immunities. Returns TRUE if the target has nothing that affects mental. +/datum/action/cooldown/power/psyker/proc/can_affect_mental(mob/living/target, charge_cost = 0) + if(!target) + return TRUE + if(target.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = charge_cost)) + return FALSE + if(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_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index 92d9b82e5a611f..7fcc8ebfd759dc 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -24,16 +24,12 @@ //The stress warning message var/datum/status_effect/power/stress_warning -// Don't modify stress directly. In the future affinity has a bearing on how much stress you gain. -/obj/item/organ/resonant/psyker/proc/add_stress(amount) - // TODO; Add clothing affinity. Wearing psychic nicknacks makes you gain less stress. - stress = max(0, stress + amount) - return - -/obj/item/organ/resonant/psyker/proc/remove_stress(amount) - // TODO: Ditto on above. - stress = max(0, stress - amount) - return +// Call to modify stress. Don't adjust directly. +/obj/item/organ/resonant/psyker/proc/modify_stress(amount, override_cap) + if(!isnum(amount)) + return + var/cap_to = isnum(override_cap) ? override_cap : PSYKER_STRESS_STANDARD_THRESHOLD * 2 + stress = clamp(stress + amount, 0, cap_to) /obj/item/organ/resonant/psyker/on_life(seconds_per_tick, times_fired) . = ..() @@ -63,10 +59,10 @@ CDstressMild = 0 CDstressSevere = 0 else if(stress >= (stress_threshold * 1.5) && CDstressSevere <= 0) // Severe Event - CDstressSevere = 60 // reset CD + CDstressSevere = 90 // reset CD stress_backlash(PSYKER_EVENT_TIER_SEVERE) else if (stress >= stress_threshold && CDstressMild <= 0) // Mild Event - CDstressMild = 60 // reset CD + CDstressMild = 90 // reset CD stress_backlash(PSYKER_EVENT_TIER_MILD) if(CDstressMild > 0) 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 0d2d276df04604..bf82467d947547 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -13,6 +13,7 @@ button_icon = 'icons/mob/actions/actions_minor_antag.dmi' button_icon_state = "beam_up" + mental = FALSE // Overlay we add to the caster var/mutable_appearance/caster_effect @@ -61,7 +62,7 @@ var/cost = PSYKER_STRESS_TRIVIAL * 2 if(psyker.get_quirk(/datum/quirk/paraplegic)) // There'll probably be several that'd like to do this. Effecively puts you just below the rate at which regen will keep up. cost = PSYKER_STRESS_TRIVIAL - add_stress(cost * seconds_per_tick) + modify_stress(cost * seconds_per_tick) // Dispel function; basically off-switch and possibly comedic faceplant /datum/action/cooldown/power/psyker/levitate/Grant(mob/granted_to) 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 c779f9333099ce..a03eff37e7d1c9 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -23,6 +23,8 @@ TODO: FIX THAT 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 @@ -151,7 +153,7 @@ TODO: FIX THAT return - add_stress(PSYKER_STRESS_TRIVIAL * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever. + modify_stress(PSYKER_STRESS_TRIVIAL * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever. // The fun part, punting shit. /datum/action/cooldown/power/psyker/telekinesis/proc/punt_held(mob/living/user, atom/target, params) @@ -174,7 +176,7 @@ TODO: FIX THAT var/atom/movable/launched = grabbed_atom // Basically the same stress cost for picking it up. - add_stress(get_stress_cost_for_atom(launched)) + modify_stress(get_stress_cost_for_atom(launched)) clear_grab(playsound = FALSE) playsound(launched, 'sound/effects/magic/repulse.ogg', 75, TRUE) @@ -268,7 +270,7 @@ TODO: FIX THAT kinesis_catcher.assign_to_mob(owner) // Amounts are in the get_stress_cost_for_atom - add_stress(get_stress_cost_for_atom(target)) + modify_stress(get_stress_cost_for_atom(target)) START_PROCESSING(SSfastprocess, src) From ce4bb548940f8bd590b7899aeddcf0968d959a0f Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 19 Feb 2026 20:37:06 +0100 Subject: [PATCH 071/212] Tweaks some costs on psyker root and levitate --- .../modular_powers/code/powers/resonant/psyker/_psyker_root.dm | 2 +- .../modular_powers/code/powers/resonant/psyker/levitate.dm | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 eb611d59c61c29..da7e15e02e9a44 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 @@ -5,7 +5,7 @@ grown by prolonged exposure to certain types of Resonance. \ The catalyst for psychic abilities; but beware overexerting it." - value = 3 + value = 2 power_flags = POWER_HUMAN_ONLY mob_trait = TRAIT_ARCHETYPE_RESONANT archetype = POWER_ARCHETYPE_RESONANT 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 bf82467d947547..221ec47191f624 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -2,7 +2,7 @@ name = "Levitate" desc = "Grants the ability to levitate yourself above surfaces and letting you propel yourself in zero-gravity. Passively drains stress while in use." - value = 5 + value = 4 priority = POWER_PRIORITY_BASIC required_powers = list(/datum/power/psyker_root) action_path = /datum/action/cooldown/power/psyker/levitate From 9e8b5af8457461897ebd61800a16ee5db608c296 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 20 Feb 2026 11:39:16 +0100 Subject: [PATCH 072/212] Adds scrying , not wholy finished. --- .../powers/resonant/psyker/_psyker_action.dm | 16 +- .../code/powers/resonant/psyker/scrying.dm | 428 ++++++++++++++++++ tgstation.dme | 1 + 3 files changed, 444 insertions(+), 1 deletion(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm 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 b0b7871316eead..b274341c9e94d6 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 @@ -47,12 +47,26 @@ return FALSE . = .. () -// Checks if the target can be affected by 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. Returns TRUE if the target has nothing that affects mental. /datum/action/cooldown/power/psyker/proc/can_affect_mental(mob/living/target, charge_cost = 0) if(!target) return TRUE if(target.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = charge_cost)) return FALSE + if(target.can_block_magic(MAGIC_RESISTANCE, charge_cost = charge_cost)) + return FALSE + if(target.can_block_resonance(charge_cost)) + return FALSE if(HAS_TRAIT(target, TRAIT_DUMB)) // this is a feature return FALSE return TRUE + +// Checks if the target can be affected by specifically psyker's scrying +/datum/action/cooldown/power/psyker/proc/can_affect_scrying(mob/living/target, charge_cost = 0) + if(!target) + return TRUE + if(!can_affect_mental(target)) + return FALSE + if(HAS_TRAIT(target, TRAIT_ANTIRESONANCE_SCRYING)) + return FALSE + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm new file mode 100644 index 00000000000000..fc0eb3849824ec --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -0,0 +1,428 @@ +/* + TODO: Add blood for targeting. +*/ + +/datum/power/psyker_power/scrying + name = "Scrying" + desc = "Using a sample of a creature's blood, you can see the world through their eyes remotely. \ + In this state, you use their sight instead of your own; but you cannot see creatures that are immune to magic, scrying; or lack the brain activity required to be detectable (dumb). \ + Passively builds up stress. The target sometimes gets preminations to indicate they are watched." + + value = 9 + priority = POWER_PRIORITY_BASIC + action_path = /datum/action/cooldown/power/psyker/scrying + +/datum/action/cooldown/power/psyker/scrying + name = "Scrying" + desc = "Using a sample of a creature's blood, you can see the world through their eyes remotely." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "telepathy" + click_to_activate = TRUE + + var/atom/movable/scry_target + + // This thing is a MESS. We have split functionality into three datums. + // Scrying Camera which handles imparting the sight of the target + var/datum/scrying_camera/scry_camera + // Scrying Vision which handles vision traits on the user. + var/datum/scrying_vision/scry_vision + // Scrying Immunity Mask which takes care of hiding creatures immune from scrying + var/datum/scrying_immunity_mask/immunity_mask + // and Scrying Tracker which basically handels any and all things related to stress gain. + var/datum/psyker_scry_tracker/tracker + + + // Did our scrying get blocked by something? + var/scry_blocked = FALSE + +/datum/action/cooldown/power/psyker/scrying/Trigger(mob/clicker, trigger_flags, atom/target) + . = ..() + if(active) + end_scrying() + to_chat(owner, span_notice("You return your senses to your mind.")) + return FALSE + return TRUE + +/* + Most of the delegation with scrying is handled by scry_vision +*/ +/datum/action/cooldown/power/psyker/scrying/use_action(mob/living/user, atom/target) + if(!isliving(target)) + return FALSE + + if(!can_affect_scrying(target)) + to_chat(user, span_warning("Your sight cannot find purchase on that mind.")) + return FALSE + + scry_target = target + active = TRUE + + scry_camera = new(user, scry_target, src) + scry_vision = new(user) + tracker = new(src, user) + immunity_mask = new(src, user, scry_camera.scry_eye) + immunity_mask.refresh_now() + + return TRUE + +/datum/action/cooldown/power/psyker/scrying/Remove(mob/removed_from) + end_scrying() + return ..() + +/datum/action/cooldown/power/psyker/scrying/proc/end_scrying() + if(!active) + return + + active = FALSE + + QDEL_NULL(tracker) + QDEL_NULL(scry_vision) + QDEL_NULL(scry_camera) + QDEL_NULL(immunity_mask) + + scry_target = null + +/* + We bypass our own vision traits and see the world from the target's pov. + Handles the removal of vision traits and the application of the overlay. +*/ +/datum/scrying_vision + // Used to remove/re-add quirk blindness safely. + var/had_blind_quirk = FALSE + var/datum/weakref/viewer_ref + +/datum/scrying_vision/New(mob/living/viewer) + . = ..() + viewer_ref = WEAKREF(viewer) + apply() + +/datum/scrying_vision/Destroy() + clear() + viewer_ref = null + return ..() + +/datum/scrying_vision/proc/apply() + var/mob/living/viewer = viewer_ref?.resolve() + if(!istype(viewer)) + return + + // If blindness is being enforced by the blind quirk, we temporarily remove it. + if(viewer.is_blind_from(QUIRK_TRAIT)) + had_blind_quirk = TRUE + viewer.remove_status_effect(/datum/status_effect/grouped/blindness, QUIRK_TRAIT) + + ADD_TRAIT(viewer, TRAIT_SIGHT_BYPASS, REF(src)) + + // Restrict vision partially. + viewer.overlay_fullscreen("curse", /atom/movable/screen/fullscreen/curse, 1) + viewer.update_sight() + +/datum/scrying_vision/proc/clear() + var/mob/living/viewer = viewer_ref?.resolve() + if(!istype(viewer)) + return + + viewer.clear_fullscreen("curse", 50) + + REMOVE_TRAIT(viewer, TRAIT_SIGHT_BYPASS, REF(src)) + + // Restore the blind quirk's blindness if we removed it. + if(had_blind_quirk) + viewer.become_blind(QUIRK_TRAIT) + + had_blind_quirk = FALSE + viewer.update_sight() + +/* + This sets the player's perspective to a scry eye that follows the target. +*/ +/datum/scrying_camera + var/datum/weakref/viewer_ref + var/datum/weakref/target_ref + var/datum/weakref/action_ref + var/mob/eye/psyker_scry/scry_eye + + +/datum/scrying_camera/New(mob/living/viewer, atom/movable/target, datum/action/cooldown/power/psyker/scrying/action) + . = ..() + viewer_ref = WEAKREF(viewer) + target_ref = WEAKREF(target) + action_ref = WEAKREF(action) + + var/turf/target_turf = get_turf(target) + if(!target_turf) + qdel(src) + return + + scry_eye = new(target_turf) + scry_eye.set_target(target) + scry_eye.assign_user(viewer) + + RegisterSignals(target, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING), PROC_REF(on_target_event)) + +/datum/scrying_camera/Destroy() + var/atom/movable/target = target_ref?.resolve() + if(target) + UnregisterSignal(target, list(COMSIG_MOVABLE_MOVED, COMSIG_QDELETING)) + + if(scry_eye) + scry_eye.assign_user(null) + QDEL_NULL(scry_eye) + + viewer_ref = null + target_ref = null + return ..() + +/datum/scrying_camera/proc/on_target_event(datum/source) + SIGNAL_HANDLER + + if(!scry_eye || QDELETED(scry_eye)) + qdel(src) + return + + var/atom/movable/target = target_ref?.resolve() + if(QDELETED(target) || !ismovable(target)) + qdel(src) + return + + var/turf/target_turf = get_turf(target) + if(target_turf) + scry_eye.setLoc(target_turf) + var/datum/action/cooldown/power/psyker/scrying/action = action_ref?.resolve() + action?.immunity_mask?.refresh_now() + + + +/* + Tracker just adds stress and handles proccessing. +*/ +/datum/psyker_scry_tracker + var/datum/weakref/action_ref + var/datum/weakref/owner_ref + +/datum/psyker_scry_tracker/New(datum/action/cooldown/power/psyker/scrying/action, mob/living/owner) + . = ..() + action_ref = WEAKREF(action) + owner_ref = WEAKREF(owner) + START_PROCESSING(SSfastprocess, src) + +/datum/psyker_scry_tracker/Destroy() + STOP_PROCESSING(SSfastprocess, src) + return ..() + +/datum/psyker_scry_tracker/process(seconds_per_tick) + var/datum/action/cooldown/power/psyker/scrying/action = action_ref?.resolve() + if(!action || !action.active) + qdel(src) + return + + var/mob/living/owner = owner_ref?.resolve() + if(!owner) + action.end_scrying() + qdel(src) + return + + var/atom/movable/current_target = action.scry_target + if(current_target && !action.can_affect_scrying(current_target)) + action.end_scrying() + to_chat(owner, span_warning("Your scrying link was cut off!")) + qdel(src) + return + + // Stress over time + action.modify_stress(PSYKER_STRESS_MINOR * seconds_per_tick) + + // Re-apply in case other systems reassert blindness/quirk/etc. + if(action.scry_vision) + action.scry_vision.apply() + +/* + Used to mask mobs from the scrying eye. +*/ +/datum/scrying_immunity_mask + var/datum/weakref/viewer_ref + var/datum/weakref/eye_ref + var/datum/weakref/action_ref + + // mob -> list(hide_image) + var/list/masked_mobs = list() + +/datum/scrying_immunity_mask/New(datum/action/cooldown/power/psyker/scrying/action, mob/living/viewer, mob/eye/psyker_scry/eye) + . = ..() + action_ref = WEAKREF(action) + viewer_ref = WEAKREF(viewer) + eye_ref = WEAKREF(eye) + + if(viewer) + viewer.mob_flags |= MOB_HAS_SCREENTIPS_NAME_OVERRIDE + RegisterSignal(viewer, COMSIG_MOB_REQUESTING_SCREENTIP_NAME_FROM_USER, PROC_REF(screentip_name_override)) + RegisterSignal(viewer, COMSIG_LIVING_PERCEIVE_EXAMINE_NAME, PROC_REF(examine_name_override)) + + START_PROCESSING(SSfastprocess, src) + +/datum/scrying_immunity_mask/Destroy() + STOP_PROCESSING(SSfastprocess, src) + var/mob/living/viewer = viewer_ref?.resolve() + if(viewer) + UnregisterSignal(viewer, list(COMSIG_MOB_REQUESTING_SCREENTIP_NAME_FROM_USER, COMSIG_LIVING_PERCEIVE_EXAMINE_NAME)) + clear_all() + return ..() + +/datum/scrying_immunity_mask/process(seconds_per_tick) + var/datum/action/cooldown/power/psyker/scrying/action = action_ref?.resolve() + var/mob/living/viewer = viewer_ref?.resolve() + var/mob/eye/psyker_scry/eye = eye_ref?.resolve() + + if(!action || !action.active || !viewer || !viewer.client || !eye) + qdel(src) + return + + update_masks(viewer, eye, action) + +/datum/scrying_immunity_mask/proc/refresh_now() + var/datum/action/cooldown/power/psyker/scrying/action = action_ref?.resolve() + var/mob/living/viewer = viewer_ref?.resolve() + var/mob/eye/psyker_scry/eye = eye_ref?.resolve() + if(!action || !action.active || !viewer || !viewer.client || !eye) + return + + update_masks(viewer, eye, action) + +/datum/scrying_immunity_mask/proc/update_masks(mob/living/viewer, mob/eye/psyker_scry/eye, datum/action/cooldown/power/psyker/scrying/action) + // Determine what mobs are currently "in view" of the scry eye. + // Use view() around the eye, not around the viewer. + var/list/current_mobs = list() + for(var/mob/living/seen_mob in view(viewer.client.view, eye)) + current_mobs += seen_mob + + // Remove masks for mobs no longer in view (or deleted) + for(var/mob/living/masked_mob as anything in masked_mobs.Copy()) + if(QDELETED(masked_mob) || !(masked_mob in current_mobs) || action.can_affect_scrying(masked_mob)) + unmask_mob(viewer, masked_mob) + + // Apply masks for newly seen immune mobs (excluding the direct target if you want) + for(var/mob/living/seen_mob as anything in current_mobs) + if(seen_mob == action.scry_target) + continue + + if(masked_mobs[seen_mob]) + continue + + if(!action.can_affect_scrying(seen_mob)) + mask_mob(viewer, seen_mob) + +/datum/scrying_immunity_mask/proc/mask_mob(mob/living/viewer, mob/living/target_mob) + if(!viewer?.client || QDELETED(target_mob)) + return + + // Hide the mob for THIS client only (visual override) + var/image/hide_image = image(loc = target_mob) + hide_image.appearance = target_mob.appearance + hide_image.override = TRUE + hide_image.alpha = 0 + + viewer.client.images += hide_image + + masked_mobs[target_mob] = list(hide_image) + RegisterSignal(target_mob, COMSIG_ATOM_EXAMINE, PROC_REF(on_target_examine)) + hide_data_huds(viewer, target_mob) + +/datum/scrying_immunity_mask/proc/unmask_mob(mob/living/viewer, mob/living/target_mob) + var/list/entry = masked_mobs[target_mob] + if(!entry) + return + + var/image/hide_image = entry[1] + + if(viewer?.client) + if(hide_image) + viewer.client.images -= hide_image + + UnregisterSignal(target_mob, COMSIG_ATOM_EXAMINE) + unhide_data_huds(viewer, target_mob) + masked_mobs -= target_mob + +/datum/scrying_immunity_mask/proc/clear_all() + var/mob/living/viewer = viewer_ref?.resolve() + if(!viewer?.client) + masked_mobs.Cut() + return + + for(var/mob/living/masked_mob as anything in masked_mobs.Copy()) + unmask_mob(viewer, masked_mob) + +/datum/scrying_immunity_mask/proc/on_target_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + + var/mob/living/viewer = viewer_ref?.resolve() + if(user != viewer) + return NONE + + if(!istype(source, /mob/living) || !masked_mobs[source]) + return NONE + + examine_list.Cut() + examine_list += span_notice("It's too hazy to make out details.") + return NONE + +/datum/scrying_immunity_mask/proc/hide_data_huds(mob/living/viewer, mob/living/target_mob) + if(!viewer || !target_mob) + return + for(var/datum/atom_hud/hud as anything in GLOB.huds) + hud.hide_single_atomhud_from(viewer, target_mob) + +/datum/scrying_immunity_mask/proc/unhide_data_huds(mob/living/viewer, mob/living/target_mob) + if(!viewer || !target_mob) + return + for(var/datum/atom_hud/hud as anything in GLOB.huds) + hud.unhide_single_atomhud_from(viewer, target_mob) + +/datum/scrying_immunity_mask/proc/examine_name_override(datum/source, mob/living/examined, visible_name, list/name_override) + SIGNAL_HANDLER + + if(!istype(examined) || !masked_mobs[examined]) + return NONE + + name_override[1] = "Unknown" + return COMPONENT_EXAMINE_NAME_OVERRIDEN + +/datum/scrying_immunity_mask/proc/screentip_name_override(datum/source, list/returned_name, obj/item/held_item, atom/hovered) + SIGNAL_HANDLER + + if(!istype(hovered) || !masked_mobs[hovered]) + return NONE + + returned_name[1] = "Unknown" + return SCREENTIP_NAME_SET + + +/* + Scry eye mob: purely perspective anchor. +*/ +/mob/eye/psyker_scry + name = "scrying eye" + var/datum/weakref/user_ref + var/datum/weakref/target_ref + +/mob/eye/psyker_scry/Destroy() + assign_user(null) + return ..() + +/mob/eye/psyker_scry/proc/assign_user(mob/living/new_user) + var/mob/living/old_user = user_ref?.resolve() + if(old_user) + old_user.reset_perspective(null) + name = initial(src.name) + + user_ref = WEAKREF(new_user) + + if(new_user) + new_user.reset_perspective(src) + name = "Scrying Eye ([new_user.name])" + +/mob/eye/psyker_scry/proc/set_target(atom/movable/target) + target_ref = WEAKREF(target) + +/mob/eye/psyker_scry/proc/setLoc(turf/destination, force_update = FALSE) + if(destination) + forceMove(destination) diff --git a/tgstation.dme b/tgstation.dme index a228400c994cf4..35175245fff1c6 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7461,6 +7461,7 @@ #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\psyker\scrying.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_action.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_power.dm" From ee4002f4eae04ab534095b4c011536aa0e6c353e Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 20 Feb 2026 13:34:15 +0100 Subject: [PATCH 073/212] Finally finishes scrying. Slight mess but I left it as clear as possible. --- .../code/powers/resonant/psyker/scrying.dm | 84 +++++++++++++++++-- 1 file changed, 78 insertions(+), 6 deletions(-) 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 fc0eb3849824ec..0d02c6df195972 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -1,5 +1,5 @@ /* - TODO: Add blood for targeting. + Lets you use blood to scry someone. Really potent for detectives and the likes; but has a massive laundry list of things that disable it. */ /datum/power/psyker_power/scrying @@ -8,7 +8,7 @@ In this state, you use their sight instead of your own; but you cannot see creatures that are immune to magic, scrying; or lack the brain activity required to be detectable (dumb). \ Passively builds up stress. The target sometimes gets preminations to indicate they are watched." - value = 9 + value = 10 priority = POWER_PRIORITY_BASIC action_path = /datum/action/cooldown/power/psyker/scrying @@ -18,6 +18,7 @@ button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "telepathy" click_to_activate = TRUE + target_range = 1 var/atom/movable/scry_target @@ -44,31 +45,96 @@ return TRUE /* - Most of the delegation with scrying is handled by scry_vision + Most of the delegation with scrying is handled by scry_vision. We simply verify here and build the datums used. */ /datum/action/cooldown/power/psyker/scrying/use_action(mob/living/user, atom/target) - if(!isliving(target)) + var/list/dna_samples = get_blood_dna_list_from_target(target) + if(!length(dna_samples)) + to_chat(user, span_warning("You need blood to focus your scrying.")) return FALSE - if(!can_affect_scrying(target)) + // If your list of dna samples has multiples then my man you gotta clean your samples. Chooses a random one. + var/selected_dna = pick(dna_samples) + var/mob/living/chosen_target = find_scry_target_from_dna(selected_dna) + if(!chosen_target) + to_chat(user, span_warning("No mind to link to.")) + return FALSE + + if(!can_affect_scrying(chosen_target)) to_chat(user, span_warning("Your sight cannot find purchase on that mind.")) return FALSE - scry_target = target active = TRUE + scry_target = chosen_target + // We create the new datums which will immedaitely handle their effects. scry_camera = new(user, scry_target, src) scry_vision = new(user) tracker = new(src, user) immunity_mask = new(src, user, scry_camera.scry_eye) immunity_mask.refresh_now() + // Bit of psyker stress on use ontop of the processing cost just to prevent too much screen flicking. + modify_stress(PSYKER_STRESS_MINOR * 2) return TRUE +// Gets DNA from blood +/datum/action/cooldown/power/psyker/scrying/proc/get_blood_dna_list_from_target(atom/target) + if(isnull(target)) + return null + + var/list/dna_list = list() + + if(ismob(target)) + return dna_list + + // Gets dna from a blood decal. + if(istype(target, /obj/effect/decal/cleanable/blood)) + var/list/blood = GET_ATOM_BLOOD_DNA(target) + for(var/dna in blood) + dna_list += dna + return dna_list + + // Gets dna from blood from reagent containers. Note: There's a bug with scraping blood not saving DNA; so if it acts weirds its likely that (as of 20/02/26) + if(istype(target, /obj/item/reagent_containers)) + for(var/datum/reagent/present_reagent as anything in target.reagents?.reagent_list) + if(!istype(present_reagent, /datum/reagent/blood)) + continue + var/blood_dna = present_reagent.data?["blood_DNA"] + if(isnull(blood_dna)) + continue + if(islist(blood_dna)) + for(var/dna in blood_dna) + dna_list += dna + else + dna_list += blood_dna + + // Any non-mob atom with forensics blood on it (e.g. clothes, tools) + var/list/blood = GET_ATOM_BLOOD_DNA(target) + if(length(blood)) + for(var/dna in blood) + dna_list += dna + + return dna_list + +// Checks the blood for a dna match. +/datum/action/cooldown/power/psyker/scrying/proc/find_scry_target_from_dna(selected_dna) + if(!selected_dna) + return null + + for(var/mob/living/target in GLOB.mob_list) + if(isobserver(target)) + continue + var/list/blood_dna = target.get_blood_dna_list() + if(blood_dna && blood_dna[selected_dna]) + return target + return null + /datum/action/cooldown/power/psyker/scrying/Remove(mob/removed_from) end_scrying() return ..() +// called by everything that eneds scrying; removes all the datums and left over signalers. /datum/action/cooldown/power/psyker/scrying/proc/end_scrying() if(!active) return @@ -229,6 +295,12 @@ qdel(src) return + // Random chance for the target to feel a chill down their spine. + if(ismob(current_target)) + var/mob/target_mob = current_target + if(prob((seconds_per_tick / 30) * 100)) + to_chat(target_mob, span_warning("A shudder runs down your spine, as if you're being watched.")) + // Stress over time action.modify_stress(PSYKER_STRESS_MINOR * seconds_per_tick) From e4e97df4bd05d8ee439c0f9d4182ca77b4c3cc43 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 20 Feb 2026 20:04:10 +0100 Subject: [PATCH 074/212] Adds a bit of QOL to scrying. --- .../powers/resonant/psyker/_psyker_action.dm | 4 ---- .../code/powers/resonant/psyker/scrying.dm | 17 +++++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) 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 b274341c9e94d6..5dd6b409144095 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 @@ -49,8 +49,6 @@ // Checks if the target can be affected by mental based psyker stuff, since it has its own litle list of unique immunities. Returns TRUE if the target has nothing that affects mental. /datum/action/cooldown/power/psyker/proc/can_affect_mental(mob/living/target, charge_cost = 0) - if(!target) - return TRUE 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)) @@ -63,8 +61,6 @@ // Checks if the target can be affected by specifically psyker's scrying /datum/action/cooldown/power/psyker/proc/can_affect_scrying(mob/living/target, charge_cost = 0) - if(!target) - return TRUE if(!can_affect_mental(target)) return FALSE if(HAS_TRAIT(target, TRAIT_ANTIRESONANCE_SCRYING)) 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 0d02c6df195972..024ded0716e64e 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -32,10 +32,6 @@ // and Scrying Tracker which basically handels any and all things related to stress gain. var/datum/psyker_scry_tracker/tracker - - // Did our scrying get blocked by something? - var/scry_blocked = FALSE - /datum/action/cooldown/power/psyker/scrying/Trigger(mob/clicker, trigger_flags, atom/target) . = ..() if(active) @@ -74,8 +70,10 @@ immunity_mask = new(src, user, scry_camera.scry_eye) immunity_mask.refresh_now() - // Bit of psyker stress on use ontop of the processing cost just to prevent too much screen flicking. - modify_stress(PSYKER_STRESS_MINOR * 2) + // Bit of psyker stress on use ontop of the processing cost just to prevent too much spam peeking. + modify_stress(PSYKER_STRESS_MINOR * 1.5) + + playsound(launched, 'sound/effects/magic/swap.ogg', 75, TRUE) return TRUE // Gets DNA from blood @@ -301,8 +299,11 @@ if(prob((seconds_per_tick / 30) * 100)) to_chat(target_mob, span_warning("A shudder runs down your spine, as if you're being watched.")) - // Stress over time - action.modify_stress(PSYKER_STRESS_MINOR * seconds_per_tick) + // Applies stress. On the trope of having cripple quirks for psyker, being blind halves your stress upkeep. + if(owner.has_quirk(/datum/quirk/item_quirk/blindness)) + action.modify_stress((PSYKER_STRESS_MINOR * seconds_per_tick) / 2) // handicap discount + else + action.modify_stress(PSYKER_STRESS_MINOR * seconds_per_tick) // normal people cost // Re-apply in case other systems reassert blindness/quirk/etc. if(action.scry_vision) From cc74a5b965aae833cbf5ff99d8268303e870dbf2 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 21 Feb 2026 07:54:01 +0100 Subject: [PATCH 075/212] Makes stress recovery more intuitive rather than this funny curve. Fixes a compile error with scrying. --- code/__DEFINES/~doppler_defines/powers.dm | 2 +- .../code/powers/resonant/psyker/_psyker_organ.dm | 8 +++++--- .../modular_powers/code/powers/resonant/psyker/scrying.dm | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 19983e21faf2d1..5c00f7c2beec5b 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -150,7 +150,7 @@ #define PSYKER_STRESS_STANDARD_THRESHOLD 100 // Standard stress recovery per second before modifiers. -#define PSYKER_STRESS_RECOVERY 1.1 +#define PSYKER_STRESS_RECOVERY 1 // How much meditate recovers. #define PSYKER_STRESS_MEDITATION_POWER 10 diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index 7fcc8ebfd759dc..a279e2cc45608e 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -40,18 +40,20 @@ stress = 0 return var/stress_to_recover = recovery_per_second - // Harder to recover at higher stress - stress_to_recover -= (stress * 0.01) - // Organ damage makes recovery worse stress_to_recover -= (damage * 0.015) + // Can't recover stress while at high stress. + if(stress => PSYKER_STRESS_STANDARD_THRESHOLD) + stress_to_recover = 0 + // Don’t let recovery go negative (would increase stress) stress_to_recover = max(stress_to_recover, 0) // Apply recovery, don't let it send stress into the negatives. stress = max(stress - stress_to_recover * seconds_per_tick, 0) + // Check if we do stress backlash after stress reduction. if(stress >= (stress_threshold * 2)) // Catastrophic event. stress_backlash(PSYKER_EVENT_TIER_CATASTROPHIC) 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 024ded0716e64e..f99b539a6d6273 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -73,7 +73,7 @@ // Bit of psyker stress on use ontop of the processing cost just to prevent too much spam peeking. modify_stress(PSYKER_STRESS_MINOR * 1.5) - playsound(launched, 'sound/effects/magic/swap.ogg', 75, TRUE) + playsound(user, 'sound/effects/magic/swap.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) return TRUE // Gets DNA from blood From 6bd2da8274ac3be9bfc719c5f553b35580d51095 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 21 Feb 2026 08:30:56 +0100 Subject: [PATCH 076/212] Slightly nerfs scrying by making everyone an unidentifyable silhouette. Reduces stress upkeep as a consequence. --- .../powers/resonant/psyker/_psyker_organ.dm | 2 +- .../code/powers/resonant/psyker/scrying.dm | 76 ++++++++++++++----- 2 files changed, 59 insertions(+), 19 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index a279e2cc45608e..e1878f359f1686 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -44,7 +44,7 @@ stress_to_recover -= (damage * 0.015) // Can't recover stress while at high stress. - if(stress => PSYKER_STRESS_STANDARD_THRESHOLD) + if(stress >= PSYKER_STRESS_STANDARD_THRESHOLD) stress_to_recover = 0 // Don’t let recovery go negative (would increase stress) 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 f99b539a6d6273..84ab988ab423ae 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -27,9 +27,9 @@ var/datum/scrying_camera/scry_camera // Scrying Vision which handles vision traits on the user. var/datum/scrying_vision/scry_vision - // Scrying Immunity Mask which takes care of hiding creatures immune from scrying + // Scrying Immunity Mask which hides people into indistinct overlays. var/datum/scrying_immunity_mask/immunity_mask - // and Scrying Tracker which basically handels any and all things related to stress gain. + // and Scrying Tracker which basically handles any and all things related to stress gain. var/datum/psyker_scry_tracker/tracker /datum/action/cooldown/power/psyker/scrying/Trigger(mob/clicker, trigger_flags, atom/target) @@ -74,8 +74,29 @@ modify_stress(PSYKER_STRESS_MINOR * 1.5) playsound(user, 'sound/effects/magic/swap.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + + // Adds listeners for dispelling on the target + RegisterSignal(scry_target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) return TRUE +// Dispel functionality +/datum/action/cooldown/power/psyker/scrying/Grant(mob/granted_to) + . = ..() + if(resonant) + RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + +/datum/action/cooldown/power/psyker/scrying/Remove(mob/removed_from) + . = ..() + if(resonant) + UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL) + end_scrying() + +/datum/action/cooldown/power/psyker/scrying/proc/on_dispel(mob/owner, atom/dispeller) + SIGNAL_HANDLER + if(active) + to_chat(owner, span_warning("Your scrying link was cut off!")) + end_scrying() + // Gets DNA from blood /datum/action/cooldown/power/psyker/scrying/proc/get_blood_dna_list_from_target(atom/target) if(isnull(target)) @@ -128,10 +149,6 @@ return target return null -/datum/action/cooldown/power/psyker/scrying/Remove(mob/removed_from) - end_scrying() - return ..() - // called by everything that eneds scrying; removes all the datums and left over signalers. /datum/action/cooldown/power/psyker/scrying/proc/end_scrying() if(!active) @@ -144,8 +161,13 @@ QDEL_NULL(scry_camera) QDEL_NULL(immunity_mask) + // removes dispel signal from target + UnregisterSignal(scry_target, COMSIG_ATOM_DISPEL) + scry_target = null + + /* We bypass our own vision traits and see the world from the target's pov. Handles the removal of vision traits and the application of the overlay. @@ -301,9 +323,9 @@ // Applies stress. On the trope of having cripple quirks for psyker, being blind halves your stress upkeep. if(owner.has_quirk(/datum/quirk/item_quirk/blindness)) - action.modify_stress((PSYKER_STRESS_MINOR * seconds_per_tick) / 2) // handicap discount + action.modify_stress((PSYKER_STRESS_MINOR * seconds_per_tick) / 4) // handicap discount else - action.modify_stress(PSYKER_STRESS_MINOR * seconds_per_tick) // normal people cost + action.modify_stress((PSYKER_STRESS_MINOR * seconds_per_tick) / 2) // normal people cost // Re-apply in case other systems reassert blindness/quirk/etc. if(action.scry_vision) @@ -362,27 +384,31 @@ update_masks(viewer, eye, action) /datum/scrying_immunity_mask/proc/update_masks(mob/living/viewer, mob/eye/psyker_scry/eye, datum/action/cooldown/power/psyker/scrying/action) - // Determine what mobs are currently "in view" of the scry eye. - // Use view() around the eye, not around the viewer. var/list/current_mobs = list() for(var/mob/living/seen_mob in view(viewer.client.view, eye)) current_mobs += seen_mob // Remove masks for mobs no longer in view (or deleted) for(var/mob/living/masked_mob as anything in masked_mobs.Copy()) - if(QDELETED(masked_mob) || !(masked_mob in current_mobs) || action.can_affect_scrying(masked_mob)) + if(QDELETED(masked_mob) || !(masked_mob in current_mobs)) unmask_mob(viewer, masked_mob) - // Apply masks for newly seen immune mobs (excluding the direct target if you want) + // Apply masks for newly seen mobs (baseline: everyone) for(var/mob/living/seen_mob as anything in current_mobs) - if(seen_mob == action.scry_target) - continue - if(masked_mobs[seen_mob]) + update_silhouette_dir(seen_mob) continue - if(!action.can_affect_scrying(seen_mob)) - mask_mob(viewer, seen_mob) + mask_mob(viewer, seen_mob) + +// makes the silhouettes directional +/datum/scrying_immunity_mask/proc/update_silhouette_dir(mob/living/target_mob) + var/list/entry = masked_mobs[target_mob] + if(!entry) + return + var/image/silhouette_image = entry[2] + if(silhouette_image) + silhouette_image.dir = target_mob.dir /datum/scrying_immunity_mask/proc/mask_mob(mob/living/viewer, mob/living/target_mob) if(!viewer?.client || QDELETED(target_mob)) @@ -394,9 +420,20 @@ hide_image.override = TRUE hide_image.alpha = 0 + // Silhouette marker, anchored to the mob so it follows movement + var/image/silhouette_image = image('icons/effects/effects.dmi', target_mob, "blank") + silhouette_image.override = FALSE + silhouette_image.layer = ABOVE_MOB_LAYER + silhouette_image.plane = GAME_PLANE + silhouette_image.appearance_flags = RESET_ALPHA | RESET_COLOR | RESET_TRANSFORM + silhouette_image.dir = target_mob.dir + viewer.client.images += hide_image + viewer.client.images += silhouette_image + + masked_mobs[target_mob] = list(hide_image, silhouette_image) - masked_mobs[target_mob] = list(hide_image) + // Keep your existing “don’t leak info” hooks RegisterSignal(target_mob, COMSIG_ATOM_EXAMINE, PROC_REF(on_target_examine)) hide_data_huds(viewer, target_mob) @@ -406,10 +443,13 @@ return var/image/hide_image = entry[1] + var/image/silhouette_image = entry[2] if(viewer?.client) if(hide_image) viewer.client.images -= hide_image + if(silhouette_image) + viewer.client.images -= silhouette_image UnregisterSignal(target_mob, COMSIG_ATOM_EXAMINE) unhide_data_huds(viewer, target_mob) From fdb59fa843efba125d41c8dbc30e0cc3877a3f9f Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 21 Feb 2026 08:38:09 +0100 Subject: [PATCH 077/212] flavor text on scrying --- .../modular_powers/code/powers/resonant/psyker/scrying.dm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 84ab988ab423ae..c766a5e5007b8f 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -4,8 +4,8 @@ /datum/power/psyker_power/scrying name = "Scrying" - desc = "Using a sample of a creature's blood, you can see the world through their eyes remotely. \ - In this state, you use their sight instead of your own; but you cannot see creatures that are immune to magic, scrying; or lack the brain activity required to be detectable (dumb). \ + desc = "Using a sample of a creature's blood, you can see the world through their eyes remotely. Creatures will be vague and hard to distinguish, but their environment will appear clear. \ + In this state, you use their sight instead of your own; but you cannot target creatures that are immune to magic, scrying; or lack the brain activity required to be detectable (dumb). \ Passively builds up stress. The target sometimes gets preminations to indicate they are watched." value = 10 @@ -36,7 +36,7 @@ . = ..() if(active) end_scrying() - to_chat(owner, span_notice("You return your senses to your mind.")) + to_chat(owner, span_notice("Your sight returns as you focus back on your own mind.")) return FALSE return TRUE From d6720e208aa9448ce849c04e9816ca045c51e66e Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 21 Feb 2026 09:36:40 +0100 Subject: [PATCH 078/212] Adds cargo crate punting. --- .../code/powers/mortal/expert/punt.dm | 77 +++++++++++++++++++ .../code/powers/mortal/expert/strider.dm | 2 +- .../code/powers/resonant/psyker/scrying.dm | 8 +- tgstation.dme | 1 + 4 files changed, 83 insertions(+), 5 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm new file mode 100644 index 00000000000000..4aed9293dbb9e2 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm @@ -0,0 +1,77 @@ +/* + Kicks an item horizontally/vertically/diagonally in a straight line. Dense objects stun and damage on impact, otherwise acts as a throw. + Inspired by the pent-up frustrations of several Cargo members on Doppler. + Scales with Athletics. Hit the bar so you can hit them with their MAIL. +*/ + +/datum/power/expert/punt + name = "Punt" + desc = "Using your foot or some other part of your body, you send an object barreling down a long distance away from you. If someone is hit by the object and it is solid, they are knocked down and take damage. \ + Distance (and damage) scale with your Athletics skill. Double distance on crates and non-bulky objects! Requires Heavy Lifter." + value = 3 + required_powers = list(/datum/power/expert/heavy_lifter) + action_path = /datum/action/cooldown/power/expert/punt + +/datum/action/cooldown/power/expert/punt + name = "Punt" + desc = "Using your foot or some other part of your body, you send an object barreling down a long distance away from you. If someone is hit by the object and it is solid, they are knocked down and take damage. \ + Distance (and damage) scale with your Athletics skill. Double distance on crates and non-bulky objects!" + button_icon = 'icons/mob/actions/actions_elites.dmi' // another placeholder + button_icon_state = "herald_teleshot" + + target_type = /obj/ + target_range = 1 + click_to_activate = TRUE + cooldown_time = 10 + + // The base distance we punt. Keep in mind this is without the athletics bonus (they'll at least be journeyman so +2) + var/base_range = 1 + // how much damage punt impact does if its a solid object. + var/base_damage = 10 + +/datum/action/cooldown/power/expert/punt/use_action(mob/living/user, obj/target) + if(!target || target.anchored || !isturf(target.loc)) + user.balloon_alert(user, "can't move that!") + return FALSE + + // Half your athletics skill rounded is added to the distance + var/athletics = round((user.mind?.get_skill_level(/datum/skill/athletics) || 0) / 2) + + var/range = base_range + athletics + + // items that are normal or smaller, or if its a crate (cargo rejoice), get punted twice as far. + if(istype(target, /obj/structure/closet/crate)) + range *= 2 + if(isitem(target)) + var/obj/item/target_item = target + if(target_item.w_class <= WEIGHT_CLASS_NORMAL) + range *= 2 + + var/dir = get_dir(user, target) + user.setDir(dir) + var/turf/target_turf = get_ranged_target_turf(target, dir, range) + + RegisterSignal(target, COMSIG_MOVABLE_IMPACT, PROC_REF(punt_impact)) + playsound(user, 'sound/effects/meteorimpact.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + target.throw_at(target_turf, range = range, speed = target.density ? 3 : 4, thrower = user, spin = isitem(target)) + return TRUE + +/datum/action/cooldown/power/expert/punt/proc/punt_impact(atom/movable/source, atom/hit_atom, datum/thrownthing/thrownthing) + SIGNAL_HANDLER + UnregisterSignal(source, COMSIG_MOVABLE_IMPACT) + + // Base damage + athletics skill level * 2 (journeyman = 4*2=8) + var/damage = base_damage + round((owner.mind?.get_skill_level(/datum/skill/athletics) || 0) * 2) + // Dense objects are treated as damaging projectiles. + if(source.density) + if(isliving(hit_atom)) // if you manage to line up the shot you deserve this + var/mob/living/living_atom = hit_atom + living_atom.apply_damage(damage, BRUTE) + living_atom.Knockdown(2 SECONDS) + playsound(living_atom, 'sound/items/lead_pipe_hit.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) // I am not sorry for this choice in sound effect + else if(hit_atom.uses_integrity) // sorry about the window ma'am + hit_atom.take_damage(damage, BRUTE, MELEE) + + // Items landing in trash bins should go inside + if(isitem(source) && istype(hit_atom, /obj/structure/closet/crate/bin)) + source.forceMove(hit_atom) 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 f5129185d81bb2..bd96c124de4993 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm @@ -4,7 +4,7 @@ /datum/power/expert/strider name = "Strider" - desc = "You're one hell of a hunk of a man. You ignore all slowdowns from held & worn items. \ + desc = "Your strength is herculean. You ignore all slowdowns from held & worn items. \ You also start out at Master proficiency athletics." value = 6 required_powers = list(/datum/power/expert/heavy_lifter) 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 c766a5e5007b8f..9f60d7e8762d84 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -15,8 +15,8 @@ /datum/action/cooldown/power/psyker/scrying name = "Scrying" desc = "Using a sample of a creature's blood, you can see the world through their eyes remotely." - button_icon = 'icons/mob/actions/actions_spells.dmi' - button_icon_state = "telepathy" + button_icon = 'icons/mob/actions/actions_animal.dmi' + button_icon_state = "gaze" click_to_activate = TRUE target_range = 1 @@ -33,11 +33,11 @@ var/datum/psyker_scry_tracker/tracker /datum/action/cooldown/power/psyker/scrying/Trigger(mob/clicker, trigger_flags, atom/target) - . = ..() if(active) end_scrying() to_chat(owner, span_notice("Your sight returns as you focus back on your own mind.")) - return FALSE + else + . = ..() return TRUE /* diff --git a/tgstation.dme b/tgstation.dme index 35175245fff1c6..3fe7069eb97a9a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7436,6 +7436,7 @@ #include "modular_doppler\modular_powers\code\powers\mortal\expert\heavy_lifter.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\punt.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\rich.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\strider.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\zoologist.dm" From 578c5f5b2b503ea62fe0e39a87bca52fbd420595 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 21 Feb 2026 10:23:50 +0100 Subject: [PATCH 079/212] Adds a fun interaction if you land a max distance punt on legendary. --- .../code/powers/mortal/expert/punt.dm | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm index 4aed9293dbb9e2..09eb1b208a4653 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm @@ -27,7 +27,7 @@ // The base distance we punt. Keep in mind this is without the athletics bonus (they'll at least be journeyman so +2) var/base_range = 1 // how much damage punt impact does if its a solid object. - var/base_damage = 10 + var/base_damage = 5 /datum/action/cooldown/power/expert/punt/use_action(mob/living/user, obj/target) if(!target || target.anchored || !isturf(target.loc)) @@ -46,13 +46,16 @@ var/obj/item/target_item = target if(target_item.w_class <= WEIGHT_CLASS_NORMAL) range *= 2 + // If we're legendary we get a bit more throw distance; enough to be able to offscreen people. + if(user.mind?.get_skill_level(/datum/skill/athletics) >= SKILL_LEVEL_LEGENDARY) + range += 2 var/dir = get_dir(user, target) user.setDir(dir) var/turf/target_turf = get_ranged_target_turf(target, dir, range) RegisterSignal(target, COMSIG_MOVABLE_IMPACT, PROC_REF(punt_impact)) - playsound(user, 'sound/effects/meteorimpact.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + playsound(user, 'sound/effects/meteorimpact.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) target.throw_at(target_turf, range = range, speed = target.density ? 3 : 4, thrower = user, spin = isitem(target)) return TRUE @@ -68,7 +71,12 @@ var/mob/living/living_atom = hit_atom living_atom.apply_damage(damage, BRUTE) living_atom.Knockdown(2 SECONDS) - playsound(living_atom, 'sound/items/lead_pipe_hit.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) // I am not sorry for this choice in sound effect + playsound(living_atom, 'sound/items/lead_pipe_hit.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) // I am not sorry for this choice in sound effect + + var/mob/thrower = thrownthing?.get_thrower() || owner + if(!thrower || get_dist(thrower, hit_atom) >= 12) //if you hit someone offscreen, which can't be done without legendary. + thrower.playsound_local(thrower, 'sound/items/weapons/homerun.ogg', 75) + to_chat(thrower, span_boldnotice("You can't see it, but you've got a hunch you just hit a fantastic shot.")) else if(hit_atom.uses_integrity) // sorry about the window ma'am hit_atom.take_damage(damage, BRUTE, MELEE) From 8046b9560cf3acdf0a2b461803837ee36798a3c5 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 21 Feb 2026 17:35:48 +0100 Subject: [PATCH 080/212] Adds alignment mechanics to cultivator. Tweaks meditate to work across psyker and cultivator. Once again tweaks punt. --- code/__DEFINES/~doppler_defines/powers.dm | 17 ++ .../code/powers/mortal/expert/punt.dm | 13 +- .../resonant/cultivator/_cultivator_action.dm | 52 +++++ .../cultivator/_cultivator_alignment.dm | 189 ++++++++++++++++++ .../cultivator/_cultivator_dantian.dm | 87 ++++++++ .../resonant/cultivator/_cultivator_power.dm | 8 + .../resonant/cultivator/_cultivator_root.dm | 19 ++ .../resonant/cultivator/astraltouched_root.dm | 20 ++ .../code/powers/resonant/meditate.dm | 23 ++- tgstation.dme | 6 + 10 files changed, 419 insertions(+), 15 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_action.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_power.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 5c00f7c2beec5b..bddc048e7c2da0 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -141,6 +141,23 @@ /// Trait held by all under the resonant archetype. #define TRAIT_ARCHETYPE_RESONANT "archetype_resonant" +/** + * RESONANT: CULTIVATOR + * All defines related to the cultivator powers. + */ + +// Maximum amount of Dantian we can have. +#define CULTIVATOR_DANTIAN_MAX 1000 + +// How much dantian we get from meditation every 2.5 seconds +#define CULTIVATOR_DANTIAN_MEDITATION_POWER 4 + +// UI location of the Cultivator element +#define CULTIVATOR_UI_SCREEN_LOC "WEST,CENTER-2:20" + +// Bonus damage on strikes done by alignment +#define CULTIVATOR_ALIGNMENT_DAMAGE_BONUS 10 + /** * RESONANT: PSYKER * All defines related to the enigmatist powers. diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm index 09eb1b208a4653..d131344074d876 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm @@ -14,7 +14,7 @@ /datum/action/cooldown/power/expert/punt name = "Punt" - desc = "Using your foot or some other part of your body, you send an object barreling down a long distance away from you. If someone is hit by the object and it is solid, they are knocked down and take damage. \ + desc = "You send an object barreling down a long distance away from you. If someone is hit by the object and it is solid, they are knocked down and take damage. \ Distance (and damage) scale with your Athletics skill. Double distance on crates and non-bulky objects!" button_icon = 'icons/mob/actions/actions_elites.dmi' // another placeholder button_icon_state = "herald_teleshot" @@ -69,17 +69,18 @@ if(source.density) if(isliving(hit_atom)) // if you manage to line up the shot you deserve this var/mob/living/living_atom = hit_atom + var/mob/thrower = thrownthing?.get_thrower() || owner + living_atom.apply_damage(damage, BRUTE) living_atom.Knockdown(2 SECONDS) playsound(living_atom, 'sound/items/lead_pipe_hit.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) // I am not sorry for this choice in sound effect - var/mob/thrower = thrownthing?.get_thrower() || owner + // logging + living_atom.log_message("was punted by an object from [thrower] for [damage] damage.") + thrower.log_message("punted an object at [living_atom] for [damage] damage.") + if(!thrower || get_dist(thrower, hit_atom) >= 12) //if you hit someone offscreen, which can't be done without legendary. thrower.playsound_local(thrower, 'sound/items/weapons/homerun.ogg', 75) to_chat(thrower, span_boldnotice("You can't see it, but you've got a hunch you just hit a fantastic shot.")) else if(hit_atom.uses_integrity) // sorry about the window ma'am hit_atom.take_damage(damage, BRUTE, MELEE) - - // Items landing in trash bins should go inside - if(isitem(source) && istype(hit_atom, /obj/structure/closet/crate/bin)) - source.forceMove(hit_atom) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_action.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_action.dm new file mode 100644 index 00000000000000..982d529a28605e --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_action.dm @@ -0,0 +1,52 @@ +/datum/action/cooldown/power/cultivator + name = "abstract cultivator power action - ahelp this" + background_icon_state = "bg_star" + overlay_icon_state = "ab_goldborder" + button_icon = 'icons/mob/actions/backgrounds.dmi' + + // The organ that processes most of the Psyker Powers. Mostly all functions here communicate with this. + var/datum/component/cultivator_dantian/dantian_component + + // The UI used for dantian.alist + var/atom/movable/screen/cultivator_dantian/cultivator_ui + + // Cost in Dantian to use + var/cost + +/datum/action/cooldown/power/cultivator/New() + . = ..() + ValidateDantianComponent() + +// Since Cultivator has multiple roots and a persistent resource system, we use a component for handling Dantian +/datum/action/cooldown/power/cultivator/proc/ValidateDantianComponent() + if(owner) // Prevents runtiming on start + var/mob/living/carrier = owner + dantian_component = carrier.GetComponent(/datum/component/cultivator_dantian) + if(!dantian_component) + return FALSE + return TRUE + +// Validation handled in the dantian component. +/datum/action/cooldown/power/cultivator/proc/adjust_dantian(amount, override_cap) + dantian_component.adjust_dantian(amount, override_cap) + +//Easy access to dantian +/datum/action/cooldown/power/cultivator/proc/get_dantian() + return dantian_component.dantian + +// We check to see if our dantian component is actually there, because usually things will go bad if they don't. +/datum/action/cooldown/power/cultivator/try_use(mob/living/user, mob/living/target) + if(!ValidateDantianComponent()) + owner.balloon_alert(owner, "Yell at the coders; you're missing your dantian system!") + return FALSE + if(dantian_component.dantian < cost) + user.balloon_alert(user, "needs [cost] dantian!") + return FALSE + . = .. () + +// Make sure the cost gets deducted after using the power (we already checked if we can afford it) +/datum/action/cooldown/power/cultivator/on_action_success(mob/living/user, atom/target) + if(cost) + adjust_dantian(-cost) + return + diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm new file mode 100644 index 00000000000000..00d51b04a62255 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm @@ -0,0 +1,189 @@ +/* + Because Cultivator's alignments have a consistent throughline fo behavior, the alignment powers are subtyped like so. + Set up to be modular and also very VV-able; because its cool if some event antag shows up in a color that nobody knows. +*/ +/datum/action/cooldown/power/cultivator/alignment + name = "abstract alignment" + + // The overlay effect for the alignment. + var/alignment_outline_size = 2 + // The overlay color for alignment, if it has one + var/alignment_outline_color = "#66d5dd" + // the name for the filter (dont nede to change this) + var/filter_id = "alignment_outline" + + // light object for the alignment + var/obj/effect/dummy/lighting_obj/moblight/alignment_light + // sounds to play when actvating alignment + var/alignment_activation_sound = 'sound/effects/magic/lightningbolt.ogg' + + // Mutable appearance stuff for the overlay. + var/mutable_appearance/alignment_overlay + var/alignment_overlay_icon = 'icons/effects/effects.dmi' + var/alignment_overlay_state = "lightning" + var/alignment_overlay_layer = ABOVE_MOB_LAYER + + // the armor datum given when in alignment + var/datum/armor/alignment_defense = /datum/armor/alignment_unarmored_defense + // the armor datum we actually add after comparing current armor against alignment_defense + var/datum/armor/alignment_added_armor + // The damage type for the alignment + var/alignment_damage_type = BRUTE + // The bonus damage for the alignment + var/alignment_damage_bonus = CULTIVATOR_ALIGNMENT_DAMAGE_BONUS + + cooldown_time = 5 // to prevent spam-clicking it off + +// Removes stray listeners. +/datum/action/cooldown/power/cultivator/alignment/Destroy() + . = ..() + if(owner) + UnregisterSignal(owner, list(COMSIG_HUMAN_UNARMED_HIT, COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM)) + remove_alignment_armor() + +// The proc for onhit. Override as desired. +/datum/action/cooldown/power/cultivator/alignment/proc/on_unarmed_hit(mob/living/user, mob/living/target, obj/item/bodypart/affecting, damage, armor_block, limb_accuracy, limb_sharpness) + SIGNAL_HANDLER + if(!active) + return + if(alignment_damage_bonus) + target.apply_damage(alignment_damage_bonus, alignment_damage_type, affecting, armor_block, sharpness = limb_sharpness) + +// Basically handles active state and activation fx. Override as needed; but please make sure to get the essentials. +/datum/action/cooldown/power/cultivator/alignment/use_action(mob/living/carbon/user) + if(!active) // If inactive, we activate (if we can pay the cost) + enable_alignment(user) + active = TRUE + return TRUE + if(active) // If active, we disable. + disable_alignment(user) + active = FALSE + return TRUE + return FALSE + +// COOL effects to show your AURA. +/datum/action/cooldown/power/cultivator/alignment/proc/activation_fx(mob/living/carbon/user, atom/target) + if(isnull(alignment_outline_color) && isnull(alignment_outline_size)) + return + // Adds the color effects + user.remove_filter(filter_id) + user.add_filter(filter_id, 2, outline_filter(size = alignment_outline_size, color = alignment_outline_color)) + + var/filter = user.get_filter(filter_id) + if(filter) + animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1) + animate(alpha = 40, time = 2.5 SECONDS) + + // Adds the glowing light. + QDEL_NULL(alignment_light) + alignment_light = user.mob_light( + range = 3, + power = 1, + color = alignment_outline_color + ) + // adds overlay + if(!alignment_overlay) + alignment_overlay = mutable_appearance(alignment_overlay_icon, alignment_overlay_state, alignment_overlay_layer) + alignment_overlay.color = alignment_outline_color + user.add_overlay(alignment_overlay) + + // plays sound + playsound(owner, alignment_activation_sound, 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + +// Everything that needs to happen when enabling alignment +/datum/action/cooldown/power/cultivator/alignment/proc/enable_alignment(mob/living/carbon/user) + activation_fx(user) + RegisterSignal(user, COMSIG_HUMAN_UNARMED_HIT, PROC_REF(on_unarmed_hit)) + RegisterSignal(user, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_equipment_changed)) + RegisterSignal(user, COMSIG_MOB_UNEQUIPPED_ITEM, PROC_REF(on_equipment_changed)) + recompute_alignment_armor(user) + return TRUE + +// Everything that needs to happen when disabling alignment +/datum/action/cooldown/power/cultivator/alignment/proc/disable_alignment(mob/living/carbon/user) + if(alignment_overlay) + user.cut_overlay(alignment_overlay) + UnregisterSignal(user, list(COMSIG_HUMAN_UNARMED_HIT, COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM)) + user.remove_filter(filter_id) + remove_alignment_armor() + QDEL_NULL(alignment_light) + return TRUE + +/* + Below is the big scary block of 'how to maths out other armor' + Because we want armor to be a minimum of 40, we need to COMPARE it against the current armor and change the armor values on the fly. + This is called when either A. you activate the alignment or B. equip/unequip stuff. + Other armor applies globally, so wearing a really good helmet/chest will still disable the alignment damage bonus for other slots that may be uncovered. +*/ + +/datum/action/cooldown/power/cultivator/alignment/proc/on_equipment_changed(datum/source, obj/item/item, slot) + SIGNAL_HANDLER + if(!active) + return + recompute_alignment_armor(source) + +// The builder that actually applies the maths from calc_needed_internal_armor and applies it. +/datum/action/cooldown/power/cultivator/alignment/proc/recompute_alignment_armor(mob/living/carbon/user) + if(!ishuman(user)) + return + var/mob/living/carbon/human/human_user = user + + remove_alignment_armor() + + var/datum/armor/target_armor = alignment_defense + if(ispath(target_armor)) + target_armor = get_armor_by_type(target_armor) + + var/list/add_values = list() + for(var/armor_type in ARMOR_LIST_ALL()) + var/target_total = target_armor.get_rating(armor_type) + var/needed = calc_needed_internal_armor(human_user, armor_type, target_total) + if(needed > 0) + add_values[armor_type] = needed + + if(LAZYLEN(add_values)) + var/datum/armor/base_armor = new /datum/armor + alignment_added_armor = base_armor.generate_new_with_specific(add_values) + human_user.physiology.armor = human_user.physiology.armor.add_other_armor(alignment_added_armor) + +// Compares the user's current worn armor against the armor from alignment_defense and returns the difference, to ensure we don't stack alignment armor past 40 armor. +/datum/action/cooldown/power/cultivator/alignment/proc/calc_needed_internal_armor(mob/living/carbon/human/human_target, armor_type, target_total) + var/list/covering_clothing = list( + human_target.head, human_target.wear_mask, human_target.wear_suit, human_target.w_uniform, human_target.back, human_target.gloves, human_target.shoes, human_target.belt, human_target.s_store, human_target.glasses, human_target.ears, human_target.wear_id, human_target.wear_neck) + + var/clothing_multiplier = 1.0 + for(var/obj/item/clothing/clothing_item in covering_clothing) + if(!clothing_item) + continue + var/clothing_rating = min(clothing_item.get_armor_rating(armor_type), 100) + clothing_multiplier *= (100 - clothing_rating) * 0.01 + + var/current_internal = human_target.physiology.armor.get_rating(armor_type) + var/current_total = human_target.getarmor(null, armor_type) + if(current_total >= target_total) + return 0 + + var/required_internal = 100 * (1 - (1 - target_total / 100) / max(clothing_multiplier, 0.0001)) + return max(0, required_internal - current_internal) + +/datum/action/cooldown/power/cultivator/alignment/proc/remove_alignment_armor() + if(!alignment_added_armor || !ishuman(owner)) + alignment_added_armor = null + return + var/mob/living/carbon/human/human_owner = owner + human_owner.physiology.armor = human_owner.physiology.armor.subtract_other_armor(alignment_added_armor) + alignment_added_armor = null + +// base armor for alignment powers. +/datum/armor/alignment_unarmored_defense + acid = 40 + bio = 40 + melee = 40 + bullet = 40 + bomb = 40 + energy = 40 + laser = 40 + fire = 40 + melee = 40 + wound = 40 + diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm new file mode 100644 index 00000000000000..8566a20dffeba4 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm @@ -0,0 +1,87 @@ +/// Helper to format the text that gets thrown onto the dantian hud element. +#define FORMAT_DANTIAN_TEXT(charges) MAPTEXT("
[round(charges)]
") + +/datum/component/cultivator_dantian + dupe_mode = COMPONENT_DUPE_UNIQUE + + // The mob we’re attached to is always `parent`. + var/mob/living/attached_mob + + // Whatever state your old attached_cultivator_dantian tracked: + var/dantian = 0 + var/max_dantian = CULTIVATOR_DANTIAN_MAX + + // The UI itself + var/atom/movable/screen/cultivator_dantian/cultivator_ui + +/datum/component/cultivator_dantian/Initialize() + . = ..() + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + attached_mob = parent + + RegisterWithParent() + +/datum/component/cultivator_dantian/RegisterWithParent() + . = ..() + if(attached_mob.hud_used) + install_dantian_hud(parent) + else + RegisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created)) + +/datum/component/cultivator_dantian/UnregisterFromParent() + // UnregisterSignal(attached_mob, list(COMSIG_..., COMSIG_...)) + . = ..() + if(attached_mob) // prevents runtiming when adding/removing duplicate components + UnregisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED) + +/datum/component/cultivator_dantian/Destroy() + UnregisterFromParent() + + if(!attached_mob) + return + + if(attached_mob.hud_used && cultivator_ui) + attached_mob.hud_used.infodisplay -= cultivator_ui + qdel(cultivator_ui) + cultivator_ui = null + + attached_mob = null + return ..() + +/datum/component/cultivator_dantian/proc/on_hud_created(datum/source) + SIGNAL_HANDLER + + var/mob/living/living_holder = attached_mob + if(!living_holder || !living_holder.hud_used) + return + + install_dantian_hud(living_holder) + +/datum/component/cultivator_dantian/proc/install_dantian_hud(mob/living/living_holder) + if(cultivator_ui) // already installed + return + + var/datum/hud/hud_used = living_holder.hud_used + cultivator_ui = new /atom/movable/screen/cultivator_dantian(null, hud_used) + hud_used.infodisplay += cultivator_ui + + // Set initial text so it isn't blank until first adjust. + cultivator_ui.maptext = FORMAT_DANTIAN_TEXT(dantian) + + hud_used.show_hud(hud_used.hud_version) + +/datum/component/cultivator_dantian/proc/adjust_dantian(amount, override_cap) + if(!isnum(amount)) + return + var/cap_to = isnum(override_cap) ? override_cap : max_dantian + dantian = clamp(dantian + amount, 0, cap_to) + + cultivator_ui?.maptext = FORMAT_DANTIAN_TEXT(dantian) + +// UI Elements for dantian +/atom/movable/screen/cultivator_dantian + name = "dantian" + icon = 'icons/hud/blob.dmi' // TODO: Get sprites/UI for this. + icon_state = "block" + screen_loc = CULTIVATOR_UI_SCREEN_LOC diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_power.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_power.dm new file mode 100644 index 00000000000000..1b6b67d3de7011 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_power.dm @@ -0,0 +1,8 @@ +/datum/power/cultivator + name = "Cultivator Power" + desc = "You're not meant to look at the roots in the ground; you're meant to look at the tree that sprouts from it. Stop staring at abstract types!" + + archetype = POWER_ARCHETYPE_RESONANT + path = POWER_PATH_CULTIVATOR + priority = POWER_PRIORITY_BASIC + abstract_parent_type = /datum/power/cultivator diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm new file mode 100644 index 00000000000000..a9d74a8de49737 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm @@ -0,0 +1,19 @@ +/datum/power/cultivator_root + name = "Abstract cultivator root" + desc = "For decades, I have honed my body, my skill. Like calligraphists have mastered the stroke of a brush, I have mastered the brush of my hand along the keyboard. \ + Lines upon lines of code created at but the single flick of the wrist. So know what is good with you; and report this abstract root." + abstract_parent_type = /datum/power/cultivator_root + + mob_trait = TRAIT_ARCHETYPE_RESONANT + archetype = POWER_ARCHETYPE_RESONANT + path = POWER_PATH_CULTIVATOR + priority = POWER_PRIORITY_ROOT + +/datum/power/cultivator_root/post_add() // I'd love to run this during add but that runtimes at round start. + if(!power_holder) // So it doesn't runtime at init + return + // We pass along the piety component to actually handle most of the piety stuff. + power_holder.AddComponent(/datum/component/cultivator_dantian, power_holder) + // Passes along meditation. + grant_action(/datum/action/cooldown/power/resonant_meditate) + . = ..() diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm new file mode 100644 index 00000000000000..e221becb1aa381 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -0,0 +1,20 @@ +/datum/power/cultivator_root/astral_touched + name = "Astral-Touched Alignment" + desc = "You gain your Dantian's aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do 10 extra burn damage.\ + Passively, your cold temprature and pressure threshold is increased by 30C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space (though you still need to breathe).\ + You gain armor IV across your whole body. Has diminishing effects with your worn armor." + action_path = /datum/action/cooldown/power/cultivator/alignment/astral_touched + + value = 7 + +/datum/action/cooldown/power/cultivator/alignment/astral_touched + name = "Astral-Touched Alignment" + desc = "Activates your Astral-Touched Alignment aura, granting you immune to cold and pressure, increasing your defenses, and increasing your strength with unarmed attacks." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "transformslime" + + alignment_outline_color = "#66c5dd" + alignment_activation_sound = 'sound/effects/magic/cosmic_energy.ogg' + alignment_overlay_state = "shieldsparkles" + + alignment_damage_type = BURN diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index a8f2e2c68156f5..44a7f5e0355807 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -11,7 +11,7 @@ Reduces stress for psykers and restores Dantian for cultivators // The components responsible for meditation. var/obj/item/organ/resonant/psyker/psyker_organ - var/cultivator_organ //TODO: Cultivator Organ + var/datum/component/cultivator_dantian/cultivator_dantian /datum/action/cooldown/power/resonant_meditate/use_action() . = ..() @@ -19,21 +19,24 @@ Reduces stress for psykers and restores Dantian for cultivators var/mob/living/spotlighttarget = owner // cause we need to call it on a mob/living to_chat(owner, span_notice("You start meditating.")) - update_organs() + // Gets the owner's psyker organ & cultivator component + update_components() // Adds visual effects var/datum/status_effect/spotlight_light/light = get_spotlight_color() spotlighttarget.apply_status_effect(light, 3000) do active = TRUE if(do_after(owner, 25, target = owner)) - if(!psyker_organ) + if(!psyker_organ && cultivator_dantian) to_chat(owner, span_notice("I have nothing to meditate on!")) - keep_going = FALSE if(psyker_organ) psyker_organ.modify_stress(-PSYKER_STRESS_MEDITATION_POWER) if(psyker_organ.stress <= 0) to_chat(owner, span_notice("I no longer feel any stress")) - keep_going = FALSE + if(cultivator_dantian) + cultivator_dantian.adjust_dantian(CULTIVATOR_DANTIAN_MEDITATION_POWER) + if(cultivator_dantian.dantian >= CULTIVATOR_DANTIAN_MAX) + to_chat(owner, span_notice("My Dantian is fully charged.")) else keep_going = FALSE while (keep_going) @@ -44,17 +47,19 @@ Reduces stress for psykers and restores Dantian for cultivators return /datum/action/cooldown/power/resonant_meditate/proc/get_spotlight_color() - if(psyker_organ && cultivator_organ) + if(psyker_organ && cultivator_dantian) return /datum/status_effect/spotlight_light/resonant else if(psyker_organ) return /datum/status_effect/spotlight_light/psyker - else if(cultivator_organ) + else if(cultivator_dantian) return /datum/status_effect/spotlight_light/cultivator else return /datum/status_effect/spotlight_light -/datum/action/cooldown/power/resonant_meditate/proc/update_organs() +// gets the psyker organ and the cultivator component +/datum/action/cooldown/power/resonant_meditate/proc/update_components() psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) + cultivator_dantian = owner.GetComponent(/datum/component/cultivator_dantian) //TODO: Cultivator Organ // I wish I could just change the color on spotlights but no we have to make it special. @@ -63,7 +68,7 @@ Reduces stress for psykers and restores Dantian for cultivators spotlight_color = "#ba2cc9" /datum/status_effect/spotlight_light/cultivator id = "cultivator_spotlight" - spotlight_color = "#f5c612" + spotlight_color = "#66c5dd" /datum/status_effect/spotlight_light/resonant // if you somehow have both id = "resonant_spotlight" spotlight_color = "#cf2525" diff --git a/tgstation.dme b/tgstation.dme index 3fe7069eb97a9a..9d6190a1eadf87 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7462,6 +7462,12 @@ #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\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_dantian.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_power.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\astraltouched_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\scrying.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_action.dm" From 359472828d0487710028e583500ac4a5560c39b2 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 22 Feb 2026 10:15:16 +0100 Subject: [PATCH 081/212] Housekeeping on various components for theologians and piety. --- .../resonant/cultivator/_cultivator_action.dm | 4 ++-- .../resonant/cultivator/_cultivator_root.dm | 16 ++++++++++++++++ .../code/powers/resonant/meditate.dm | 19 ++++++++++++++++++- .../sorcerous/theologist/_theologist_root.dm | 16 ++++++++++++++++ 4 files changed, 52 insertions(+), 3 deletions(-) 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 982d529a28605e..8b2ca25560e567 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_star" - overlay_icon_state = "ab_goldborder" + background_icon_state = "bg_revenant" + overlay_icon_state = "bg_spell_border" button_icon = 'icons/mob/actions/backgrounds.dmi' // The organ that processes most of the Psyker Powers. Mostly all functions here communicate with this. 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 a9d74a8de49737..0eadc4a25be568 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 @@ -17,3 +17,19 @@ // Passes along meditation. grant_action(/datum/action/cooldown/power/resonant_meditate) . = ..() + +/datum/power/cultivator_root/remove() + . = ..() + if(!power_holder) + return + + // We check for other roots of our type, in the event that admin shenanigangs gave multiple roots. Don't want to throw out the whole component when other things are still in use. + var/has_other_root = FALSE + for(var/datum/power/power as anything in power_holder.powers) + if(istype(power, /datum/power/cultivator_root)) + has_other_root = TRUE + break + + if(!has_other_root) + var/tobedel = power_holder.GetComponent(/datum/component/cultivator_dantian) + QDELL_NULL(tobedel) diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index 44a7f5e0355807..64882830030491 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -27,7 +27,11 @@ Reduces stress for psykers and restores Dantian for cultivators do active = TRUE if(do_after(owner, 25, target = owner)) - if(!psyker_organ && cultivator_dantian) + if(user_has_active_power(target)) + to_chat(owner, span_notice("You have active abilities draining your resources!")) + keep_going = FALSE + break + if(!psyker_organ && !cultivator_dantian) to_chat(owner, span_notice("I have nothing to meditate on!")) if(psyker_organ) psyker_organ.modify_stress(-PSYKER_STRESS_MEDITATION_POWER) @@ -39,6 +43,7 @@ Reduces stress for psykers and restores Dantian for cultivators to_chat(owner, span_notice("My Dantian is fully charged.")) else keep_going = FALSE + break while (keep_going) to_chat(owner, "You stop meditating.") @@ -62,6 +67,18 @@ Reduces stress for psykers and restores Dantian for cultivators cultivator_dantian = owner.GetComponent(/datum/component/cultivator_dantian) //TODO: Cultivator Organ +// Returns TRUE if any active Cultivator or Psyker power is active on the target. +/datum/action/cooldown/power/resonant_meditate/proc/user_has_active_power(mob/living/user) + if(!user || !user.powers) + return FALSE + for(var/datum/power/power in user.powers) + if(power.path != POWER_PATH_CULTIVATOR && power.path != POWER_PATH_PSYKER) + continue + var/datum/action/cooldown/power/action = power.action_path + if(action && action.active) + return TRUE + return FALSE + // I wish I could just change the color on spotlights but no we have to make it special. /datum/status_effect/spotlight_light/psyker id = "psyker_spotlight" 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 f3ac1ae1eeeaba..de14da428af6e2 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 @@ -15,3 +15,19 @@ // We pass along the piety component to actually handle most of the piety stuff. power_holder.AddComponent(/datum/component/theologist_piety, power_holder) . = ..() + +/datum/power/theologist_root/remove() + . = ..() + if(!power_holder) + return + + // We check for other roots of our type, in the event that admin shenanigangs gave multiple roots. Don't want to throw out the whole component when other things are still in use. + var/has_other_root = FALSE + for(var/datum/power/power as anything in power_holder.powers) + if(istype(power, /datum/power/theologist_root)) + has_other_root = TRUE + break + + if(!has_other_root) + var/tobedel = power_holder.GetComponent(/datum/component/theologist_piety) + QDELL_NULL(tobedel) From 563ccb914d600780b62d30ae300442818e34308b Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 22 Feb 2026 10:53:16 +0100 Subject: [PATCH 082/212] Adds the 3 base cultivator roots. Fixes my inability to spell. --- .../resonant/cultivator/_cultivator_root.dm | 2 +- .../resonant/cultivator/astraltouched_root.dm | 37 +++++++-- .../resonant/cultivator/flamesoul_root.dm | 60 +++++++++++++++ .../resonant/cultivator/shadowwalker_root.dm | 77 +++++++++++++++++++ .../sorcerous/theologist/_theologist_root.dm | 2 +- tgstation.dme | 2 + 6 files changed, 172 insertions(+), 8 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm 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 0eadc4a25be568..af563641ba9cd3 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 @@ -32,4 +32,4 @@ if(!has_other_root) var/tobedel = power_holder.GetComponent(/datum/component/cultivator_dantian) - QDELL_NULL(tobedel) + QDEL_NULL(tobedel) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm index e221becb1aa381..808cc9d3c90921 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -1,20 +1,45 @@ /datum/power/cultivator_root/astral_touched - name = "Astral-Touched Alignment" + name = "Astral Touched Alignment" desc = "You gain your Dantian's aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do 10 extra burn damage.\ - Passively, your cold temprature and pressure threshold is increased by 30C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space (though you still need to breathe).\ + Passively, your cold temprature tolerance is increased by 30C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\ You gain armor IV across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/astral_touched - value = 7 + value = 6 + +// Gives innate resistance to cold. +/datum/power/cultivator_root/astral_touched/post_add() + . = ..() + if(!iscarbon(power_holder)) + return + var/mob/living/carbon/owner = power_holder + owner.dna.species.bodytemp_cold_damage_limit -= 30 + +/datum/power/cultivator_root/astral_touched/remove() + . = ..() + if(!iscarbon(power_holder)) + return + var/mob/living/carbon/owner = power_holder + owner.dna.species.bodytemp_cold_damage_limit += 30 /datum/action/cooldown/power/cultivator/alignment/astral_touched - name = "Astral-Touched Alignment" - desc = "Activates your Astral-Touched Alignment aura, granting you immune to cold and pressure, increasing your defenses, and increasing your strength with unarmed attacks." + name = "Astral Touched Alignment" + desc = "Activates your Astral Touched Alignment aura, granting you immune to cold and pressure, increasing your defenses (if unarmored), and increasing your strength with unarmed attacks." button_icon = 'icons/mob/actions/actions_spells.dmi' - button_icon_state = "transformslime" + button_icon_state = "teleport" alignment_outline_color = "#66c5dd" alignment_activation_sound = 'sound/effects/magic/cosmic_energy.ogg' alignment_overlay_state = "shieldsparkles" alignment_damage_type = BURN + +// Adds pressure immunity & cold immunity. +/datum/action/cooldown/power/cultivator/alignment/astral_touched/enable_alignment(mob/living/carbon/user) + . = ..() + user.add_traits(list(TRAIT_RESISTLOWPRESSURE, TRAIT_RESISTCOLD), src) + +/datum/action/cooldown/power/cultivator/alignment/astral_touched/disable_alignment(mob/living/carbon/user) + . = ..() + user.remove_traits(list(TRAIT_RESISTLOWPRESSURE, TRAIT_RESISTCOLD), src) + diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm new file mode 100644 index 00000000000000..ffc641777e155c --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -0,0 +1,60 @@ +/datum/power/cultivator_root/flame_soul + name = "Flame Soul Alignment" + desc = "You gain your Dantian's aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do 10 extra burn damage.\ + Passively, your high temprature threshold is increased by 30C regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ + You gain armor III and laser VI across your whole body. Has diminishing effects with your worn armor." + action_path = /datum/action/cooldown/power/cultivator/alignment/flame_soul + + value = 6 + +// Gives innate resistance to heat. +/datum/power/cultivator_root/flame_soul/post_add() + . = ..() + if(!iscarbon(power_holder)) + return + var/mob/living/carbon/owner = power_holder + owner.dna.species.bodytemp_heat_damage_limit += 30 + +/datum/power/cultivator_root/flame_soul/remove() + . = ..() + if(!iscarbon(power_holder)) + return + var/mob/living/carbon/owner = power_holder + owner.dna.species.bodytemp_heat_damage_limit -= 30 + +/datum/action/cooldown/power/cultivator/alignment/flame_soul + name = "Flame Soul Alignment" + desc = "Activates your Astral-Touched Alignment aura, granting you immunity to fire, increasing your defenses (if unarmored), and increasing your strength with unarmed attacks." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "sacredflame" + + alignment_outline_color = "#e99a3f" + alignment_activation_sound = 'sound/effects/magic/fireball.ogg' + alignment_overlay_state = "blessed" + + alignment_damage_type = BURN + alignment_defense = /datum/armor/alignment_flamesoul_defense + +// Adds pressure immunity & cold immunity. +/datum/action/cooldown/power/cultivator/alignment/flame_soul/enable_alignment(mob/living/carbon/user) + . = ..() + user.add_traits(list(TRAIT_RESISTHEAT, TRAIT_NOFIRE), src) + +/datum/action/cooldown/power/cultivator/alignment/flame_soul/disable_alignment(mob/living/carbon/user) + . = ..() + user.remove_traits(list(TRAIT_RESISTHEAT, TRAIT_NOFIRE), src) + + +// special laser & fire proofed armor for flamesoul. +/datum/armor/alignment_flamesoul_defense + acid = 30 + bio = 30 + melee = 30 + bullet = 30 + bomb = 30 + energy = 30 + laser = 60 + fire = 100 + melee = 30 + wound = 30 + diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm new file mode 100644 index 00000000000000..e3ccf8a39545be --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm @@ -0,0 +1,77 @@ +/datum/power/cultivator_root/shadow_walker + name = "Shadow Walker Alignment" + desc = "You gain your Dantian's aura through dark rooms and environments. Activating it wraps you in an aura of shadow.\ + You are entirely unrecognizeable in this state and your punches do 10 extra toxins damage.\ + Passively, you have enhanced darkvision, and gain full on night vision while your alignment is activated.\ + You gain armor IV across your whole body. Has diminishing effects with your worn armor." + action_path = /datum/action/cooldown/power/cultivator/alignment/shadow_walker + + value = 6 + +// Lets you see in the dark. +/datum/power/cultivator_root/shadow_walker/post_add() + . = ..() + ADD_TRAIT(power_holder, TRAIT_MINOR_NIGHT_VISION, REF(src)) + power_holder.update_sight() + +/datum/power/cultivator_root/shadow_walker/remove() + . = ..() + REMOVE_TRAIT(power_holder, TRAIT_MINOR_NIGHT_VISION, REF(src)) + power_holder.update_sight() + +/datum/action/cooldown/power/cultivator/alignment/shadow_walker + name = "Shadow Walker Alignment" + desc = "Activates your Shadow Walker Alignment aura, granting you immunity to slowdowns, increasing your defenses (if unarmored), and increasing your strength with unarmed attacks." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "void_magnet" + + + alignment_outline_color = "#000000" + alignment_damage_type = TOX + + // the spooky overlay unique to shadow walker + var/mutable_appearance/echo_overlay + +// Adds pressure immunity & cold immunity. +/datum/action/cooldown/power/cultivator/alignment/shadow_walker/enable_alignment(mob/living/carbon/user) + . = ..() + user.add_traits(list(TRAIT_UNKNOWN, TRAIT_TRUE_NIGHT_VISION), src) + user.update_sight() + +/datum/action/cooldown/power/cultivator/alignment/shadow_walker/disable_alignment(mob/living/carbon/user) + . = ..() + user.remove_traits(list(TRAIT_UNKNOWN, TRAIT_TRUE_NIGHT_VISION), src) + user.update_sight() + user.cut_overlay(echo_overlay) + +// We override the normal fx activation because this looks cooler. +/datum/action/cooldown/power/cultivator/alignment/shadow_walker/activation_fx(mob/living/carbon/user, atom/target) + // Use the same matrix as echolocation + var/static/list/black_white_matrix = list( + 85, 85, 85, 0, + 85, 85, 85, 0, + 85, 85, 85, 0, + 0, 0, 0, 1, + -254, -254, -254, 0 + ) + echo_overlay = new /mutable_appearance() + echo_overlay.appearance = user + echo_overlay.color = black_white_matrix + echo_overlay.filters += outline_filter(size = 1, color = COLOR_WHITE) + + echo_overlay.layer = user.layer + 0.1 + user.add_overlay(echo_overlay) + + // adds overlay + if(!alignment_overlay) + alignment_overlay = mutable_appearance(alignment_overlay_icon, alignment_overlay_state, alignment_overlay_layer) + alignment_overlay.color = alignment_outline_color + user.add_overlay(alignment_overlay) + + // white outline but slightly differently + user.remove_filter(filter_id) + user.add_filter(filter_id, 2, outline_filter(size = alignment_outline_size, color = "#ffffff")) + var/filter = user.get_filter(filter_id) + if(filter) + animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1) + animate(alpha = 40, time = 2.5 SECONDS) 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 de14da428af6e2..3fe9007c7324f4 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 @@ -30,4 +30,4 @@ if(!has_other_root) var/tobedel = power_holder.GetComponent(/datum/component/theologist_piety) - QDELL_NULL(tobedel) + QDEL_NULL(tobedel) diff --git a/tgstation.dme b/tgstation.dme index 9d6190a1eadf87..eeb36b8ce1a1d2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7468,6 +7468,8 @@ #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\astraltouched_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\flamesoul_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\shadowwalker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\scrying.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_action.dm" From 7ab2a4f3f7b4b90c790b287a2286b0cd6e52f969 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 22 Feb 2026 19:09:37 +0100 Subject: [PATCH 083/212] Housekeeping powers subsystem changes to make it so roots of the same path automatically block eachother, rather than needing to blacklist. --- .../code/powers_prefs_middleware.dm | 26 ++++++++++++++++++- .../modular_powers/code/powers_subsystem.dm | 12 ++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index 2eea55a4b6fa67..c486a6ec82b4d7 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -54,7 +54,7 @@ if(get_requiring_power(power_type)) locked_in = TRUE else - if(get_incompatible_power(power_type) || get_required_power(power_type)) + if(get_incompatible_power(power_type) || get_required_power(power_type) || would_exceed_path_limit(power_type)) locked_in = TRUE var/state @@ -411,12 +411,36 @@ * and returns the first one encountered if so. */ /datum/preference_middleware/powers/proc/get_incompatible_power(datum/power/power_type) + // checks for blacklist for(var/list/blacklist as anything in GLOB.powers_blacklist) if(!(power_type in blacklist)) continue for(var/datum/power/other_power_type as anything in blacklist) if(other_power_type.name in preferences.all_powers) return other_power_type + // checks for multiple roots of same path + if(power_type.priority == POWER_PRIORITY_ROOT) + for(var/existing_power_name in preferences.all_powers) + var/datum/power/existing_power_type = SSpowers.powers[existing_power_name] + if(!existing_power_type) + continue + if(existing_power_type.priority == POWER_PRIORITY_ROOT && existing_power_type.path == power_type.path) + return existing_power_type + +/** + * Returns TRUE if selecting power_type would exceed the 2-path limit. + */ +/datum/preference_middleware/powers/proc/would_exceed_path_limit(datum/power/power_type) + var/list/unique_paths = list() + for(var/existing_power_name in preferences.all_powers) + var/datum/power/existing_power_type = SSpowers.powers[existing_power_name] + if(!existing_power_type) + continue + unique_paths[existing_power_type.path] = TRUE + + // If this power adds a third distinct path, block it. + if(!(power_type.path in unique_paths) && length(unique_paths) >= 2) + return TRUE /datum/asset/simple/powers assets = list( diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index ea9f09988745cb..dc3684e0dd151f 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -2,7 +2,6 @@ // Both of these lists are shifted to glob so they are generated at world start instead of risking players doing preference stuff before the subsystem inits. GLOBAL_LIST_INIT_TYPED(powers_blacklist, /list/datum/power, list( //list(/datum/power/item_power/thaumaturge_root, /datum/power/enigmatist_root), - list(/datum/power/theologist_root/revered, /datum/power/theologist_root/shared, /datum/power/theologist_root/twisted) // The three Theologist Roots )) GLOBAL_LIST_INIT(powers_requirements_list, generate_powers_requirements_list()) @@ -144,6 +143,9 @@ PROCESSING_SUBSYSTEM_DEF(powers) // Track distinct paths we accept while filtering this batch var/list/unique_paths = list() + // Track distinct roots we accept. + var/list/root_by_path = list() + // TODO: work out how to filter powers missing their requirements. // This could be higher priorities, but could also be at the same priority level. // TODO: work out how to filter for going over the balance cap without introducing major issues. @@ -174,6 +176,12 @@ PROCESSING_SUBSYSTEM_DEF(powers) continue // Third distinct path, discard. unique_paths[power_type.path] = TRUE + // Block multiple root powers on the same path + if(power_type.priority == POWER_PRIORITY_ROOT) + if(root_by_path[power_type.path]) + continue // another root of this path already accepted + root_by_path[power_type.path] = power_type + // Make sure we don't have incompatible powers var/blacklisted = FALSE for(var/list/blacklist as anything in GLOB.powers_blacklist) @@ -187,6 +195,8 @@ PROCESSING_SUBSYSTEM_DEF(powers) break if(blacklisted) continue // Incompatible, discard. + + // Succes = add power intermediary_powers += power_name // Build a set of selected power types. From e851a6e2c6bad5b8ff262d124ef2d8495de2a3ae Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 22 Feb 2026 19:17:09 +0100 Subject: [PATCH 084/212] Fixes a ton of spelling mistakes, tweak effects of the cultivator roots --- code/__DEFINES/~doppler_defines/powers.dm | 8 +- .../cultivator/_cultivator_alignment.dm | 1 + .../resonant/cultivator/_cultivator_root.dm | 7 +- .../resonant/cultivator/flamesoul_root.dm | 5 +- .../resonant/cultivator/shadowwalker_root.dm | 2 +- .../sorcerous/theologist/_theologist_piety.dm | 5 +- .../sorcerous/theologist/_theologist_root.dm | 7 +- tgstation.dme | 94 +++++++++---------- 8 files changed, 71 insertions(+), 58 deletions(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index bddc048e7c2da0..b9341454086f86 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -130,6 +130,12 @@ // Maximum amount of Piety #define THEOLOGIAN_PIETY_MAX 50 +// UI location of the Piety element +#define THEOLOGIST_UI_SCREEN_LOC "WEST,CENTER-2:15" + +// In case the space is taken up by cultivator +#define THEOLOGIST_ALT_UI_SCREEN_LOC "WEST+1,CENTER-2:15" + // Trait made as to prevent duplicate smites. #define TRAIT_HAS_SMITING_STRIKE "has_smiting_strike" @@ -153,7 +159,7 @@ #define CULTIVATOR_DANTIAN_MEDITATION_POWER 4 // UI location of the Cultivator element -#define CULTIVATOR_UI_SCREEN_LOC "WEST,CENTER-2:20" +#define CULTIVATOR_UI_SCREEN_LOC "WEST,CENTER-2:15" // Bonus damage on strikes done by alignment #define CULTIVATOR_ALIGNMENT_DAMAGE_BONUS 10 diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm index 00d51b04a62255..828afc0ad9420b 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm @@ -103,6 +103,7 @@ /datum/action/cooldown/power/cultivator/alignment/proc/disable_alignment(mob/living/carbon/user) if(alignment_overlay) user.cut_overlay(alignment_overlay) + QDEL_NULL(alignment_overlay) UnregisterSignal(user, list(COMSIG_HUMAN_UNARMED_HIT, COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM)) user.remove_filter(filter_id) remove_alignment_armor() 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 af563641ba9cd3..7f90aad70add3b 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 @@ -20,16 +20,17 @@ /datum/power/cultivator_root/remove() . = ..() - if(!power_holder) + var/mob/living/holder = power_holder + if(!holder) return // We check for other roots of our type, in the event that admin shenanigangs gave multiple roots. Don't want to throw out the whole component when other things are still in use. var/has_other_root = FALSE - for(var/datum/power/power as anything in power_holder.powers) + for(var/datum/power/power as anything in holder.powers) if(istype(power, /datum/power/cultivator_root)) has_other_root = TRUE break if(!has_other_root) - var/tobedel = power_holder.GetComponent(/datum/component/cultivator_dantian) + var/tobedel = holder.GetComponent(/datum/component/cultivator_dantian) QDEL_NULL(tobedel) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm index ffc641777e155c..dee37631acc573 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -2,7 +2,7 @@ name = "Flame Soul Alignment" desc = "You gain your Dantian's aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do 10 extra burn damage.\ Passively, your high temprature threshold is increased by 30C regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ - You gain armor III and laser VI across your whole body. Has diminishing effects with your worn armor." + You gain armor III (with laser VI) across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/flame_soul value = 6 @@ -30,7 +30,8 @@ alignment_outline_color = "#e99a3f" alignment_activation_sound = 'sound/effects/magic/fireball.ogg' - alignment_overlay_state = "blessed" + alignment_overlay_icon = 'icons/effects/eldritch.dmi' + alignment_overlay_state = "ring_leader_effect" alignment_damage_type = BURN alignment_defense = /datum/armor/alignment_flamesoul_defense diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm index e3ccf8a39545be..196de29d275890 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm @@ -27,7 +27,7 @@ alignment_outline_color = "#000000" - alignment_damage_type = TOX + alignment_overlay_state = "curse" // the spooky overlay unique to shadow walker var/mutable_appearance/echo_overlay 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 4f30ddb47935c3..d9f73eabd1b3c7 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 @@ -68,6 +68,9 @@ var/datum/hud/hud_used = living_holder.hud_used theologist_ui = new /atom/movable/screen/theologist_piety(null, hud_used) + // If the cultivator dantian UI is present, use the alternate screen loc to avoid overlap. + if(living_holder.GetComponent(/datum/component/cultivator_dantian)) + theologist_ui.screen_loc = THEOLOGIST_ALT_UI_SCREEN_LOC hud_used.infodisplay += theologist_ui // Set initial text so it isn't blank until first adjust. @@ -88,4 +91,4 @@ name = "piety" icon = 'icons/hud/blob.dmi' // TODO: Get sprites/UI for this. icon_state = "block" - screen_loc = "WEST,CENTER-2:15" // TODO: Define & Move this. + screen_loc = THEOLOGIST_UI_SCREEN_LOC 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 3fe9007c7324f4..c90c84052963a9 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 @@ -18,16 +18,17 @@ /datum/power/theologist_root/remove() . = ..() - if(!power_holder) + var/mob/living/holder = power_holder + if(!holder) return // We check for other roots of our type, in the event that admin shenanigangs gave multiple roots. Don't want to throw out the whole component when other things are still in use. var/has_other_root = FALSE - for(var/datum/power/power as anything in power_holder.powers) + for(var/datum/power/power as anything in holder.powers) if(istype(power, /datum/power/theologist_root)) has_other_root = TRUE break if(!has_other_root) - var/tobedel = power_holder.GetComponent(/datum/component/theologist_piety) + var/tobedel = holder.GetComponent(/datum/component/theologist_piety) QDEL_NULL(tobedel) diff --git a/tgstation.dme b/tgstation.dme index eeb36b8ce1a1d2..83d2816c913a61 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -6800,6 +6800,8 @@ #include "interface\fonts\vcr_osd_mono.dm" #include "modular_doppler\z_lore_rename.dm" #include "modular_doppler\_HELPERS\preferences.dm" +#include "modular_doppler\_savefile_migration\code\_preferences_savefile.dm" +#include "modular_doppler\_savefile_migration\code\powers_migration.dm" #include "modular_doppler\accent_toggle\code\accent_toggle.dm" #include "modular_doppler\accessable_storage\accessable_storage.dm" #include "modular_doppler\accessable_storage\item.dm" @@ -7418,19 +7420,20 @@ #include "modular_doppler\modular_mood\code\mood_events\dog_wag.dm" #include "modular_doppler\modular_mood\code\mood_events\hotspring.dm" #include "modular_doppler\modular_mood\code\mood_events\race_drink.dm" -#include "modular_doppler\_savefile_migration\code\_preferences_savefile.dm" -#include "modular_doppler\_savefile_migration\code\powers_migration.dm" #include "modular_doppler\modular_powers\code\_power.dm" #include "modular_doppler\modular_powers\code\powers_action.dm" -#include "modular_doppler\modular_powers\code\powers_living.dm" +#include "modular_doppler\modular_powers\code\powers_antimagic.dm" #include "modular_doppler\modular_powers\code\powers_helpers.dm" +#include "modular_doppler\modular_powers\code\powers_living.dm" #include "modular_doppler\modular_powers\code\powers_prefs.dm" #include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm" #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" -#include "modular_doppler\modular_powers\code\powers_antimagic.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_action.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_power.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\expert\creature_tamer.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\eye_for_ingredients.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\filthy_rich.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\heavy_lifter.dm" @@ -7440,9 +7443,6 @@ #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\expert\creature_tamer.dm" -#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" -#include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_command_action.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_action.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\_warfighter_power.dm" @@ -7458,10 +7458,13 @@ #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\quick_draw.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\tackler.dm" #include "modular_doppler\modular_powers\code\powers\mortal\warfighter\components\grenade_components.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root_anomalous.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\resonant_immune.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_dantian.dm" @@ -7470,62 +7473,59 @@ #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\astraltouched_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\flamesoul_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\shadowwalker_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_organ.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\levitate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\scrying.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\_psyker_event.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\brain_trauma.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\cardiac_arrest.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\magic_trauma.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\silence_trauma.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\telekinetic_backlash.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\dizziness.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\headache.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\nosebleed.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\twitching.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\exhaustion.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\eyes_bleed.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\hallucinate.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\vomit.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_root.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_spell.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\lodestone_legends.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_action.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_power.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_preperation.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_spell_focus.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_affinity.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\phantasmal_tool.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_root.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\blend_for_me.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\gale_blast.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\magic_barrage.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\brazen_bindings.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\conjure_rain.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\gale_blast.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\magic_barrage.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\phantasmal_tool.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\vitalize_flora.dm" -#include "modular_doppler\modular_powers\code\security\resonant_cuffs.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_root.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_spell.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\lodestone_legends.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\test_powerthatrequirespower.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_affinity.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_spell_focus.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_action.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_piety.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_power.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_revered.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_shared.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_twisted.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_revered.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_piety.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\entropic_mending.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\pious_prayer.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike_upgrades.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\pious_prayer.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\meditate.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\silence_trauma.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_organ.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\levitate.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\_psyker_event.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\nosebleed.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\headache.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\dizziness.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\twitching.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\vomit.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\hallucinate.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\exhaustion.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\eyes_bleed.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\telekinetic_backlash.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\cardiac_arrest.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\magic_trauma.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\silence_trauma.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\brain_trauma.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\resonant_immune.dm" +#include "modular_doppler\modular_powers\code\security\resonant_cuffs.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" #include "modular_doppler\modular_quirks\bouncy\bouncy.dm" From e17a929ddfb195e91cf426336884458dfc1a6ef0 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 22 Feb 2026 19:21:47 +0100 Subject: [PATCH 085/212] I missed a few lmao --- .../code/powers/resonant/cultivator/_cultivator_dantian.dm | 2 +- .../code/powers/resonant/cultivator/flamesoul_root.dm | 1 + .../code/powers/sorcerous/theologist/_theologist_piety.dm | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm index 8566a20dffeba4..296743df740c8b 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm @@ -7,7 +7,7 @@ // The mob we’re attached to is always `parent`. var/mob/living/attached_mob - // Whatever state your old attached_cultivator_dantian tracked: + // Current Dantian & the cap itself. var/dantian = 0 var/max_dantian = CULTIVATOR_DANTIAN_MAX diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm index dee37631acc573..d20d0e3a6b61c5 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -32,6 +32,7 @@ alignment_activation_sound = 'sound/effects/magic/fireball.ogg' alignment_overlay_icon = 'icons/effects/eldritch.dmi' alignment_overlay_state = "ring_leader_effect" + alignment_overlay_layer = LOW_MOB_LAYER alignment_damage_type = BURN alignment_defense = /datum/armor/alignment_flamesoul_defense 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 d9f73eabd1b3c7..c9b54f3fbebdb5 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 @@ -7,7 +7,7 @@ // The mob we’re attached to is always `parent`. var/mob/living/attached_mob - // Whatever state your old attached_theologist_piety tracked: + // current piety & max piety var/piety = 0 var/max_piety = THEOLOGIAN_PIETY_MAX From 67a88b85e40f38ed5bf384c7172349c4a2e53428 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 23 Feb 2026 11:06:35 +0100 Subject: [PATCH 086/212] Finalizes all alignment powers & aura farm. Various tweaks to how scrying si rendered. Slightly buffed krav maga cause unarmed doesnt stack with it. --- code/__DEFINES/~doppler_defines/powers.dm | 22 ++- .../powers/mortal/warfighter/krav_maga.dm | 5 +- .../resonant/cultivator/_cultivator_action.dm | 8 + .../cultivator/_cultivator_alignment.dm | 2 +- .../cultivator/_cultivator_dantian.dm | 22 ++- .../resonant/cultivator/astraltouched_root.dm | 36 ++++ .../resonant/cultivator/flamesoul_root.dm | 115 +++++++++++++ .../resonant/cultivator/shadowwalker_root.dm | 162 ++++++++++++++++-- .../code/powers/resonant/meditate.dm | 16 +- .../powers/resonant/psyker/_psyker_root.dm | 4 +- .../code/powers/resonant/psyker/scrying.dm | 50 ++---- 11 files changed, 386 insertions(+), 56 deletions(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index b9341454086f86..3dce549a0d7298 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -156,7 +156,7 @@ #define CULTIVATOR_DANTIAN_MAX 1000 // How much dantian we get from meditation every 2.5 seconds -#define CULTIVATOR_DANTIAN_MEDITATION_POWER 4 +#define CULTIVATOR_DANTIAN_MEDITATION_POWER 2 // UI location of the Cultivator element #define CULTIVATOR_UI_SCREEN_LOC "WEST,CENTER-2:15" @@ -164,6 +164,26 @@ // Bonus damage on strikes done by alignment #define CULTIVATOR_ALIGNMENT_DAMAGE_BONUS 10 +// The max amount of Dantian we give from aura farming per second +#define CULTIVATOR_MAX_CULTIVATION_BONUS 3 +// The min amount of Dantian we give from aura farming per second +#define CULTIVATOR_MIN_CULTIVATION_BONUS 0.15 + +// Standard Dantian cost defines for Cultivators. Since it scales funny it has a 1/1000 called paltry. +#define CULTIVATOR_DANTIAN_PALTRY (CULTIVATOR_DANTIAN_MAX / 1000) +#define CULTIVATOR_DANTIAN_TRIVIAL (CULTIVATOR_DANTIAN_MAX / 100) +#define CULTIVATOR_DANTIAN_MINOR (CULTIVATOR_DANTIAN_MAX / 10) +#define CULTIVATOR_DANTIAN_MODERATE (CULTIVATOR_DANTIAN_MAX / 5) +#define CULTIVATOR_DANTIAN_MAJOR (CULTIVATOR_DANTIAN_MAX / 2) +#define CULTIVATOR_DANTIAN_CRUSHING (CULTIVATOR_DANTIAN_MAX) + +// Defines SPECIFICALLY for auro farming amounts +#define CULTIVATOR_AURA_FARM_TRIVIAL (CULTIVATOR_MAX_CULTIVATION_BONUS / 100) +#define CULTIVATOR_AURA_FARM_MINOR (CULTIVATOR_MAX_CULTIVATION_BONUS / 10) +#define CULTIVATOR_AURA_FARM_MODERATE (CULTIVATOR_MAX_CULTIVATION_BONUS / 5) +#define CULTIVATOR_AURA_FARM_MAJOR (CULTIVATOR_MAX_CULTIVATION_BONUS / 2) +#define CULTIVATOR_AURA_FARM_CRUSHING (CULTIVATOR_MAX_CULTIVATION_BONUS) + /** * RESONANT: PSYKER * All defines related to the enigmatist powers. diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm index bc0af0fe013ce1..36e9f900fc48f7 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm @@ -3,8 +3,9 @@ */ /datum/power/warfighter/krav_maga name = "Krav Maga" - desc = "Trained in various disarming moves, you can wield the martial arts of Krav Maga without any external assistance." - value = 9 + desc = "Trained in various disarming moves, you can wield the martial arts of Krav Maga without any external assistance.\ + (Powers that give you access to Martial Arts override your unarmed attacks and thusly do not stack with any modifier that affect your punches)" + value = 8 required_powers = list(/datum/power/warfighter/martial_artist) /// Mindbound martial art component so the style follows mind transfers var/datum/component/mindbound_martial_arts/krav_component 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 8b2ca25560e567..a08c98865407d4 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 @@ -13,10 +13,18 @@ // Cost in Dantian to use var/cost + // Does this power get called by _cultivator_dantian.dm when we check for aura farming? Used for potential future powers that allow you to aura farm in other ways. + var/contributes_to_aura_farming = FALSE + /datum/action/cooldown/power/cultivator/New() . = ..() ValidateDantianComponent() +// Feng Shui / Aura farming mechanics; get stuff in the environment, increase dantian based on it +// The func should be responsible for checking all the environmental stuff, calculating it and then returning it to the dantian system. +/datum/action/cooldown/power/cultivator/proc/aura_farm() + return 0 + // Since Cultivator has multiple roots and a persistent resource system, we use a component for handling Dantian /datum/action/cooldown/power/cultivator/proc/ValidateDantianComponent() if(owner) // Prevents runtiming on start diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm index 828afc0ad9420b..729e12a9cc6fad 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm @@ -33,7 +33,7 @@ var/alignment_damage_bonus = CULTIVATOR_ALIGNMENT_DAMAGE_BONUS cooldown_time = 5 // to prevent spam-clicking it off - + contributes_to_aura_farming = TRUE // needs to be always be on or you won't get aura from alignment // Removes stray listeners. /datum/action/cooldown/power/cultivator/alignment/Destroy() . = ..() diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm index 296743df740c8b..ad0c4fcd4bb4ce 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm @@ -1,5 +1,5 @@ /// Helper to format the text that gets thrown onto the dantian hud element. -#define FORMAT_DANTIAN_TEXT(charges) MAPTEXT("
[round(charges)]
") +#define FORMAT_DANTIAN_TEXT(charges) MAPTEXT("
[floor(charges)]
") /datum/component/cultivator_dantian dupe_mode = COMPONENT_DUPE_UNIQUE @@ -19,8 +19,8 @@ if(!isliving(parent)) return COMPONENT_INCOMPATIBLE attached_mob = parent - RegisterWithParent() + START_PROCESSING(SSfastprocess, src) /datum/component/cultivator_dantian/RegisterWithParent() . = ..() @@ -37,6 +37,7 @@ /datum/component/cultivator_dantian/Destroy() UnregisterFromParent() + STOP_PROCESSING(SSfastprocess, src) if(!attached_mob) return @@ -49,6 +50,23 @@ attached_mob = null return ..() +// Processing is responsible for most of the aura farming / 'passive dantian gain'. +/datum/component/cultivator_dantian/process(seconds_per_tick) + if(!attached_mob) + return + if(HAS_TRAIT(attached_mob, TRAIT_RESONANCE_SILENCED)) // no aura farming when silenced + return + // Just for the sake of future proofing, you can have multiple sources of aura farming. + var/total = 0 + for(var/datum/action/cooldown/power/cultivator/power in attached_mob.actions) + if(power.contributes_to_aura_farming && !power.active) // needs to have the contributing flag and not be active + total += power.aura_farm() + + total = clamp(total, CULTIVATOR_MIN_CULTIVATION_BONUS, CULTIVATOR_MAX_CULTIVATION_BONUS) + total *= seconds_per_tick // I love spess game time-based maths + + adjust_dantian(total) + /datum/component/cultivator_dantian/proc/on_hud_created(datum/source) SIGNAL_HANDLER diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm index 808cc9d3c90921..63b166a40d3549 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -43,3 +43,39 @@ . = ..() user.remove_traits(list(TRAIT_RESISTLOWPRESSURE, TRAIT_RESISTCOLD), src) +/datum/action/cooldown/power/cultivator/alignment/astral_touched/aura_farm() + var/total = 0 + var/mob/living/owner_mob = owner + if(!owner_mob) + return total + + var/space_value = CULTIVATOR_AURA_FARM_MINOR * 0.6 // the real thing + var/glass_value = CULTIVATOR_AURA_FARM_MINOR * 0.3 // not as cool but its something + var/fake_space_value = CULTIVATOR_AURA_FARM_MINOR * 0.4 // looks pretty real. + var/space_cube_value = CULTIVATOR_AURA_FARM_MINOR * 0.5 // Praise the space cube poster. + var/in_space_value = CULTIVATOR_AURA_FARM_MAJOR // Being out in space basically guarantees 50% charge. + + // Do we see space turfs? + for(var/turf/T in view(owner_mob)) + if(istype(T, /turf/open/space)) + total += space_value + continue + if(istype(T, /turf/open/floor/glass)) // Note, we check if you can see space on the z-level below. If you can or there's no z-level you get the space bonus. + var/turf/below = locate(T.x, T.y, T.z - 1) + if(!below || istype(below, /turf/open/space)) + total += glass_value + continue + if(istype(T, /turf/open/floor/fakespace)) + total += fake_space_value + continue + + // PRAISE THE CUBE POSTER. IT HAS SPACE ON IT - THAT COUNTS! + for(var/obj/structure/sign/poster/contraband/space_cube/cube in view(owner_mob)) + total += space_cube_value + + // Are we in space? + var/turf/owner_turf = get_turf(owner_mob) + if(istype(owner_turf, /turf/open/space)) + total += in_space_value + + return total diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm index d20d0e3a6b61c5..4bcbd35a8d4396 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -60,3 +60,118 @@ melee = 30 wound = 30 + +/datum/action/cooldown/power/cultivator/alignment/flame_soul/aura_farm() + var/total = 0 + var/mob/living/owner_mob = owner + if(!owner_mob) + return total + + var/object_on_fire_value = CULTIVATOR_AURA_FARM_MINOR * 0.3 // stuff that is on fire that shouldnt be e.g a paper stack + var/natural_fire_object_value = CULTIVATOR_AURA_FARM_MINOR * 0.5 // exposed flames that are intended e.g candles + var/big_natural_fire_object_value = CULTIVATOR_AURA_FARM_MODERATE // exposed flames that are intended and big e.g bonfires + var/fire_turf_value = CULTIVATOR_AURA_FARM_MINOR // turfs being on fire e.g plasma fire + var/smoking_value = CULTIVATOR_AURA_FARM_MODERATE // smoking is cool and good for aura. + + var/others_on_fire_value = CULTIVATOR_AURA_FARM_MODERATE // someone else is on fire + var/user_on_fire_value = CULTIVATOR_AURA_FARM_MAJOR // we're on fire + + // Big ol list of objects that are meant to be onfire. + var/static/list/natural_fire_typecache = typecacheof(list( + /obj/item/flashlight/flare, + /obj/item/flashlight/flare/candle, + /obj/item/match, + /obj/item/lighter, + /obj/item/oxygen_candle, + /obj/item/sparkler, + /obj/structure/wall_torch + )) + + + // Big ol list of big objects that are meant to be on fire. + var/static/list/big_natural_fire_typecache = typecacheof(list( + /obj/structure/bonfire, + /obj/structure/fireplace, + /obj/structure/firepit + )) + + // Checks for hotspots aka is the engine on fire and does that let us aura farm? + for(var/turf/open/open_turf in view(owner_mob)) + if(open_turf.active_hotspot) + total += fire_turf_value + + // Check if there is anyone on fire nearby. + for(var/mob/living/burning_mob in view(owner_mob)) + if(burning_mob == owner_mob) // we check this separetely. + continue + if(burning_mob.on_fire) + total += others_on_fire_value + + // Check if we are on fire. + if(owner_mob.on_fire) + total += user_on_fire_value + + // Check if we are smoking something in our mask slot. + var/obj/item/mask_item = owner_mob.get_item_by_slot(ITEM_SLOT_MASK) + if(istype(mask_item, /obj/item/cigarette)) + var/obj/item/cigarette/smoking_item = mask_item + if(smoking_item.lit) + total += smoking_value + + // Goes through all the objects in view. + for(var/obj/scene_object in view(owner_mob)) + // List that goes through all the big items and checks if they are on fire. + if(is_type_in_typecache(scene_object, big_natural_fire_typecache)) + if(istype(scene_object, /obj/structure/bonfire)) + var/obj/structure/bonfire/bonfire_object = scene_object + if(bonfire_object.burning) + total += big_natural_fire_object_value + continue + if(istype(scene_object, /obj/structure/fireplace)) + var/obj/structure/fireplace/fireplace_object = scene_object + if(fireplace_object.lit) + total += big_natural_fire_object_value + continue + if(istype(scene_object, /obj/structure/firepit)) + var/obj/structure/firepit/firepit_object = scene_object + if(firepit_object.active) + total += big_natural_fire_object_value + continue + // List that goes through all the smaller scene objects and check if they are on fire. + if(is_type_in_typecache(scene_object, natural_fire_typecache)) + if(istype(scene_object, /obj/item/flashlight/flare)) + var/obj/item/flashlight/flare/flare_object = scene_object + if(flare_object.light_on) + total += natural_fire_object_value + continue + if(istype(scene_object, /obj/structure/wall_torch)) + var/obj/structure/wall_torch/wall_torch_object = scene_object + if(wall_torch_object.burning) + total += natural_fire_object_value + continue + if(istype(scene_object, /obj/item/oxygen_candle)) + var/obj/item/oxygen_candle/oxygen_candle_object = scene_object + if(oxygen_candle_object.processing) + total += natural_fire_object_value + continue + if(istype(scene_object, /obj/item/match)) + var/obj/item/match/match_object = scene_object + if(match_object.lit) + total += natural_fire_object_value + continue + if(istype(scene_object, /obj/item/lighter)) + var/obj/item/lighter/lighter_object = scene_object + if(lighter_object.lit) + total += natural_fire_object_value + continue + if(istype(scene_object, /obj/item/sparkler)) + var/obj/item/sparkler/sparkler_object = scene_object + if(sparkler_object.lit) + total += natural_fire_object_value + continue + + // Checks if the item is on fire when its nto meant to be on fire. + if(scene_object.resistance_flags & ON_FIRE) + total += object_on_fire_value + + return total diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm index 196de29d275890..b5a847a805cd73 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm @@ -1,7 +1,7 @@ /datum/power/cultivator_root/shadow_walker name = "Shadow Walker Alignment" desc = "You gain your Dantian's aura through dark rooms and environments. Activating it wraps you in an aura of shadow.\ - You are entirely unrecognizeable in this state and your punches do 10 extra toxins damage.\ + You are entirely unrecognizeable in this state and your punches do 10 extra brute damage.\ Passively, you have enhanced darkvision, and gain full on night vision while your alignment is activated.\ You gain armor IV across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/shadow_walker @@ -31,21 +31,86 @@ // the spooky overlay unique to shadow walker var/mutable_appearance/echo_overlay + // global name/identity masking + var/datum/shadowwalker_identity/shadowwalker_identity // Adds pressure immunity & cold immunity. /datum/action/cooldown/power/cultivator/alignment/shadow_walker/enable_alignment(mob/living/carbon/user) . = ..() - user.add_traits(list(TRAIT_UNKNOWN, TRAIT_TRUE_NIGHT_VISION), src) + ADD_TRAIT(user, TRAIT_TRUE_NIGHT_VISION, src) user.update_sight() + RegisterSignal(user, COMSIG_MOB_UPDATE_HELD_ITEMS, PROC_REF(on_held_items_updated)) + RegisterSignal(user, COMSIG_LIVING_POST_UPDATE_TRANSFORM, PROC_REF(on_transform_updated)) + if(!shadowwalker_identity) + shadowwalker_identity = new(user) + refresh_echo_overlay(user) /datum/action/cooldown/power/cultivator/alignment/shadow_walker/disable_alignment(mob/living/carbon/user) . = ..() - user.remove_traits(list(TRAIT_UNKNOWN, TRAIT_TRUE_NIGHT_VISION), src) + REMOVE_TRAIT(user, TRAIT_TRUE_NIGHT_VISION, src) user.update_sight() + UnregisterSignal(user, COMSIG_MOB_UPDATE_HELD_ITEMS) + UnregisterSignal(user, COMSIG_LIVING_POST_UPDATE_TRANSFORM) + QDEL_NULL(shadowwalker_identity) user.cut_overlay(echo_overlay) +/datum/action/cooldown/power/cultivator/alignment/shadow_walker/aura_farm() + var/total = 0 + var/mob/living/owner_mob = owner + if(!owner_mob) + return total + + // For reference: dark means lum <=0.2, dim is lum <=0.5 and everything above that is called bright. + var/dim_space_value = CULTIVATOR_AURA_FARM_TRIVIAL * 0.2 // if there's dim light + var/darkness_space_value = CULTIVATOR_AURA_FARM_TRIVIAL // darkness itself + var/stood_in_darkness = CULTIVATOR_AURA_FARM_MODERATE // if we are stood in the dark + var/fully_dark_bonus = CULTIVATOR_AURA_FARM_MODERATE // only seeing dim and dark + var/spacious_fully_dark_bonus = CULTIVATOR_AURA_FARM_MODERATE // only seeing dim and dark + is spacious (30) + + var/dim_threshold = 0.5 + var/viewable_turfs = 0 + var/any_bright = FALSE + + // Gets the dim and darkness of every space + for(var/turf/T in view(owner_mob)) + if(!istype(T, /turf/open)) // no walls + continue + if(IS_OPAQUE_TURF(T)) // no non-opaque stuff (shutters, blackened windows, etc) that still counts as open + continue + viewable_turfs++ + var/lum = T.get_lumcount() + if(lum <= LIGHTING_TILE_IS_DARK) + total += darkness_space_value + else if(lum <= dim_threshold) + total += dim_space_value + else + any_bright = TRUE + + // Are we stood in darkness? Or are we stuffed away somewhere that the light probably doesn't see us? + var/turf/owner_turf = get_turf(owner_mob) + if(!isturf(owner_mob.loc) || (owner_turf && owner_turf.get_lumcount() <= dim_threshold)) + total += stood_in_darkness + + // Are there any bright tiles? + if(!any_bright) + total += fully_dark_bonus + if(viewable_turfs >= 30) + total += spacious_fully_dark_bonus + + return total + // We override the normal fx activation because this looks cooler. /datum/action/cooldown/power/cultivator/alignment/shadow_walker/activation_fx(mob/living/carbon/user, atom/target) + refresh_echo_overlay(user) + + // adds overlay + if(!alignment_overlay) + alignment_overlay = mutable_appearance(alignment_overlay_icon, alignment_overlay_state, alignment_overlay_layer) + alignment_overlay.color = alignment_outline_color + user.add_overlay(alignment_overlay) + +// Refreshes the overlay, because mechanically we want to always keep the user covered, we need to actually reupdate it during various animations (knockdown e.g) +/datum/action/cooldown/power/cultivator/alignment/shadow_walker/proc/refresh_echo_overlay(mob/living/carbon/user) // Use the same matrix as echolocation var/static/list/black_white_matrix = list( 85, 85, 85, 0, @@ -54,24 +119,95 @@ 0, 0, 0, 1, -254, -254, -254, 0 ) - echo_overlay = new /mutable_appearance() - echo_overlay.appearance = user + user.cut_overlay(echo_overlay) + echo_overlay = new /mutable_appearance(user.icon, user.icon_state) + echo_overlay.copy_overlays(user) + echo_overlay.dir = user.dir echo_overlay.color = black_white_matrix echo_overlay.filters += outline_filter(size = 1, color = COLOR_WHITE) - echo_overlay.layer = user.layer + 0.1 + echo_overlay.layer = user.layer user.add_overlay(echo_overlay) - // adds overlay - if(!alignment_overlay) - alignment_overlay = mutable_appearance(alignment_overlay_icon, alignment_overlay_state, alignment_overlay_layer) - alignment_overlay.color = alignment_outline_color - user.add_overlay(alignment_overlay) - - // white outline but slightly differently + // Keep the pulsing outline filter alive through rebuilds. user.remove_filter(filter_id) user.add_filter(filter_id, 2, outline_filter(size = alignment_outline_size, color = "#ffffff")) var/filter = user.get_filter(filter_id) if(filter) animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1) animate(alpha = 40, time = 2.5 SECONDS) + +/datum/action/cooldown/power/cultivator/alignment/shadow_walker/proc/on_held_items_updated(mob/living/carbon/user) + SIGNAL_HANDLER + if(!user) + return + refresh_echo_overlay(user) + +/datum/action/cooldown/power/cultivator/alignment/shadow_walker/proc/on_transform_updated(mob/living/carbon/user) + SIGNAL_HANDLER + if(!user) + return + refresh_echo_overlay(user) + + +/* + Global identity masking for Shadow Walker alignment. +*/ +/datum/shadowwalker_identity + var/mob/living/carbon/human/owner + var/datum/weakref/owner_ref + var/active = FALSE + +/datum/shadowwalker_identity/New(mob/living/carbon/human/owner_arg) + . = ..() + owner = owner_arg + owner_ref = WEAKREF(owner) + apply() + +/datum/shadowwalker_identity/Destroy() + clear() + owner = null + owner_ref = null + return ..() + +/datum/shadowwalker_identity/proc/apply() + var/mob/living/carbon/human/owner = src.owner || owner_ref?.resolve() + if(!istype(owner)) + return + active = TRUE + RegisterSignal(owner, COMSIG_HUMAN_GET_VISIBLE_NAME, PROC_REF(on_visible_name)) + RegisterSignal(owner, COMSIG_HUMAN_GET_FORCED_NAME, PROC_REF(on_forced_name)) + RegisterSignal(owner, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) + owner.update_visible_name() + +/datum/shadowwalker_identity/proc/clear() + var/mob/living/carbon/human/owner = src.owner || owner_ref?.resolve() + if(owner) + UnregisterSignal(owner, list(COMSIG_HUMAN_GET_VISIBLE_NAME, COMSIG_HUMAN_GET_FORCED_NAME, COMSIG_ATOM_EXAMINE)) + owner.update_visible_name() + active = FALSE + +/datum/shadowwalker_identity/proc/on_visible_name(mob/living/carbon/human/source, list/identity) + SIGNAL_HANDLER + if(!active) + return + if(identity[VISIBLE_NAME_FORCED]) + return + identity[VISIBLE_NAME_FACE] = "Unknown" + identity[VISIBLE_NAME_ID] = "Unknown" + +/datum/shadowwalker_identity/proc/on_forced_name(mob/living/carbon/human/source, list/identity) + SIGNAL_HANDLER + if(!active) + return + identity[VISIBLE_NAME_FORCED] = INFINITY + identity[VISIBLE_NAME_FACE] = "Unknown" + identity[VISIBLE_NAME_ID] = "Unknown" + +/datum/shadowwalker_identity/proc/on_examine(datum/source, mob/user, list/examine_list) + SIGNAL_HANDLER + if(!active) + return NONE + examine_list.Cut() + examine_list += span_notice("It's too shrouded in shadow to make out any details.") + return NONE diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index 64882830030491..a662c56b1694c2 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -13,9 +13,19 @@ Reduces stress for psykers and restores Dantian for cultivators var/obj/item/organ/resonant/psyker/psyker_organ var/datum/component/cultivator_dantian/cultivator_dantian + // used for the do while loop + var/keep_going +// Makes it end meditation by clicking it again. +/datum/action/cooldown/power/resonant_meditate/Trigger(mob/clicker, trigger_flags, atom/target) + if(active) + keep_going = FALSE + else + . = ..() + return TRUE + /datum/action/cooldown/power/resonant_meditate/use_action() . = ..() - var/keep_going = TRUE + keep_going = TRUE var/mob/living/spotlighttarget = owner // cause we need to call it on a mob/living to_chat(owner, span_notice("You start meditating.")) @@ -27,7 +37,7 @@ Reduces stress for psykers and restores Dantian for cultivators do active = TRUE if(do_after(owner, 25, target = owner)) - if(user_has_active_power(target)) + if(user_has_active_power(owner)) to_chat(owner, span_notice("You have active abilities draining your resources!")) keep_going = FALSE break @@ -69,7 +79,7 @@ Reduces stress for psykers and restores Dantian for cultivators // Returns TRUE if any active Cultivator or Psyker power is active on the target. /datum/action/cooldown/power/resonant_meditate/proc/user_has_active_power(mob/living/user) - if(!user || !user.powers) + if(!istype(user, /mob/living) || !user.powers) return FALSE for(var/datum/power/power in user.powers) if(power.path != POWER_PATH_CULTIVATOR && power.path != POWER_PATH_PSYKER) 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 da7e15e02e9a44..de744536b24e6c 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 @@ -12,15 +12,15 @@ path = POWER_PATH_PSYKER priority = POWER_PRIORITY_ROOT - action_path = /datum/action/cooldown/power/resonant_meditate // todo; deal with duplicates - var/obj/item/organ/resonant/psyker/psyker_organ /datum/power/psyker_root/add(client/client_source) psyker_organ = new /obj/item/organ/resonant/psyker psyker_organ.Insert(power_holder, special = TRUE) + grant_action(/datum/action/cooldown/power/resonant_meditate) /datum/power/psyker_root/remove(client/client_source) + if(psyker_organ) qdel(psyker_organ) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm index 9f60d7e8762d84..dd4c81ee3b1f14 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -339,7 +339,7 @@ var/datum/weakref/eye_ref var/datum/weakref/action_ref - // mob -> list(hide_image) + // mob -> mask_image var/list/masked_mobs = list() /datum/scrying_immunity_mask/New(datum/action/cooldown/power/psyker/scrying/action, mob/living/viewer, mob/eye/psyker_scry/eye) @@ -403,53 +403,39 @@ // makes the silhouettes directional /datum/scrying_immunity_mask/proc/update_silhouette_dir(mob/living/target_mob) - var/list/entry = masked_mobs[target_mob] - if(!entry) + var/image/mask_image = masked_mobs[target_mob] + if(!mask_image) return - var/image/silhouette_image = entry[2] - if(silhouette_image) - silhouette_image.dir = target_mob.dir + mask_image.dir = target_mob.dir /datum/scrying_immunity_mask/proc/mask_mob(mob/living/viewer, mob/living/target_mob) if(!viewer?.client || QDELETED(target_mob)) return - // Hide the mob for THIS client only (visual override) - var/image/hide_image = image(loc = target_mob) - hide_image.appearance = target_mob.appearance - hide_image.override = TRUE - hide_image.alpha = 0 + // Delusion-style override: a client-only mask image that owns the click/name. + var/image/mask_image = image(loc = target_mob) + mask_image.appearance = target_mob.appearance + mask_image.override = TRUE + mask_image.name = "Unknown" + mask_image.color = "#000000" + mask_image.alpha = 180 + mask_image.dir = target_mob.dir + SET_PLANE_EXPLICIT(mask_image, ABOVE_GAME_PLANE, target_mob) - // Silhouette marker, anchored to the mob so it follows movement - var/image/silhouette_image = image('icons/effects/effects.dmi', target_mob, "blank") - silhouette_image.override = FALSE - silhouette_image.layer = ABOVE_MOB_LAYER - silhouette_image.plane = GAME_PLANE - silhouette_image.appearance_flags = RESET_ALPHA | RESET_COLOR | RESET_TRANSFORM - silhouette_image.dir = target_mob.dir - - viewer.client.images += hide_image - viewer.client.images += silhouette_image - - masked_mobs[target_mob] = list(hide_image, silhouette_image) + viewer.client.images += mask_image + masked_mobs[target_mob] = mask_image // Keep your existing “don’t leak info” hooks RegisterSignal(target_mob, COMSIG_ATOM_EXAMINE, PROC_REF(on_target_examine)) hide_data_huds(viewer, target_mob) /datum/scrying_immunity_mask/proc/unmask_mob(mob/living/viewer, mob/living/target_mob) - var/list/entry = masked_mobs[target_mob] - if(!entry) + var/image/mask_image = masked_mobs[target_mob] + if(!mask_image) return - var/image/hide_image = entry[1] - var/image/silhouette_image = entry[2] - if(viewer?.client) - if(hide_image) - viewer.client.images -= hide_image - if(silhouette_image) - viewer.client.images -= silhouette_image + viewer.client.images -= mask_image UnregisterSignal(target_mob, COMSIG_ATOM_EXAMINE) unhide_data_huds(viewer, target_mob) From 2ae86fb455ea1de491626e74f4bf3c27e5889577 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 23 Feb 2026 17:19:02 +0100 Subject: [PATCH 087/212] Adds many stars (aggressive action for Astral Touched). Added a generic function that lets us call damage and have it be affected by armor. Tweaks a variety of other lil things. --- code/__DEFINES/~doppler_defines/powers.dm | 4 +- .../code/powers/mortal/expert/punt.dm | 4 +- .../cultivator/_cultivator_dantian.dm | 6 +- .../resonant/cultivator/flamesoul_root.dm | 5 +- .../powers/resonant/cultivator/many_stars.dm | 316 ++++++++++++++++++ .../resonant/cultivator/shadowwalker_root.dm | 2 +- .../modular_powers/code/powers_action.dm | 24 ++ tgstation.dme | 1 + 8 files changed, 355 insertions(+), 7 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 3dce549a0d7298..be0c2284734e71 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -156,7 +156,7 @@ #define CULTIVATOR_DANTIAN_MAX 1000 // How much dantian we get from meditation every 2.5 seconds -#define CULTIVATOR_DANTIAN_MEDITATION_POWER 2 +#define CULTIVATOR_DANTIAN_MEDITATION_POWER 3 // UI location of the Cultivator element #define CULTIVATOR_UI_SCREEN_LOC "WEST,CENTER-2:15" @@ -167,7 +167,7 @@ // The max amount of Dantian we give from aura farming per second #define CULTIVATOR_MAX_CULTIVATION_BONUS 3 // The min amount of Dantian we give from aura farming per second -#define CULTIVATOR_MIN_CULTIVATION_BONUS 0.15 +#define CULTIVATOR_MIN_CULTIVATION_BONUS 0 // Standard Dantian cost defines for Cultivators. Since it scales funny it has a 1/1000 called paltry. #define CULTIVATOR_DANTIAN_PALTRY (CULTIVATOR_DANTIAN_MAX / 1000) diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm index d131344074d876..6cf033ba528c44 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm @@ -76,8 +76,8 @@ playsound(living_atom, 'sound/items/lead_pipe_hit.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) // I am not sorry for this choice in sound effect // logging - living_atom.log_message("was punted by an object from [thrower] for [damage] damage.") - thrower.log_message("punted an object at [living_atom] for [damage] damage.") + living_atom.log_message("was punted by an object from [thrower] for [damage] damage.", LOG_VICTIM) + thrower.log_message("punted an object at [living_atom] for [damage] damage.", LOG_ATTACK) if(!thrower || get_dist(thrower, hit_atom) >= 12) //if you hit someone offscreen, which can't be done without legendary. thrower.playsound_local(thrower, 'sound/items/weapons/homerun.ogg', 75) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm index ad0c4fcd4bb4ce..39717f3dedc913 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm @@ -14,6 +14,10 @@ // The UI itself var/atom/movable/screen/cultivator_dantian/cultivator_ui + // min and max values for aurafarming + var/aura_min = CULTIVATOR_MIN_CULTIVATION_BONUS + var/aura_max = CULTIVATOR_MAX_CULTIVATION_BONUS + /datum/component/cultivator_dantian/Initialize() . = ..() if(!isliving(parent)) @@ -62,7 +66,7 @@ if(power.contributes_to_aura_farming && !power.active) // needs to have the contributing flag and not be active total += power.aura_farm() - total = clamp(total, CULTIVATOR_MIN_CULTIVATION_BONUS, CULTIVATOR_MAX_CULTIVATION_BONUS) + total = clamp(total, aura_min, aura_max) total *= seconds_per_tick // I love spess game time-based maths adjust_dantian(total) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm index 4bcbd35a8d4396..dd94be110844ed 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -72,6 +72,7 @@ var/big_natural_fire_object_value = CULTIVATOR_AURA_FARM_MODERATE // exposed flames that are intended and big e.g bonfires var/fire_turf_value = CULTIVATOR_AURA_FARM_MINOR // turfs being on fire e.g plasma fire var/smoking_value = CULTIVATOR_AURA_FARM_MODERATE // smoking is cool and good for aura. + var/lava_value = CULTIVATOR_AURA_FARM_MINOR // lava turfs: hot shit. var/others_on_fire_value = CULTIVATOR_AURA_FARM_MODERATE // someone else is on fire var/user_on_fire_value = CULTIVATOR_AURA_FARM_MAJOR // we're on fire @@ -95,8 +96,10 @@ /obj/structure/firepit )) - // Checks for hotspots aka is the engine on fire and does that let us aura farm? + // Checks for hotspots aka is the engine on fire and does that let us aura farm? Also checks for lava for(var/turf/open/open_turf in view(owner_mob)) + if(istype(open_turf, /turf/open/lava)) + total += lava_value if(open_turf.active_hotspot) total += fire_turf_value diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm new file mode 100644 index 00000000000000..94497d7236d4d9 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm @@ -0,0 +1,316 @@ +/datum/power/cultivator/many_stars + name = "The Many Stars that Dot the Endless Sky" + desc = "An active ability. Activating it sends forth a little star, which stops when it reaches it's destination (or hits an object) passively glowing in an area as a light source for 60 seconds. \ + ou can have up to 8 of these active. \ + While in alignment, you can right click with this ability to explode all active stars that are not in motion dealing 20 burn damage to all creatures in a 3x3 area centered on it. \ + Exploding the stars consumes Dantian per star. No cooldown." + + value = 5 + priority = POWER_PRIORITY_BASIC + required_powers = list(/datum/power/cultivator_root/astral_touched) + action_path = /datum/action/cooldown/power/cultivator/many_stars + +/datum/action/cooldown/power/cultivator/many_stars + name = "The Many Stars that Dot the Endless Sky" + desc = "Activating the ability sends forth a little star, which stops when it reaches it's destination (or hits an object) passively glowing in an area as a light source for 60 seconds. \ + ou can have up to 8 of these active. \ + While in alignment, you can right click with this ability to explode all active stars that are not in motion dealing 20 burn damage to all creatures in a 3x3 area centered on it. \ + Exploding the stars consumes Dantian per star. No cooldown." + button_icon = 'icons/mob/actions/actions_minor_antag.dmi' + button_icon_state = "beam_up" + + click_to_activate = TRUE + unset_after_click = FALSE + anti_magic_on_click_target = FALSE + + // icon & state + var/star_icon = 'icons/effects/eldritch.dmi' + var/star_state = "ring_leader_effect" + var/star_color = "#66c5dd" + + // how big are the stars + var/star_size = 0.7 + // how long do the stars last + var/star_duration = 60 SECONDS + // the light range + var/star_light_range = 5 + // the light's power (how strong of al ight) + var/star_light_power = 1 + // the max amount of stars + var/max_active_stars = 8 + // tracked stars + var/list/active_stars + + // Cooldown for shots in miliseconds. + var/next_star_shot_time = 0 + var/star_shot_delay = 3 + + // how much damage does the star do on explosion + var/star_explosion_damage = 20 + // the explosion effect icon + var/star_explosion_icon = 'icons/effects/effects.dmi' + var/star_explosion_state = "ion_fade" + // the explosion effect range + var/star_explosion_range = 1 + // the explosion sound + var/star_explosion_sound = 'sound/effects/magic/wandodeath.ogg' + + // Cached alignment action for gating effects. + var/datum/action/cooldown/power/cultivator/alignment/astral_touched/astral_alignment + +/datum/action/cooldown/power/cultivator/many_stars/use_action(mob/living/user, atom/target) + if(world.time < next_star_shot_time) + return FALSE + next_star_shot_time = world.time + star_shot_delay + if(fire_projectile(user, target, /obj/projectile/resonant/many_stars)) + playsound(user, 'sound/effects/magic/cosmic_energy.ogg', 60, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + return TRUE + return FALSE + +/datum/action/cooldown/power/cultivator/many_stars/InterceptClickOn(mob/living/clicker, params, atom/target) + var/list/modifiers = params2list(params) + if(LAZYACCESS(modifiers, RIGHT_CLICK)) // EXPLOSION + explode_active_stars(clicker) + return TRUE + . = ..() + if(clicker != owner) + return FALSE + return . + +// Checks where to place the star +/datum/action/cooldown/power/cultivator/many_stars/proc/can_place_star(turf/target_turf) + if(!target_turf || !isopenturf(target_turf)) + return FALSE + if(target_turf.is_blocked_turf(exclude_mobs = TRUE)) // space needs to not be blocked + return FALSE + if(locate(/obj/effect/many_stars_star) in target_turf) // space can't already have a star + return FALSE + return TRUE + +// Adds a star to the list and removes the oldest if it exceeds the max +/datum/action/cooldown/power/cultivator/many_stars/proc/register_star(obj/effect/many_stars_star/new_star) + if(!new_star) + return + LAZYINITLIST(active_stars) + active_stars += new_star + if(length(active_stars) > max_active_stars) + var/obj/effect/many_stars_star/oldest_star = active_stars[1] + if(oldest_star) + qdel(oldest_star) + +/datum/action/cooldown/power/cultivator/many_stars/proc/unregister_star(obj/effect/many_stars_star/old_star) + if(!active_stars || !old_star) + return + active_stars -= old_star + +// KA-BOOOOOOOM. +/datum/action/cooldown/power/cultivator/many_stars/proc/explode_active_stars(mob/living/user) + if(!is_astral_alignment_active(user)) + if(user) + user.balloon_alert(user, "alignment required!") + return + if(!active_stars || !length(active_stars)) + return + if(user) + user.log_message("detonated their Many Stars.", LOG_GAME) + + var/list/stars_to_explode = active_stars.Copy() + for(var/obj/effect/many_stars_star/star as anything in stars_to_explode) + if(QDELETED(star)) + continue + var/turf/star_turf = get_turf(star) + if(!star_turf) + continue + playsound(star_turf, star_explosion_sound, 75, TRUE) + star_turf.visible_message(span_bolddanger("The star explodes in a shower of light!")) + // applies damage, does cool effects, does logging. + for(var/turf/effect_turf in range(star_explosion_range, star_turf)) + var/obj/effect/temp_visual/dir_setting/speedbike_trail/effect = new(effect_turf) + effect.icon = star_explosion_icon + effect.icon_state = star_explosion_state + + for(var/mob/living/target in effect_turf) + var/dam_dealt = apply_damage_with_armor(target, star_explosion_damage, damage_type = astral_alignment?.alignment_damage_type || BURN, attack_flag = BOMB) + target.log_message("was hit by a Many Stars detonation from [user] for [dam_dealt] damage.", LOG_VICTIM) + if(user) + user.log_message("detonated Many Stars against [target] for [dam_dealt] damage.", LOG_ATTACK) + to_chat(target, span_userdanger("You are hit by an explosive blast of energy!")) + qdel(star) + +// Gets & sets astral allignment. We only really reference it in the explosion. +/datum/action/cooldown/power/cultivator/many_stars/proc/is_astral_alignment_active(mob/living/user) + if(!astral_alignment) + for(var/datum/action/cooldown/power/cultivator/alignment/astral_touched/alignment_action in user.actions) + astral_alignment = alignment_action + break + if(!astral_alignment) + return FALSE + return astral_alignment.active + +// Creates the lingering star on impact. +/datum/action/cooldown/power/cultivator/many_stars/proc/spawn_star(turf/impact_turf, turf/fallback_turf, obj/projectile/resonant/many_stars/source_projectile) + var/turf/placement_turf = null + if(can_place_star(impact_turf)) + placement_turf = impact_turf + else if(can_place_star(fallback_turf)) + placement_turf = fallback_turf + + if(!placement_turf) + return + + var/obj/effect/many_stars_star/new_star = new(placement_turf) + new_star.owner_power = src + new_star.lifespan = star_duration + new_star.light_range = star_light_range + new_star.light_power = star_light_power + // Copies the effects of the soruce projectile if possible. + if(source_projectile) + new_star.icon = source_projectile.icon + new_star.icon_state = source_projectile.icon_state + new_star.color = source_projectile.color + new_star.light_color = source_projectile.star_color ? source_projectile.star_color : source_projectile.color + new_star.star_size = source_projectile.star_size + else + new_star.icon = star_icon + new_star.icon_state = star_state + new_star.color = star_color + new_star.light_color = star_color + new_star.star_size = star_size + + new_star.apply_star_scale() + + register_star(new_star) + +// Applies the configured effects to the star. +/datum/action/cooldown/power/cultivator/many_stars/ready_projectile(obj/projectile/projectile_instance, atom/target, mob/living/user) + . = ..() + if(!projectile_instance || !istype(projectile_instance, /obj/projectile/resonant/many_stars)) + return + + // Applies the icon, state and color to the projectie. + var/obj/projectile/resonant/many_stars/star_proj = projectile_instance + star_proj.star_icon = star_icon + star_proj.star_state = star_state + star_proj.star_color = star_color + + if(star_icon) + star_proj.icon = star_icon + if(star_state) + star_proj.icon_state = star_state + if(star_color) + star_proj.color = star_color + if(star_size) + star_proj.star_size = star_size + star_proj.apply_star_scale() + + // Applies the light to hte projectile. + star_proj.light_range = star_light_range + star_proj.light_power = star_light_power + star_proj.light_color = star_color ? star_color : star_proj.color + star_proj.light_on = TRUE + star_proj.set_light(star_light_range, star_light_power, star_color ? star_color : star_proj.color, l_on = TRUE) + + var/turf/target_turf = get_turf(target) + star_proj.target_turf = target_turf + +/obj/projectile/resonant/many_stars + name = "star" + icon = 'icons/effects/eldritch.dmi' + icon_state = "ring_leader_effect" + + // icon state & color data saved + var/star_icon + var/star_state + var/star_color + var/star_size = 0.5 + var/turf/last_passed_turf + var/turf/target_turf + var/reached_target = FALSE + +// Tracks the last space we were in +/obj/projectile/resonant/many_stars/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change = TRUE) + . = ..() + if(old_loc) + last_passed_turf = get_turf(old_loc) + if(!reached_target && target_turf && get_turf(src) == target_turf) // if we're at the click target we stop + reached_target = TRUE + deletion_queued = PROJECTILE_RANGE_DELETE + +// Runs the star spawning on hit +/obj/projectile/resonant/many_stars/on_hit(atom/target, blocked, pierce_hit) + . = ..() + var/turf/impact_turf = get_turf(target) + var/datum/action/cooldown/power/cultivator/many_stars/power = creating_power + if(power) + power.spawn_star(impact_turf, last_passed_turf, src) + +// If we cap out on range +/obj/projectile/resonant/many_stars/on_range() + . = ..() + var/turf/impact_turf = get_turf(src) + var/datum/action/cooldown/power/cultivator/many_stars/power = creating_power + if(power) + power.spawn_star(impact_turf, last_passed_turf, src) + +// Applies the size to the projectile +/obj/projectile/resonant/many_stars/proc/apply_star_scale() + if(!star_size) + return + var/matrix/scale_matrix = matrix() + scale_matrix.Scale(star_size, star_size) + transform = scale_matrix + +// The lingering star effect +/obj/effect/many_stars_star + name = "star" + icon = 'icons/effects/eldritch.dmi' + icon_state = "ring_leader_effect" + anchored = TRUE + density = FALSE + max_integrity = 1 + light_range = 3 + light_power = 1 + light_color = "#66c5dd" + var/star_size = 0.5 + var/lifespan = 60 SECONDS + var/datum/action/cooldown/power/cultivator/many_stars/owner_power + +// Adds expiration timer and size modifier. +/obj/effect/many_stars_star/Initialize(mapload) + . = ..() + apply_star_scale() + if(lifespan) + addtimer(CALLBACK(src, PROC_REF(expire)), lifespan) + +/obj/effect/many_stars_star/Destroy() + if(owner_power) + owner_power.unregister_star(src) + owner_power = null + return ..() + +/obj/effect/many_stars_star/proc/expire() + qdel(src) + +// Dissipate harmlessly on attack +/obj/effect/many_stars_star/attackby(obj/item/attacking_item, mob/living/user, list/modifiers, list/attack_modifiers) + . = ..() + if(attacking_item?.force) + if(user) + to_chat(user, span_notice("You interact with the star, and it vanishes!")) + qdel(src) + return . + +// Same with an unarmed touch. +/obj/effect/many_stars_star/attack_hand(mob/living/user, list/modifiers) + . = ..() + if(user) + to_chat(user, span_notice("You interact with the star, and it vanishes!")) + qdel(src) + return . + +/obj/effect/many_stars_star/proc/apply_star_scale() + if(!star_size) + return + var/matrix/scale_matrix = matrix() + scale_matrix.Scale(star_size, star_size) + transform = scale_matrix diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm index b5a847a805cd73..dfe3e5af9390ec 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm @@ -209,5 +209,5 @@ if(!active) return NONE examine_list.Cut() - examine_list += span_notice("It's too shrouded in shadow to make out any details.") + examine_list += span_warning("It's too shrouded in shadow to make out any details.") return NONE diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 685994f24c4660..d6a3c3cb5ee835 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -118,6 +118,30 @@ // Cost systems for archetypes to name an example. /datum/action/cooldown/power/proc/on_action_success(mob/living/user, atom/target) return + +// Applies damage to a living target, automatically applying an armor check. +// Returns the amount of damage dealt (as per apply_damage). +/datum/action/cooldown/power/proc/apply_damage_with_armor( + mob/living/target, + damage, + damage_type = BRUTE, + attack_flag = MELEE, + def_zone = null, + armour_penetration = 0, + weak_against_armour = FALSE, + silent = TRUE, +) + if(!target) + return 0 + + var/armor_block = target.run_armor_check( + def_zone = def_zone, + attack_flag = attack_flag, + armour_penetration = armour_penetration, + weak_against_armour = weak_against_armour, + silent = silent, + ) + return target.apply_damage(damage, damage_type, def_zone, armor_block) /* Handles all the logic involved in using a targeted, click-based action. - First press: enables click intercept (targeting mode) diff --git a/tgstation.dme b/tgstation.dme index 83d2816c913a61..e2ceb64bec224d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7472,6 +7472,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\astraltouched_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\flamesoul_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\many_stars.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\shadowwalker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_organ.dm" From 50ead51d05a3f481f8e9b48efb89d10f3a1bbb5e Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 24 Feb 2026 07:40:22 +0100 Subject: [PATCH 088/212] Finishes many_stars (I swear for real this time). Minor tweaks to things cause I swear to god I don't have dislexyia --- code/__DEFINES/~doppler_defines/powers.dm | 4 ++-- .../mortal/warfighter/martial_artist.dm | 2 +- .../cultivator/_cultivator_alignment.dm | 2 +- .../resonant/cultivator/astraltouched_root.dm | 2 +- .../resonant/cultivator/flamesoul_root.dm | 2 +- .../powers/resonant/cultivator/many_stars.dm | 21 +++++++++++-------- .../resonant/cultivator/shadowwalker_root.dm | 2 +- .../modular_powers/code/powers_action.dm | 11 +--------- 8 files changed, 20 insertions(+), 26 deletions(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index be0c2284734e71..e1a1f2dd104ec5 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -161,8 +161,8 @@ // UI location of the Cultivator element #define CULTIVATOR_UI_SCREEN_LOC "WEST,CENTER-2:15" -// Bonus damage on strikes done by alignment -#define CULTIVATOR_ALIGNMENT_DAMAGE_BONUS 10 +// Bonus damage on strikes done while in alignment. Balancing notes: punches have a base 20% miss chance, and this does not stack with martial arts. +#define CULTIVATOR_ALIGNMENT_DAMAGE_BONUS 15 // The max amount of Dantian we give from aura farming per second #define CULTIVATOR_MAX_CULTIVATION_BONUS 3 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 29b5ee0a83980e..d18244174cf11e 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 @@ -22,5 +22,5 @@ SIGNAL_HANDLER if(!target || bonus_damage <= 0) return - target.apply_damage(bonus_damage, BRUTE, affecting, armor_block, sharpness = limb_sharpness) + apply_damage_with_armor(target, bonus_damage, BRUTE, affecting, armor_block, sharpness = limb_sharpness, attack_flag = MELEE) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm index 729e12a9cc6fad..e67c2f950474d7 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm @@ -47,7 +47,7 @@ if(!active) return if(alignment_damage_bonus) - target.apply_damage(alignment_damage_bonus, alignment_damage_type, affecting, armor_block, sharpness = limb_sharpness) + apply_damage_with_armor(target, alignment_damage_bonus, alignment_damage_type, affecting, armor_block, sharpness = limb_sharpness, attack_flag = MELEE) // Basically handles active state and activation fx. Override as needed; but please make sure to get the essentials. /datum/action/cooldown/power/cultivator/alignment/use_action(mob/living/carbon/user) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm index 63b166a40d3549..ca4dfb84b473f9 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -1,6 +1,6 @@ /datum/power/cultivator_root/astral_touched name = "Astral Touched Alignment" - desc = "You gain your Dantian's aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do 10 extra burn damage.\ + desc = "You gain your Dantian's aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do extra burn damage.\ Passively, your cold temprature tolerance is increased by 30C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\ You gain armor IV across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/astral_touched diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm index dd94be110844ed..e707546a1999a9 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -1,6 +1,6 @@ /datum/power/cultivator_root/flame_soul name = "Flame Soul Alignment" - desc = "You gain your Dantian's aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do 10 extra burn damage.\ + desc = "You gain your Dantian's aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do extra burn damage.\ Passively, your high temprature threshold is increased by 30C regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ You gain armor III (with laser VI) across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/flame_soul diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm index 94497d7236d4d9..064b8564fcae10 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm @@ -16,8 +16,8 @@ ou can have up to 8 of these active. \ While in alignment, you can right click with this ability to explode all active stars that are not in motion dealing 20 burn damage to all creatures in a 3x3 area centered on it. \ Exploding the stars consumes Dantian per star. No cooldown." - button_icon = 'icons/mob/actions/actions_minor_antag.dmi' - button_icon_state = "beam_up" + button_icon = 'icons/effects/eldritch.dmi' + button_icon_state = "ring_leader_effect" click_to_activate = TRUE unset_after_click = FALSE @@ -122,18 +122,15 @@ if(!star_turf) continue playsound(star_turf, star_explosion_sound, 75, TRUE) - star_turf.visible_message(span_bolddanger("The star explodes in a shower of light!")) + star_turf.visible_message(span_bolddanger("The star explodes in a wave of energy!")) // applies damage, does cool effects, does logging. + new /obj/effect/temp_visual/circle_wave/many_stars(star_turf) for(var/turf/effect_turf in range(star_explosion_range, star_turf)) - var/obj/effect/temp_visual/dir_setting/speedbike_trail/effect = new(effect_turf) - effect.icon = star_explosion_icon - effect.icon_state = star_explosion_state - for(var/mob/living/target in effect_turf) var/dam_dealt = apply_damage_with_armor(target, star_explosion_damage, damage_type = astral_alignment?.alignment_damage_type || BURN, attack_flag = BOMB) - target.log_message("was hit by a Many Stars detonation from [user] for [dam_dealt] damage.", LOG_VICTIM) + target.log_message("was hit by a Many Stars detonation from [user] for [dam_dealt] damage.", LOG_VICTIM, color="blue") if(user) - user.log_message("detonated Many Stars against [target] for [dam_dealt] damage.", LOG_ATTACK) + user.log_message("detonated Many Stars against [target] for [dam_dealt] damage.", LOG_ATTACK, color="red") to_chat(target, span_userdanger("You are hit by an explosive blast of energy!")) qdel(star) @@ -314,3 +311,9 @@ var/matrix/scale_matrix = matrix() scale_matrix.Scale(star_size, star_size) transform = scale_matrix + +// The aoe explosion circle from many_stars +/obj/effect/temp_visual/circle_wave/many_stars + color = COLOR_CYAN + duration = 0.5 SECONDS + amount_to_scale = 1.5 diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm index dfe3e5af9390ec..dea73fbf8b2c74 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm @@ -1,7 +1,7 @@ /datum/power/cultivator_root/shadow_walker name = "Shadow Walker Alignment" desc = "You gain your Dantian's aura through dark rooms and environments. Activating it wraps you in an aura of shadow.\ - You are entirely unrecognizeable in this state and your punches do 10 extra brute damage.\ + You are entirely unrecognizeable in this state and your punches do extra brute damage.\ Passively, you have enhanced darkvision, and gain full on night vision while your alignment is activated.\ You gain armor IV across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/shadow_walker diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index d6a3c3cb5ee835..30e0bb221905b7 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -121,16 +121,7 @@ // Applies damage to a living target, automatically applying an armor check. // Returns the amount of damage dealt (as per apply_damage). -/datum/action/cooldown/power/proc/apply_damage_with_armor( - mob/living/target, - damage, - damage_type = BRUTE, - attack_flag = MELEE, - def_zone = null, - armour_penetration = 0, - weak_against_armour = FALSE, - silent = TRUE, -) +/datum/action/cooldown/power/proc/apply_damage_with_armor(mob/living/target, damage, damage_type = BRUTE, attack_flag = MELEE, def_zone = null, armour_penetration = 0, weak_against_armour = FALSE, silent = TRUE) if(!target) return 0 From 82b1a6ec9a755100cf4e11fd6a0964ad2cac72a8 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 24 Feb 2026 10:38:06 +0100 Subject: [PATCH 089/212] Finishes out the kit for astral touched. Added cost to everything. Tweaked Levitate to use AddElementTrait instead. --- code/__DEFINES/~doppler_defines/powers.dm | 26 +++++++-- .../mortal/warfighter/martial_artist.dm | 2 +- .../resonant/cultivator/_cultivator_action.dm | 5 +- .../cultivator/_cultivator_alignment.dm | 21 ++++++- .../cultivator/_cultivator_dantian.dm | 10 ++++ .../resonant/cultivator/astraltouched_root.dm | 2 +- .../cultivator/fly_like_a_shooting_star.dm | 57 +++++++++++++++++++ .../powers/resonant/cultivator/many_stars.dm | 35 ++++++++---- .../code/powers/resonant/psyker/levitate.dm | 10 ++-- .../powers/sorcerous/_resonant_projectile.dm | 2 +- tgstation.dme | 1 + 11 files changed, 143 insertions(+), 28 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/fly_like_a_shooting_star.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index e1a1f2dd104ec5..b1838df2da85f3 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -43,6 +43,9 @@ // Trait for when you are immune to resonant powers that reveal any information about you. #define TRAIT_ANTIRESONANCE_SCRYING "TRAIT_ANTIRESONANCE_SCRYING" +// How much anti resonant stuff should cost by default +#define ANTIRESONANCE_BASE_CHARGE_COST 1 + // Listener for dispelling #define COMSIG_ATOM_DISPEL "atom_dispel" @@ -156,7 +159,7 @@ #define CULTIVATOR_DANTIAN_MAX 1000 // How much dantian we get from meditation every 2.5 seconds -#define CULTIVATOR_DANTIAN_MEDITATION_POWER 3 +#define CULTIVATOR_DANTIAN_MEDITATION_POWER 5 // UI location of the Cultivator element #define CULTIVATOR_UI_SCREEN_LOC "WEST,CENTER-2:15" @@ -169,8 +172,13 @@ // The min amount of Dantian we give from aura farming per second #define CULTIVATOR_MIN_CULTIVATION_BONUS 0 -// Standard Dantian cost defines for Cultivators. Since it scales funny it has a 1/1000 called paltry. -#define CULTIVATOR_DANTIAN_PALTRY (CULTIVATOR_DANTIAN_MAX / 1000) +// How much does activating the alignment cost +#define CULTIVATOR_ALIGNMENT_ACTIVATION_COST 200 + +// How much does sustaining the alignment cost +#define CULTIVATOR_ALIGNMENT_UPKEEP_COST 3 + +// Standard Dantian cost defines for Cultivators. #define CULTIVATOR_DANTIAN_TRIVIAL (CULTIVATOR_DANTIAN_MAX / 100) #define CULTIVATOR_DANTIAN_MINOR (CULTIVATOR_DANTIAN_MAX / 10) #define CULTIVATOR_DANTIAN_MODERATE (CULTIVATOR_DANTIAN_MAX / 5) @@ -184,6 +192,13 @@ #define CULTIVATOR_AURA_FARM_MAJOR (CULTIVATOR_MAX_CULTIVATION_BONUS / 2) #define CULTIVATOR_AURA_FARM_CRUSHING (CULTIVATOR_MAX_CULTIVATION_BONUS) +// Cultivator alignment activion/deactivation signals +#define COMSIG_CULTIVATOR_ALIGNMENT_ENABLED "cultivator_alignment_enabled" +#define COMSIG_CULTIVATOR_ALIGNMENT_DISABLED "cultivator_alignment_disabled" + +// The trait for Astral Touched's flight upgrades (using AddElementTrait) +#define TRAIT_ASTRAL_TOUCHED_FLIGHT "astral_touched_flight" + /** * RESONANT: PSYKER * All defines related to the enigmatist powers. @@ -217,7 +232,10 @@ #define PSYKER_EVENT_RARITY_VERYRARE 10 // Standard messages for Psyker Events -#define PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE "As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity; as well as a feeling that something is very wrong." +#define PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE "As you strain your psychic powers past the breaking point, you are suddenly hit with a strange sense of clarity; as well as a feeling that something is very wrong." + +// The trait for Psyker's Levitate power. +#define TRAIT_PSYKER_LEVITATE_FLIGHT "psyker_levitate_flight" /**MORTAL DEFINES * I'm literally just using this to define Breacher Knuckle right now 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 d18244174cf11e..29b5ee0a83980e 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 @@ -22,5 +22,5 @@ SIGNAL_HANDLER if(!target || bonus_damage <= 0) return - apply_damage_with_armor(target, bonus_damage, BRUTE, affecting, armor_block, sharpness = limb_sharpness, attack_flag = MELEE) + target.apply_damage(bonus_damage, BRUTE, affecting, armor_block, sharpness = limb_sharpness) 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 a08c98865407d4..1281a06c89f40e 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 @@ -12,7 +12,8 @@ // Cost in Dantian to use var/cost - + // Bypasses the cost check while active. On_action_success still subtracts it as normal. + var/bypass_cost // Does this power get called by _cultivator_dantian.dm when we check for aura farming? Used for potential future powers that allow you to aura farm in other ways. var/contributes_to_aura_farming = FALSE @@ -47,7 +48,7 @@ if(!ValidateDantianComponent()) owner.balloon_alert(owner, "Yell at the coders; you're missing your dantian system!") return FALSE - if(dantian_component.dantian < cost) + if(dantian_component.dantian < cost && !bypass_cost) user.balloon_alert(user, "needs [cost] dantian!") return FALSE . = .. () diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm index e67c2f950474d7..6bf9ba9e2a9fce 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm @@ -31,9 +31,13 @@ var/alignment_damage_type = BRUTE // The bonus damage for the alignment var/alignment_damage_bonus = CULTIVATOR_ALIGNMENT_DAMAGE_BONUS + // The upkeep cost of the alignment + var/alignment_upkeep_cost = CULTIVATOR_ALIGNMENT_UPKEEP_COST cooldown_time = 5 // to prevent spam-clicking it off contributes_to_aura_farming = TRUE // needs to be always be on or you won't get aura from alignment + cost = CULTIVATOR_ALIGNMENT_ACTIVATION_COST + // Removes stray listeners. /datum/action/cooldown/power/cultivator/alignment/Destroy() . = ..() @@ -47,17 +51,15 @@ if(!active) return if(alignment_damage_bonus) - apply_damage_with_armor(target, alignment_damage_bonus, alignment_damage_type, affecting, armor_block, sharpness = limb_sharpness, attack_flag = MELEE) + apply_damage_with_armor(target, alignment_damage_bonus, alignment_damage_type, affecting, armor_block, attack_flag = MELEE) // Basically handles active state and activation fx. Override as needed; but please make sure to get the essentials. /datum/action/cooldown/power/cultivator/alignment/use_action(mob/living/carbon/user) if(!active) // If inactive, we activate (if we can pay the cost) enable_alignment(user) - active = TRUE return TRUE if(active) // If active, we disable. disable_alignment(user) - active = FALSE return TRUE return FALSE @@ -92,15 +94,20 @@ // Everything that needs to happen when enabling alignment /datum/action/cooldown/power/cultivator/alignment/proc/enable_alignment(mob/living/carbon/user) + active = TRUE + bypass_cost = TRUE // makes it so we don't check for cost next time. activation_fx(user) RegisterSignal(user, COMSIG_HUMAN_UNARMED_HIT, PROC_REF(on_unarmed_hit)) RegisterSignal(user, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_equipment_changed)) RegisterSignal(user, COMSIG_MOB_UNEQUIPPED_ITEM, PROC_REF(on_equipment_changed)) recompute_alignment_armor(user) + SEND_SIGNAL(user, COMSIG_CULTIVATOR_ALIGNMENT_ENABLED, src) return TRUE // Everything that needs to happen when disabling alignment /datum/action/cooldown/power/cultivator/alignment/proc/disable_alignment(mob/living/carbon/user) + active = FALSE + bypass_cost = FALSE if(alignment_overlay) user.cut_overlay(alignment_overlay) QDEL_NULL(alignment_overlay) @@ -108,8 +115,16 @@ user.remove_filter(filter_id) remove_alignment_armor() QDEL_NULL(alignment_light) + SEND_SIGNAL(user, COMSIG_CULTIVATOR_ALIGNMENT_DISABLED, src) return TRUE +// Deactivating the power doesn't cost anything so we skip the cost component. +/datum/action/cooldown/power/cultivator/alignment/on_action_success(mob/living/carbon/user) + if(!active) + return + else + . = ..() + /* Below is the big scary block of 'how to maths out other armor' Because we want armor to be a minimum of 40, we need to COMPARE it against the current armor and change the armor values on the fly. diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm index 39717f3dedc913..3427243dde2ea4 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm @@ -58,6 +58,16 @@ /datum/component/cultivator_dantian/process(seconds_per_tick) if(!attached_mob) return + + // Handles upkeep for alignment powers. + for(var/datum/action/cooldown/power/cultivator/alignment/power in attached_mob.actions) + if(power.active) + adjust_dantian(-(power.alignment_upkeep_cost * seconds_per_tick)) + if(dantian <= 0) // disable if we're out of dantian + to_chat(attached_mob, span_boldwarning("You've ran out of Dantian!")) + power.disable_alignment(attached_mob) + + // Aura farming code below if(HAS_TRAIT(attached_mob, TRAIT_RESONANCE_SILENCED)) // no aura farming when silenced return // Just for the sake of future proofing, you can have multiple sources of aura farming. diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm index ca4dfb84b473f9..4d109c4f1a4f14 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -28,7 +28,7 @@ button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "teleport" - alignment_outline_color = "#66c5dd" + alignment_outline_color = "#c1effa" alignment_activation_sound = 'sound/effects/magic/cosmic_energy.ogg' alignment_overlay_state = "shieldsparkles" diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/fly_like_a_shooting_star.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/fly_like_a_shooting_star.dm new file mode 100644 index 00000000000000..c5dbdc910e18d4 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/fly_like_a_shooting_star.dm @@ -0,0 +1,57 @@ +/datum/power/cultivator/fly_like_a_shooting_star + name = "Fly Like A Shooting Star" + desc = "Whilst your alignment is active, you can fly. You can propel yourself through the air and space as if wearing a jetpack. \ + If you aren't able to use your legs, you're able to move around with this ability, regardless of the current gravity." + + value = 3 + required_powers = list(/datum/power/cultivator_root/astral_touched) + + // the trailing particles + var/datum/effect_system/trail_follow/ion/grav_allowed/flight_trail + // ref to the root power's action + var/datum/action/cooldown/power/cultivator/alignment/astral_touched/astral_alignment + +/datum/power/cultivator/fly_like_a_shooting_star/add(client/client_source) + . = ..() + if(!power_holder) + return + RegisterSignal(power_holder, COMSIG_CULTIVATOR_ALIGNMENT_ENABLED, PROC_REF(on_alignment_enabled)) + RegisterSignal(power_holder, COMSIG_CULTIVATOR_ALIGNMENT_DISABLED, PROC_REF(on_alignment_disabled)) + +/datum/power/cultivator/fly_like_a_shooting_star/remove() + if(power_holder) + UnregisterSignal(power_holder, list(COMSIG_CULTIVATOR_ALIGNMENT_ENABLED, COMSIG_CULTIVATOR_ALIGNMENT_DISABLED)) + remove_flight(power_holder) + . = ..() + +/datum/power/cultivator/fly_like_a_shooting_star/proc/on_alignment_enabled(mob/living/user, datum/action/cooldown/power/cultivator/alignment/alignment_action) + SIGNAL_HANDLER + if(!istype(alignment_action, /datum/action/cooldown/power/cultivator/alignment/astral_touched)) + return + apply_flight(user) + +/datum/power/cultivator/fly_like_a_shooting_star/proc/on_alignment_disabled(mob/living/user, datum/action/cooldown/power/cultivator/alignment/alignment_action) + SIGNAL_HANDLER + if(!istype(alignment_action, /datum/action/cooldown/power/cultivator/alignment/astral_touched)) + return + remove_flight(user) + +// Adds the flight traits and particles on alignment activation +/datum/power/cultivator/fly_like_a_shooting_star/proc/apply_flight(mob/living/user) + if(!user) + return + user.AddElementTrait(TRAIT_ASTRAL_TOUCHED_FLIGHT, REF(src), /datum/element/forced_gravity, 0) + user.AddElementTrait(TRAIT_ASTRAL_TOUCHED_FLIGHT, REF(src), /datum/element/simple_flying) + if(!flight_trail) + flight_trail = new + flight_trail.set_up(user) + flight_trail.start() + +// Removes the flight trait and particles on alignment deactivation +/datum/power/cultivator/fly_like_a_shooting_star/proc/remove_flight(mob/living/user) + if(!user) + return + REMOVE_TRAIT(user, TRAIT_ASTRAL_TOUCHED_FLIGHT, REF(src)) + if(flight_trail) + flight_trail.stop() + QDEL_NULL(flight_trail) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm index 064b8564fcae10..d2642ae4437f7a 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm @@ -6,13 +6,12 @@ Exploding the stars consumes Dantian per star. No cooldown." value = 5 - priority = POWER_PRIORITY_BASIC required_powers = list(/datum/power/cultivator_root/astral_touched) action_path = /datum/action/cooldown/power/cultivator/many_stars /datum/action/cooldown/power/cultivator/many_stars name = "The Many Stars that Dot the Endless Sky" - desc = "Activating the ability sends forth a little star, which stops when it reaches it's destination (or hits an object) passively glowing in an area as a light source for 60 seconds. \ + desc = "Activating the ability sends forth a little star, which stops when it reaches it's destination (or hits an object) passively glowing in an area as a light source for 5 minutes. \ ou can have up to 8 of these active. \ While in alignment, you can right click with this ability to explode all active stars that are not in motion dealing 20 burn damage to all creatures in a 3x3 area centered on it. \ Exploding the stars consumes Dantian per star. No cooldown." @@ -26,12 +25,12 @@ // icon & state var/star_icon = 'icons/effects/eldritch.dmi' var/star_state = "ring_leader_effect" - var/star_color = "#66c5dd" + var/star_color = "#c1effa" // how big are the stars var/star_size = 0.7 // how long do the stars last - var/star_duration = 60 SECONDS + var/star_duration = 300 SECONDS // the light range var/star_light_range = 5 // the light's power (how strong of al ight) @@ -46,14 +45,15 @@ var/star_shot_delay = 3 // how much damage does the star do on explosion - var/star_explosion_damage = 20 - // the explosion effect icon - var/star_explosion_icon = 'icons/effects/effects.dmi' - var/star_explosion_state = "ion_fade" + var/star_explosion_damage = 25 // the explosion effect range var/star_explosion_range = 1 // the explosion sound var/star_explosion_sound = 'sound/effects/magic/wandodeath.ogg' + // the dantian cost for exploding the stars + var/star_explosion_cost = CULTIVATOR_DANTIAN_TRIVIAL * 2 + // the dantian cost per star + var/star_explosion_cost_per_star = CULTIVATOR_DANTIAN_TRIVIAL // Cached alignment action for gating effects. var/datum/action/cooldown/power/cultivator/alignment/astral_touched/astral_alignment @@ -111,10 +111,13 @@ return if(!active_stars || !length(active_stars)) return + if(dantian_component.dantian < star_explosion_cost) + user.balloon_alert(user, "needs more dantian!") if(user) user.log_message("detonated their Many Stars.", LOG_GAME) var/list/stars_to_explode = active_stars.Copy() + adjust_dantian(-star_explosion_cost) // removes the base cost for(var/obj/effect/many_stars_star/star as anything in stars_to_explode) if(QDELETED(star)) continue @@ -124,9 +127,13 @@ playsound(star_turf, star_explosion_sound, 75, TRUE) star_turf.visible_message(span_bolddanger("The star explodes in a wave of energy!")) // applies damage, does cool effects, does logging. - new /obj/effect/temp_visual/circle_wave/many_stars(star_turf) + var/obj/effect/temp_visual/circle_wave/explosion_fx = new /obj/effect/temp_visual/circle_wave/many_stars(star_turf) + explosion_fx.color = star_color for(var/turf/effect_turf in range(star_explosion_range, star_turf)) for(var/mob/living/target in effect_turf) + // We skip this whole thing if the mob is immune to resonance + if(target.can_block_resonance(ANTIRESONANCE_BASE_CHARGE_COST) || target.can_block_magic(MAGIC_RESISTANCE, ANTIRESONANCE_BASE_CHARGE_COST)) + continue var/dam_dealt = apply_damage_with_armor(target, star_explosion_damage, damage_type = astral_alignment?.alignment_damage_type || BURN, attack_flag = BOMB) target.log_message("was hit by a Many Stars detonation from [user] for [dam_dealt] damage.", LOG_VICTIM, color="blue") if(user) @@ -134,6 +141,14 @@ to_chat(target, span_userdanger("You are hit by an explosive blast of energy!")) qdel(star) + // Removes cost per star; if we end up at 0, explode no more stars and shut off their power. + adjust_dantian(-star_explosion_cost_per_star) + if(dantian_component.dantian <= 0) + user.balloon_alert(user, "no more dantian!") + if(astral_alignment.active) + astral_alignment.disable_alignment(user) + break + // Gets & sets astral allignment. We only really reference it in the explosion. /datum/action/cooldown/power/cultivator/many_stars/proc/is_astral_alignment_active(mob/living/user) if(!astral_alignment) @@ -269,7 +284,7 @@ light_power = 1 light_color = "#66c5dd" var/star_size = 0.5 - var/lifespan = 60 SECONDS + var/lifespan = 300 SECONDS var/datum/action/cooldown/power/cultivator/many_stars/owner_power // Adds expiration timer and size modifier. 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 221ec47191f624..3b662308188de1 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -20,8 +20,8 @@ /datum/action/cooldown/power/psyker/levitate/use_action() . = ..() if(!active) - owner.AddElement(/datum/element/forced_gravity, 0) - owner.AddElement(/datum/element/simple_flying) + owner.AddElementTrait(TRAIT_PSYKER_LEVITATE_FLIGHT, REF(src), /datum/element/forced_gravity, 0) + owner.AddElementTrait(TRAIT_PSYKER_LEVITATE_FLIGHT, REF(src), /datum/element/simple_flying) to_chat(owner, span_boldnotice("Your body gently floats in the air!")) START_PROCESSING(SSfastprocess, src) active = TRUE @@ -36,8 +36,7 @@ owner.add_overlay(caster_effect) playsound(owner, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) else - owner.RemoveElement(/datum/element/forced_gravity, 0) - owner.RemoveElement(/datum/element/simple_flying) + REMOVE_TRAIT(owner, TRAIT_PSYKER_LEVITATE_FLIGHT, REF(src)) to_chat(owner, span_boldnotice("You let yourself gently drop the ground.")) STOP_PROCESSING(SSfastprocess, src) active = FALSE @@ -80,8 +79,7 @@ var/mob/living/carbon/human/victim = owner if(active) - owner.RemoveElement(/datum/element/forced_gravity, 0) - owner.RemoveElement(/datum/element/simple_flying) + REMOVE_TRAIT(owner, TRAIT_PSYKER_LEVITATE_FLIGHT, REF(src)) STOP_PROCESSING(SSfastprocess, src) active = FALSE // visual fx diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm b/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm index fd9df1c3c2f004..f3462e887150ea 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/_resonant_projectile.dm @@ -11,7 +11,7 @@ // We have to play coy with the existing magic resistance system, for checking against resonance use victim.can_block_resonance(antimagic_charge_cost) var/antimagic_flags = MAGIC_RESISTANCE /// determines the drain cost on the antimagic item - var/antimagic_charge_cost = 1 + var/antimagic_charge_cost = ANTIRESONANCE_BASE_CHARGE_COST // The power that made the projectile. var/datum/action/cooldown/power/creating_power diff --git a/tgstation.dme b/tgstation.dme index e2ceb64bec224d..3b5e1296a4116b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7472,6 +7472,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\astraltouched_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\flamesoul_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\fly_like_a_shooting_star.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\many_stars.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\shadowwalker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" From ad3e844deea2f9d9b9cb4f9eef1626835e408f19 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 25 Feb 2026 14:19:14 +0100 Subject: [PATCH 090/212] Adds set_fire_to_dry_hay, which lets you set stuff on fire and in alignment shoot stuff to set it on fire. Tweaked some backend stuff to do with targeting. --- .../resonant/cultivator/_cultivator_action.dm | 5 +- .../powers/resonant/cultivator/many_stars.dm | 2 +- .../cultivator/set_fire_to_dry_hay.dm | 174 ++++++++++++++++++ .../sorcerous/thaumaturge/conjure_rain.dm | 2 +- .../sorcerous/thaumaturge/gale_blast.dm | 2 +- .../sorcerous/thaumaturge/magic_barrage.dm | 2 +- .../theologist/_theologist_action.dm | 3 +- .../modular_powers/code/powers_action.dm | 16 +- tgstation.dme | 1 + 9 files changed, 193 insertions(+), 14 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm 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 1281a06c89f40e..46d48bce50043e 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 @@ -17,9 +17,11 @@ // Does this power get called by _cultivator_dantian.dm when we check for aura farming? Used for potential future powers that allow you to aura farm in other ways. var/contributes_to_aura_farming = FALSE -/datum/action/cooldown/power/cultivator/New() + +/datum/action/cooldown/power/cultivator/Grant(mob/grant_to) . = ..() ValidateDantianComponent() + return . // Feng Shui / Aura farming mechanics; get stuff in the environment, increase dantian based on it // The func should be responsible for checking all the environmental stuff, calculating it and then returning it to the dantian system. @@ -58,4 +60,3 @@ if(cost) adjust_dantian(-cost) return - diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm index d2642ae4437f7a..59e857cf1b57b5 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm @@ -20,7 +20,7 @@ click_to_activate = TRUE unset_after_click = FALSE - anti_magic_on_click_target = FALSE + anti_magic_on_target = FALSE // icon & state var/star_icon = 'icons/effects/eldritch.dmi' diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm new file mode 100644 index 00000000000000..82017303618cd7 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm @@ -0,0 +1,174 @@ +/datum/power/cultivator/set_fire_to_dry_hay + name = "Set Fire to Dry Hay" + desc = "You can set fire onto anything you touch. This works similary to a ligher in terms of functionality. \ + While in Alignment, you can right click shoot a flameblast that ignite everything in the area where it lands. \ + Using the alignment version consumes Dantian. No cooldown." + + value = 5 + required_powers = list(/datum/power/cultivator_root/flame_soul) + action_path = /datum/action/cooldown/power/cultivator/set_fire_to_dry_hay + +/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay + name = "Set Fire to Dry Hay" + desc = "You can set fire onto anything you touch. This works similary to a lighter in terms of functionality. \ + While in Alignment, you can right click to shoot a flameblast that ignite everything in the area where it lands. \ + Using the alignment version consumes Dantian. No cooldown." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "fireball" + + click_to_activate = TRUE + unset_after_click = FALSE + + // Cooldown for right click projectile, in deciseconds. + var/projectile_delay = 5 + var/next_projectile_time = 0 + + // cost for flameblast + var/flameblast_cost = 20 + // icon, state and scale of flameblast + var/flameblast_icon = null + var/flameblast_icon_state = "fireball" + var/flameblast_scale = 0.5 + // light range, power and color of flameblast + var/flameblast_light_range = 3 + var/flameblast_light_power = 1 + var/flameblast_light_color = "#e99a3f" + // damage & firestacks of flameblast + var/flameblast_damage = 10 + var/flameblast_firestacks = 0.7 + // the sound of flameblast impacting + var/flameblast_impact_sound = 'sound/effects/fire_puff.ogg' + // Cached alignment action for gating right click effects. + var/datum/action/cooldown/power/cultivator/alignment/flame_soul/flame_soul_alignment + +/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/use_action(mob/living/user, atom/target) + if(!target) + return FALSE + // Lighter version only works in melee range. + var/turf/user_turf = get_turf(user) + var/turf/target_turf = get_turf(target) + if(user_turf && target_turf && get_dist(user_turf, target_turf) > 1) + owner.balloon_alert(user, "Out of range!") + return FALSE + var/obj/item/cultivator_virtual_lighter/lighter = new + // Allow the lighter's cigarette lighting behavior on mobs. + if(isliving(target)) + var/mob/living/target_mob = target + lighter.attack(target_mob, user, list(), list()) + qdel(lighter) + return TRUE + + // Only ignite flammable targets. + if((target.resistance_flags & FLAMMABLE) && !(target.resistance_flags & FIRE_PROOF)) + target.fire_act(lighter.get_temperature()) + qdel(lighter) + playsound(user, 'sound/effects/fire_puff.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + // Always return TRUE to keep the click ability active. + return TRUE + +// We use both left and right mouse button. +/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/InterceptClickOn(mob/living/clicker, params, atom/target) + var/list/modifiers = params2list(params) + if(LAZYACCESS(modifiers, RIGHT_CLICK)) + if(!shoot_flameblast(clicker, target)) + return FALSE + return TRUE + ..() + // Always consume the click to avoid normal click interactions. + return TRUE + +// Gets & caches flame soul alignment for gating the right click. +/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/proc/is_flame_soul_alignment_active(mob/living/user) + if(!flame_soul_alignment) + for(var/datum/action/cooldown/power/cultivator/alignment/flame_soul/alignment_action in user.actions) + flame_soul_alignment = alignment_action + break + if(!flame_soul_alignment) + return FALSE + return flame_soul_alignment.active + +// Shoots a lil flameblast when we're in alignment. +/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/proc/shoot_flameblast(mob/living/user, atom/target) + if(!is_flame_soul_alignment_active(user)) + user.balloon_alert(user, "alignment required!") + return FALSE + if(world.time < next_projectile_time) + return FALSE + next_projectile_time = world.time + projectile_delay + fire_projectile(user, target, /obj/projectile/resonant/fire_to_dry_hay) + playsound(user, 'sound/effects/fire_puff.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + adjust_dantian(-flameblast_cost) + return TRUE + +// Applies projectile customization here. +/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/ready_projectile(obj/projectile/projectile_instance, atom/target, mob/living/user) + . = ..() + if(!projectile_instance) + return + var/obj/projectile/flame_projectile = projectile_instance + if(flameblast_icon) + flame_projectile.icon = flameblast_icon + if(flameblast_icon_state) + flame_projectile.icon_state = flameblast_icon_state + if(flameblast_scale) + var/matrix/scale_matrix = matrix(flame_projectile.transform) + scale_matrix.Scale(flameblast_scale, flameblast_scale) + flame_projectile.transform = scale_matrix + flame_projectile.damage = flameblast_damage + flame_projectile.light_range = flameblast_light_range + flame_projectile.light_power = flameblast_light_power + flame_projectile.light_color = flameblast_light_color + flame_projectile.light_on = TRUE + flame_projectile.set_light(flame_projectile.light_range, flame_projectile.light_power, flame_projectile.light_color, l_on = TRUE) + +// Because welder/lighter interactions check for get_temprature on the item we kind of have to make an abstract item do the work for us. +/obj/item/cultivator_virtual_lighter + parent_type = /obj/item/lighter + name = "\improper cultivator flame" + desc = "A conjured spark of cultivated flame." + fancy = TRUE + lit = TRUE + heat_while_on = HIGH_TEMPERATURE_REQUIRED - 100 + +/obj/item/cultivator_virtual_lighter/Initialize(mapload) + . = ..() + set_lit(TRUE) + +/obj/item/cultivator_virtual_lighter/get_fuel() + return INFINITY + +/obj/item/cultivator_virtual_lighter/ignition_effect(atom/A, mob/user) + if(get_temperature()) + return span_infoplain(span_rose("[user] touches the tip of [A] with [user.p_their()] finger and it ignites. Badass!")) + return "" + +/obj/item/cultivator_virtual_lighter/attack(mob/living/target_mob, mob/living/user, list/modifiers, list/attack_modifiers) + . = ..() + +// The fire projectile +/obj/projectile/resonant/fire_to_dry_hay + name = "flameblast" + icon_state = "fireball" + damage = 10 + armour_penetration = 0 // doesnt do jack to fireproofing + damage_type = BURN + armor_flag = FIRE + +/obj/projectile/resonant/fire_to_dry_hay/on_hit(atom/target, blocked, pierce_hit) + . = ..() + var/turf/impact_turf = get_turf(target) + if(!impact_turf) + return + var/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/power = creating_power + if(power?.flameblast_impact_sound) + playsound(impact_turf, power.flameblast_impact_sound, 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + // Apply fire stacks to mobs; ignite objects on the turf. + for(var/mob/living/burning_mob in impact_turf.contents) + burning_mob.adjust_fire_stacks(power?.flameblast_firestacks || 0) + burning_mob.ignite_mob() + for(var/atom/movable/ignite_target in impact_turf.contents) + if(ismob(ignite_target)) + continue + if(ignite_target.resistance_flags & FIRE_PROOF) + continue + ignite_target.fire_act(500) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm index 4cb81c8f5c3f93..a640df2bf8523e 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm @@ -19,7 +19,7 @@ required_affinity = 3 prep_cost = 4 click_to_activate = TRUE - anti_magic_on_click_target = FALSE + anti_magic_on_target = FALSE use_time_overlay_type = /obj/effect/temp_visual/conjure_rain use_time = 5 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 5b3adf7da893a7..064fed7804a691 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 @@ -23,7 +23,7 @@ required_affinity = 3 prep_cost = 3 click_to_activate = TRUE - anti_magic_on_click_target = FALSE + anti_magic_on_target = FALSE /datum/action/cooldown/power/thaumaturge/gale_blast/use_action(mob/living/user, atom/target) if(fire_projectile(user, target, /obj/projectile/resonant/gale_blast)) 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 b1a43625f063ed..5c09f9deec6142 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 @@ -19,7 +19,7 @@ required_affinity = 3 prep_cost = 5 - anti_magic_on_click_target = FALSE + anti_magic_on_target = FALSE // The projectile we fire var/obj/projectile/projectile_path = /obj/projectile/resonant/magic_barrage 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 1cb2c16d632ad7..37a90c560e218c 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 @@ -14,9 +14,10 @@ // THIS IS NOT IN EVERY POWER DATUM, ONLY ONES THAT HAVE RESOURCE SPENDING MECHANICS. var/cost -/datum/action/cooldown/power/theologist/New() +/datum/action/cooldown/power/theologist/Grant(mob/grant_to) . = ..() ValidatePietyComponent() + return . // Since Theologist has both 3 roots and a persistent resource system, we use a component for handling Piety /datum/action/cooldown/power/theologist/proc/ValidatePietyComponent() diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 30e0bb221905b7..ef98ff1aa36d01 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -11,7 +11,7 @@ overlay_icon_state = "bg_revenant_border" button_icon = 'icons/mob/actions/backgrounds.dmi' active_overlay_icon_state = "bg_spell_border_active_red" - ranged_mousepointer = 'icons/effects/mouse_pointers/supplypod_target.dmi' + ranged_mousepointer = 'icons/effects/mouse_pointers/weapon_pointer.dmi' /// Maximum state of consciousness before the ability is blocked. /// For example, `UNCONSCIOUS` prevents it from being used when in hard crit or dead, @@ -43,8 +43,8 @@ var/target_range /// If set, clicked target MUST be of this type (or subtype). var/target_type - /// Do we check for anti magic on the target when we click 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_click_target = 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 // When you press the button // Attempts to actively use the action @@ -53,7 +53,7 @@ if(!can_use(user, target)) return FALSE // Checking for anti-resonance/anti-magic below which really is a pain. - if(anti_magic_on_click_target && resonant && ismob(target) && target != user) // If the spell does check for antimagic on click, and if the spell is resonance based, and if the target is a mob, and if the target is not us. + if(anti_magic_on_target && resonant && ismob(target) && target != user) // If the spell does check for antimagic on click, and if the spell is resonance based, and if the target is a mob, and if the target is not us. var/mob/mob_target = target if(mob_target.can_block_resonance(1)) // Runs the special can_block_resonance function which also handles the anti-magic part. // I would like to deduct resources on spell fail, but that is going to be so utterly complex. TODO for the future chap who wants this. @@ -156,9 +156,11 @@ Handles all the logic involved in using a targeted, click-based action. StartCooldown() return TRUE -// Intercepts client owner clicks to activate the ability. -// Called by the base click intercept system on left click. -// Whilst /datum/action/cooldown does have click support, it doesn't support range-detecting and target filtering, so we are overriding that with our own. +/** Intercepts client owner clicks to activate the ability. + * Called by the base click intercept system on left click. + * Whilst /datum/action/cooldown does have click support, it doesn't support range-detecting and target filtering, so we are overriding that with our own. + * Returning only tells the game if we consume the normal click behavior (if true) or not (if false) + */ /datum/action/cooldown/power/InterceptClickOn(mob/living/clicker, params, atom/target) if(!IsAvailable(feedback = TRUE)) return FALSE diff --git a/tgstation.dme b/tgstation.dme index 3b5e1296a4116b..317d43a84f7266 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7474,6 +7474,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\flamesoul_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\fly_like_a_shooting_star.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\many_stars.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\set_fire_to_dry_hay.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\shadowwalker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_organ.dm" From 7b397aae49b6553957582028f7ee707b472191d8 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 25 Feb 2026 14:22:17 +0100 Subject: [PATCH 091/212] minor typos --- .../code/powers/resonant/cultivator/set_fire_to_dry_hay.dm | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm index 82017303618cd7..b7554a2e2cd2fd 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm @@ -125,7 +125,7 @@ /obj/item/cultivator_virtual_lighter parent_type = /obj/item/lighter name = "\improper cultivator flame" - desc = "A conjured spark of cultivated flame." + desc = "A conjured spark of flame." fancy = TRUE lit = TRUE heat_while_on = HIGH_TEMPERATURE_REQUIRED - 100 @@ -142,9 +142,6 @@ return span_infoplain(span_rose("[user] touches the tip of [A] with [user.p_their()] finger and it ignites. Badass!")) return "" -/obj/item/cultivator_virtual_lighter/attack(mob/living/target_mob, mob/living/user, list/modifiers, list/attack_modifiers) - . = ..() - // The fire projectile /obj/projectile/resonant/fire_to_dry_hay name = "flameblast" From 94bbd97ae01ad5839255fe635e90b544d839e601 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 25 Feb 2026 15:53:16 +0100 Subject: [PATCH 092/212] Finalizes flame soul. Tweaks a lack of can_use checks in right clicks. --- .../resonant/cultivator/astraltouched_root.dm | 9 ++-- .../resonant/cultivator/flamesoul_root.dm | 10 +++-- .../cultivator/from_friction_comes_flame.dm | 42 +++++++++++++++++++ .../powers/resonant/cultivator/many_stars.dm | 9 ++++ .../cultivator/set_fire_to_dry_hay.dm | 6 ++- .../modular_powers/code/powers_action.dm | 2 +- tgstation.dme | 1 + 7 files changed, 69 insertions(+), 10 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/from_friction_comes_flame.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm index 4d109c4f1a4f14..01240978b8b89c 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -1,26 +1,29 @@ /datum/power/cultivator_root/astral_touched name = "Astral Touched Alignment" desc = "You gain your Dantian's aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do extra burn damage.\ - Passively, your cold temprature tolerance is increased by 30C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\ + Passively, your cold temprature tolerance is increased by 40K; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\ You gain armor IV across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/astral_touched value = 6 + // bonus to cold tolerance + var/cold_tolerance_bonus = 40 + // Gives innate resistance to cold. /datum/power/cultivator_root/astral_touched/post_add() . = ..() if(!iscarbon(power_holder)) return var/mob/living/carbon/owner = power_holder - owner.dna.species.bodytemp_cold_damage_limit -= 30 + owner.dna.species.bodytemp_cold_damage_limit -= cold_tolerance_bonus /datum/power/cultivator_root/astral_touched/remove() . = ..() if(!iscarbon(power_holder)) return var/mob/living/carbon/owner = power_holder - owner.dna.species.bodytemp_cold_damage_limit += 30 + owner.dna.species.bodytemp_cold_damage_limit += cold_tolerance_bonus /datum/action/cooldown/power/cultivator/alignment/astral_touched name = "Astral Touched Alignment" diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm index e707546a1999a9..34b720d8530d30 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -1,26 +1,28 @@ /datum/power/cultivator_root/flame_soul name = "Flame Soul Alignment" desc = "You gain your Dantian's aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do extra burn damage.\ - Passively, your high temprature threshold is increased by 30C regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ + Passively, your high temprature threshold is increased by 60K regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ You gain armor III (with laser VI) across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/flame_soul - value = 6 + // bonus to heat tolerance + var/heat_tolerance_bonus = 60 + // Gives innate resistance to heat. /datum/power/cultivator_root/flame_soul/post_add() . = ..() if(!iscarbon(power_holder)) return var/mob/living/carbon/owner = power_holder - owner.dna.species.bodytemp_heat_damage_limit += 30 + owner.dna.species.bodytemp_heat_damage_limit += heat_tolerance_bonus /datum/power/cultivator_root/flame_soul/remove() . = ..() if(!iscarbon(power_holder)) return var/mob/living/carbon/owner = power_holder - owner.dna.species.bodytemp_heat_damage_limit -= 30 + owner.dna.species.bodytemp_heat_damage_limit -= heat_tolerance_bonus /datum/action/cooldown/power/cultivator/alignment/flame_soul name = "Flame Soul Alignment" diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/from_friction_comes_flame.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/from_friction_comes_flame.dm new file mode 100644 index 00000000000000..6c19a307836b31 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/from_friction_comes_flame.dm @@ -0,0 +1,42 @@ +/* + Punches cause heat build-up; sets on fire at a certain heat target (warm-bloods beware!) +*/ +/datum/power/cultivator/from_friction_comes_flame + name = "From Friction Comes Flame" + desc = "Your punches while in alignment cause the target to heat up. Once they reach 100C (373K), your strikes also combust the target." + value = 3 + + // how much we BRING THE HEAT on our punches + var/bonus_heat = 35 + // the flame stacks we apply per punch + var/bonus_flame_stacks = 0.15 + // the threshold on setting targets on fire in KELVIN (basically celcius but +273) + var/temperature_threshold = 373 + // reference to flame soul alignment + var/datum/action/cooldown/power/cultivator/alignment/flame_soul/flame_soul_alignment + +/datum/power/cultivator/from_friction_comes_flame/add() + RegisterSignal(power_holder, COMSIG_HUMAN_UNARMED_HIT, PROC_REF(on_unarmed_hit)) + +/datum/power/cultivator/from_friction_comes_flame/remove() + UnregisterSignal(power_holder, COMSIG_HUMAN_UNARMED_HIT) + +// Sends a signal to the new signaler for unarmed punches. +// Will probably be used a lot more with cultivator. +/datum/power/cultivator/from_friction_comes_flame/proc/on_unarmed_hit(mob/living/user, mob/living/target, obj/item/bodypart/affecting, damage, armor_block, limb_accuracy, limb_sharpness) + SIGNAL_HANDLER + if(!target || !is_flame_soul_alignment_active(user)) + return + target.adjust_bodytemperature(bonus_heat, 0, 1000) + if(target.bodytemperature >= temperature_threshold) + target.adjust_fire_stacks(bonus_flame_stacks) + target.ignite_mob() + +/datum/power/cultivator/from_friction_comes_flame/proc/is_flame_soul_alignment_active(mob/living/user) + if(!flame_soul_alignment) + for(var/datum/action/cooldown/power/cultivator/alignment/flame_soul/alignment_action in user.actions) + flame_soul_alignment = alignment_action + break + if(!flame_soul_alignment) + return FALSE + return flame_soul_alignment.active diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm index 59e857cf1b57b5..8b84b1865b5f8f 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm @@ -77,6 +77,13 @@ return FALSE return . +/datum/action/cooldown/power/cultivator/many_stars/proc/dispel(atom/target, atom/dispeller) + var/list/stars_to_del = active_stars.Copy() + if(stars_to_del) + to_chat(target, span_boldwarning("Your stars suddenly vanish!")) + for(var/obj/effect/many_stars_star/star as anything in stars_to_del) + qdel(star) + // Checks where to place the star /datum/action/cooldown/power/cultivator/many_stars/proc/can_place_star(turf/target_turf) if(!target_turf || !isopenturf(target_turf)) @@ -111,6 +118,8 @@ return if(!active_stars || !length(active_stars)) return + if(!can_use(user, target)) // we need to revalidate can_use since right click normally doesnt have that. + return if(dantian_component.dantian < star_explosion_cost) user.balloon_alert(user, "needs more dantian!") if(user) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm index b7554a2e2cd2fd..dd3abce49652b4 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm @@ -28,14 +28,14 @@ // icon, state and scale of flameblast var/flameblast_icon = null var/flameblast_icon_state = "fireball" - var/flameblast_scale = 0.5 + var/flameblast_scale = 0.7 // light range, power and color of flameblast var/flameblast_light_range = 3 var/flameblast_light_power = 1 var/flameblast_light_color = "#e99a3f" // damage & firestacks of flameblast var/flameblast_damage = 10 - var/flameblast_firestacks = 0.7 + var/flameblast_firestacks = 0.1 // the sound of flameblast impacting var/flameblast_impact_sound = 'sound/effects/fire_puff.ogg' // Cached alignment action for gating right click effects. @@ -92,6 +92,8 @@ if(!is_flame_soul_alignment_active(user)) user.balloon_alert(user, "alignment required!") return FALSE + if(!can_use(user, target)) // we need to revalidate can_use since right click normally doesnt have that. + return FALSE if(world.time < next_projectile_time) return FALSE next_projectile_time = world.time + projectile_delay diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index ef98ff1aa36d01..081d90cbef8bdd 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -53,7 +53,7 @@ if(!can_use(user, target)) return FALSE // Checking for anti-resonance/anti-magic below which really is a pain. - if(anti_magic_on_target && resonant && ismob(target) && target != user) // If the spell does check for antimagic on click, and if the spell is resonance based, and if the target is a mob, and if the target is not us. + if(anti_magic_on_target && resonant && ismob(target) && target != user) // If the spell does check for antimagic on the target, and if the spell is resonance based, and if the target is a mob, and if the target is not us. var/mob/mob_target = target if(mob_target.can_block_resonance(1)) // Runs the special can_block_resonance function which also handles the anti-magic part. // I would like to deduct resources on spell fail, but that is going to be so utterly complex. TODO for the future chap who wants this. diff --git a/tgstation.dme b/tgstation.dme index 317d43a84f7266..cd64fe5f5a0fd7 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7473,6 +7473,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\astraltouched_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\flamesoul_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\fly_like_a_shooting_star.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\from_friction_comes_flame.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\many_stars.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\set_fire_to_dry_hay.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\shadowwalker_root.dm" From f96b9730bb404c1e804689a203322be3289bd6d1 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 26 Feb 2026 07:35:17 +0100 Subject: [PATCH 093/212] Finalizes shadowwalker. Tweaked a few small litlte things. --- .../cultivator/_cultivator_alignment.dm | 15 ++- .../resonant/cultivator/astraltouched_root.dm | 2 +- .../resonant/cultivator/flamesoul_root.dm | 4 +- .../cultivator/from_friction_comes_flame.dm | 7 +- .../resonant/cultivator/shadowwalker_root.dm | 5 +- .../travel_under_the_veil_of_night.dm | 110 ++++++++++++++++++ .../cultivator/vanish_unseen_into_shadow.dm | 79 +++++++++++++ tgstation.dme | 2 + 8 files changed, 214 insertions(+), 10 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/vanish_unseen_into_shadow.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm index 6bf9ba9e2a9fce..18a441e844f29c 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm @@ -42,7 +42,7 @@ /datum/action/cooldown/power/cultivator/alignment/Destroy() . = ..() if(owner) - UnregisterSignal(owner, list(COMSIG_HUMAN_UNARMED_HIT, COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM)) + UnregisterSignal(owner, list(COMSIG_HUMAN_UNARMED_HIT, COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM, COMSIG_ATOM_DISPEL)) remove_alignment_armor() // The proc for onhit. Override as desired. @@ -100,6 +100,7 @@ RegisterSignal(user, COMSIG_HUMAN_UNARMED_HIT, PROC_REF(on_unarmed_hit)) RegisterSignal(user, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_equipment_changed)) RegisterSignal(user, COMSIG_MOB_UNEQUIPPED_ITEM, PROC_REF(on_equipment_changed)) + RegisterSignal(user, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) recompute_alignment_armor(user) SEND_SIGNAL(user, COMSIG_CULTIVATOR_ALIGNMENT_ENABLED, src) return TRUE @@ -111,13 +112,22 @@ if(alignment_overlay) user.cut_overlay(alignment_overlay) QDEL_NULL(alignment_overlay) - UnregisterSignal(user, list(COMSIG_HUMAN_UNARMED_HIT, COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM)) + UnregisterSignal(user, list(COMSIG_HUMAN_UNARMED_HIT, COMSIG_MOB_EQUIPPED_ITEM, COMSIG_MOB_UNEQUIPPED_ITEM, COMSIG_ATOM_DISPEL)) user.remove_filter(filter_id) remove_alignment_armor() QDEL_NULL(alignment_light) SEND_SIGNAL(user, COMSIG_CULTIVATOR_ALIGNMENT_DISABLED, src) return TRUE +// Dispel handler: drains Dantian if alignment is active. +/datum/action/cooldown/power/cultivator/alignment/proc/on_dispel(mob/owner, atom/dispeller) + SIGNAL_HANDLER + if(!active) + return NONE + if(ValidateDantianComponent()) + adjust_dantian(-CULTIVATOR_DANTIAN_MODERATE) + return DISPEL_RESULT_DISPELLED + // Deactivating the power doesn't cost anything so we skip the cost component. /datum/action/cooldown/power/cultivator/alignment/on_action_success(mob/living/carbon/user) if(!active) @@ -202,4 +212,3 @@ fire = 40 melee = 40 wound = 40 - diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm index 01240978b8b89c..824eb50b2ed9ee 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -1,7 +1,7 @@ /datum/power/cultivator_root/astral_touched name = "Astral Touched Alignment" desc = "You gain your Dantian's aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do extra burn damage.\ - Passively, your cold temprature tolerance is increased by 40K; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\ + Passively, your cold temprature tolerance is increased by 40C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\ You gain armor IV across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/astral_touched diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm index 34b720d8530d30..5383d16133c318 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -1,8 +1,8 @@ /datum/power/cultivator_root/flame_soul name = "Flame Soul Alignment" desc = "You gain your Dantian's aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do extra burn damage.\ - Passively, your high temprature threshold is increased by 60K regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ - You gain armor III (with laser VI) across your whole body. Has diminishing effects with your worn armor." + Passively, your high temprature threshold is increased by 60C regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ + You gain armor III (with laser VI and fire X) across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/flame_soul value = 6 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 6c19a307836b31..e94ffe64aeb99f 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 @@ -3,15 +3,16 @@ */ /datum/power/cultivator/from_friction_comes_flame name = "From Friction Comes Flame" - desc = "Your punches while in alignment cause the target to heat up. Once they reach 100C (373K), your strikes also combust the target." + desc = "Your punches while in alignment cause the target to heat up. Once they reach 80C, your strikes also combust the target." value = 3 + required_powers = list(/datum/power/cultivator_root/flame_soul) // how much we BRING THE HEAT on our punches - var/bonus_heat = 35 + var/bonus_heat = 20 // the flame stacks we apply per punch var/bonus_flame_stacks = 0.15 // the threshold on setting targets on fire in KELVIN (basically celcius but +273) - var/temperature_threshold = 373 + var/temperature_threshold = 353 // reference to flame soul alignment var/datum/action/cooldown/power/cultivator/alignment/flame_soul/flame_soul_alignment diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm index dea73fbf8b2c74..38f43a2f5d4f1b 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm @@ -6,7 +6,7 @@ You gain armor IV across your whole body. Has diminishing effects with your worn armor." action_path = /datum/action/cooldown/power/cultivator/alignment/shadow_walker - value = 6 + value = 5 // Lets you see in the dark. /datum/power/cultivator_root/shadow_walker/post_add() @@ -44,6 +44,9 @@ if(!shadowwalker_identity) shadowwalker_identity = new(user) refresh_echo_overlay(user) + //extra spooky 4 clown + if(is_clown_job(user.mind?.assigned_role)) + playsound(user, 'sound/misc/scary_horn.ogg', 60, TRUE) /datum/action/cooldown/power/cultivator/alignment/shadow_walker/disable_alignment(mob/living/carbon/user) . = ..() diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm new file mode 100644 index 00000000000000..3bc2f42ad00764 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm @@ -0,0 +1,110 @@ +/* + Teleports you from one dark turf to the other. + Note: I am disgusted at the sheer amount of ifs but uhh, this is the best I could think of. +*/ + + +/datum/power/cultivator/travel_under_the_veil_of_night + name = "Travel Under the Veil of Night" + desc = "Whilst your alignment is active, you can spend 2 seconds channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Dantian cost; no cooldown." + value = 5 + required_powers = list(/datum/power/cultivator_root/shadow_walker) + action_path = /datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night + +/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night + name = "Travel Under the Veil of Night" + desc = "Whilst your alignment is active, you can spend 2 seconds channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Dantian cost; no cooldown." + button_icon = 'icons/effects/effects.dmi' + button_icon_state = "blank" + + click_to_activate = TRUE + unset_after_click = TRUE + cost = 50 + + // Cached alignment action for gating effects. + var/datum/action/cooldown/power/cultivator/alignment/shadow_walker/shadow_walker_alignment + +/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/use_action(mob/living/user, atom/target) + if(!target) + return FALSE + // no teleporting out of where-ever-the-nowhere you are + var/turf/user_turf = get_turf(user) + if(!user_turf) + return FALSE + // alignment required + darkness + if(!is_shadow_walker_alignment_active(user) || !is_turf_in_darkness(user_turf)) + user.balloon_alert(user, "alignment + darkness required!") + return FALSE + // LOS requirement + if(!(target in view(user.client.view, user))) + user.balloon_alert(user, "out of view!") + return FALSE + var/turf/target_turf = get_turf(target) + // is it open & unblocked? + if(!is_valid_destination(target_turf)) + user.balloon_alert(user, "invalid destination!") + return FALSE + + // Do after. + if(!do_after(user, 2 SECONDS, target = user)) + return FALSE + + // Revalidate after channeling. + // recheck if we still have valid turfs (something may happen to them???) + if(!user_turf || !target_turf) + return FALSE + // alignment required + darkness + if(!is_shadow_walker_alignment_active(user) || !is_turf_in_darkness(user_turf)) + user.balloon_alert(user, "alignment + darkness required!") + return FALSE + // LOS requirement + if(!(target in view(user.client.view, user))) + user.balloon_alert(user, "out of view!") + return FALSE + // is it open & unblocked? + if(!is_valid_destination(target_turf)) + user.balloon_alert(user, "invalid destination!") + return FALSE + + // Okay so after that giant check of requirements now we actually try to teleport the person. + if(!do_teleport(user, target_turf, no_effects = TRUE)) + return FALSE + playsound(target_turf, 'sound/effects/nightmare_poof.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + playsound(user_turf, 'sound/effects/nightmare_reappear.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + // After image + new /obj/effect/temp_visual/blank_echo(user_turf) + return TRUE + +// Basically is the turf open/blocked? +/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/proc/is_valid_destination(turf/target_turf) + if(!target_turf || !isopenturf(target_turf)) + return FALSE + if(target_turf.is_blocked_turf(exclude_mobs = TRUE)) + return FALSE + return is_turf_in_darkness(target_turf) + +// IS IT DARK?! +/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/proc/is_turf_in_darkness(turf/target_turf) + if(!target_turf) + return FALSE + return target_turf.get_lumcount() <= LIGHTING_TILE_IS_DARK + +// ARE WE GOING SUPER SAIYAN?! +/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/proc/is_shadow_walker_alignment_active(mob/living/user) + if(!shadow_walker_alignment || QDELETED(shadow_walker_alignment)) + for(var/datum/action/cooldown/power/cultivator/alignment/shadow_walker/alignment_action in user.actions) + shadow_walker_alignment = alignment_action + break + if(!shadow_walker_alignment) + return FALSE + return shadow_walker_alignment.active + +/obj/effect/temp_visual/blank_echo + icon = 'icons/effects/effects.dmi' + icon_state = "blank" + duration = 2 SECONDS + randomdir = FALSE + +/obj/effect/temp_visual/blank_echo/Initialize(mapload) + . = ..() + animate(src, alpha = 0, time = duration) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/vanish_unseen_into_shadow.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/vanish_unseen_into_shadow.dm new file mode 100644 index 00000000000000..2d290a39a47e20 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/vanish_unseen_into_shadow.dm @@ -0,0 +1,79 @@ +/* + Untrackable by resonant means and no slowdown in darkness. Quick getaways ahoy. +*/ + +/datum/power/cultivator/vanish_unseen_into_shadow + name = "Vanish Unseen into Shadow" + desc = "You are untrackable within the shadows. You are immune to resonant scrying and slowdowns while you're stood in darkness or are in alignment." + value = 5 + required_powers = list(/datum/power/cultivator_root/shadow_walker) + power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES + + // Cached alignment action for gating effects. + var/datum/action/cooldown/power/cultivator/alignment/shadow_walker/shadow_walker_alignment + // Current instance of the status effect + var/datum/status_effect/power/vanish_unseen_into_shadow/active_effect + +// Cleanup lingering effects +/datum/power/cultivator/vanish_unseen_into_shadow/remove() + if(active_effect) + qdel(active_effect) + active_effect = null + return ..() + +// Keeps the status effect applied while in darkness or alignment. +/datum/power/cultivator/vanish_unseen_into_shadow/process(seconds_per_tick) + var/mob/living/user = power_holder + if(!user) + return + + var/should_apply = is_in_darkness(user) || is_shadow_walker_alignment_active(user) + if(should_apply) + if(!active_effect || QDELETED(active_effect)) + active_effect = user.apply_status_effect(/datum/status_effect/power/vanish_unseen_into_shadow) + return + + if(active_effect) + qdel(active_effect) + active_effect = null + +/datum/power/cultivator/vanish_unseen_into_shadow/proc/is_in_darkness(mob/living/user) + var/turf/user_turf = get_turf(user) + if(!user_turf) + return FALSE + return user_turf.get_lumcount() <= LIGHTING_TILE_IS_DARK + +/datum/power/cultivator/vanish_unseen_into_shadow/proc/is_shadow_walker_alignment_active(mob/living/user) + if(!shadow_walker_alignment || QDELETED(shadow_walker_alignment)) + for(var/datum/action/cooldown/power/cultivator/alignment/shadow_walker/alignment_action in user.actions) + shadow_walker_alignment = alignment_action + break + if(!shadow_walker_alignment) + return FALSE + return shadow_walker_alignment.active + +// Status effect that handles the bonuses. +/datum/status_effect/power/vanish_unseen_into_shadow + id = "vanish_unseen_into_shadow" + duration = STATUS_EFFECT_PERMANENT + tick_interval = STATUS_EFFECT_NO_TICK + alert_type = /atom/movable/screen/alert/status_effect/vanish_unseen_into_shadow + +/datum/status_effect/power/vanish_unseen_into_shadow/on_apply() + if(!owner) + return FALSE + owner.ignore_slowdown(type) + ADD_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, type) + return TRUE + +/datum/status_effect/power/vanish_unseen_into_shadow/on_remove() + if(owner) + owner.unignore_slowdown(type) + REMOVE_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, type) + return + +/atom/movable/screen/alert/status_effect/vanish_unseen_into_shadow + name = "Vanish Unseen Into Shadow" + desc = "You are undetectable and are unaffected by slowdowns." + icon = 'icons/effects/effects.dmi' + icon_state = "blank" diff --git a/tgstation.dme b/tgstation.dme index cd64fe5f5a0fd7..d82379fd057df7 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7477,6 +7477,8 @@ #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\many_stars.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\set_fire_to_dry_hay.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\shadowwalker_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\travel_under_the_veil_of_night.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\vanish_unseen_into_shadow.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_organ.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm" From da71ec88905f80437d3a7522093311613b8334b1 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 26 Feb 2026 08:33:34 +0100 Subject: [PATCH 094/212] Adds Divine Protection; tweaked a few things about theologist. --- code/__DEFINES/~doppler_defines/powers.dm | 15 +++-- .../sorcerous/theologist/_theologist_piety.dm | 2 +- .../theologist/_theologist_root_revered.dm | 4 +- .../theologist/_theologist_root_shared.dm | 10 +-- .../theologist/_theologist_root_twisted.dm | 4 +- .../sorcerous/theologist/divine_protection.dm | 67 +++++++++++++++++++ .../sorcerous/theologist/pious_prayer.dm | 8 +-- .../theologist/smiting_strike_upgrades.dm | 8 +-- tgstation.dme | 1 + 9 files changed, 94 insertions(+), 25 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/divine_protection.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index b1838df2da85f3..a0198af58d5188 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -125,13 +125,13 @@ */ // How much root abilities should heal (max), if they heal. -#define THEOLOGIAN_ROOT_HEALING 30 +#define THEOLOGIST_ROOT_HEALING 30 // Healing equates to this much piety. -#define THEOLOGIAN_PIETY_HEALING_COEFFICIENT 0.2 +#define THEOLOGIST_PIETY_HEALING_COEFFICIENT 0.2 -// Maximum amount of Piety -#define THEOLOGIAN_PIETY_MAX 50 +// Maximum amount of Piety (chaplain gets double this amount) +#define THEOLOGIST_PIETY_MAX 50 // UI location of the Piety element #define THEOLOGIST_UI_SCREEN_LOC "WEST,CENTER-2:15" @@ -142,6 +142,13 @@ // Trait made as to prevent duplicate smites. #define TRAIT_HAS_SMITING_STRIKE "has_smiting_strike" +// Standard Theologian costs +#define THEOLOGIST_PIETY_TRIVIAL (CULTIVATOR_DANTIAN_MAX / 100) +#define THEOLOGIST_PIETY_MINOR (CULTIVATOR_DANTIAN_MAX / 10) +#define THEOLOGIST_PIETY_MODERATE (CULTIVATOR_DANTIAN_MAX / 5) +#define THEOLOGIST_PIETY_MAJOR (CULTIVATOR_DANTIAN_MAX / 2) +#define THEOLOGIST_PIETY_CRUSHING (CULTIVATOR_DANTIAN_MAX) + /** * RESONANT * All defines related to the resonant archetype. 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 c9b54f3fbebdb5..978b368e50ebc0 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 @@ -9,7 +9,7 @@ // current piety & max piety var/piety = 0 - var/max_piety = THEOLOGIAN_PIETY_MAX + var/max_piety = THEOLOGIST_PIETY_MAX // The UI itself var/atom/movable/screen/theologist_piety/theologist_ui diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index 367ea90b53533c..d175deda89a6bb 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -66,7 +66,7 @@ // The power responsible for this, so we can make sure it properly gives piety to the caster var/datum/action/cooldown/power/theologist/theologist_root/revered/burden_power // The maximum amount we will heal - var/healing_max = THEOLOGIAN_ROOT_HEALING + var/healing_max = THEOLOGIST_ROOT_HEALING // How much we have healed already var/healing_done = 0 // How much we heal per tick. @@ -162,7 +162,7 @@ // QDEL destroys burden_power /datum/status_effect/power/burden_revered/proc/expire() - var/piety_gained = max(0, floor(healing_done * THEOLOGIAN_PIETY_HEALING_COEFFICIENT)) // TODO: defines + var/piety_gained = max(0, floor(healing_done * THEOLOGIST_PIETY_HEALING_COEFFICIENT)) // TODO: defines // Report back BEFORE deletion starts if(burden_power) burden_power.effect_expired(owner, piety_gained) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index 945aab806d0fd6..01cd0a4d5fa83f 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -64,7 +64,7 @@ // Have we been a good boy? if(piety_buildup >= 1) piety_buildup -= 1 - adjust_piety(1) + adjust_piety(THEOLOGIST_PIETY_TRIVIAL) to_chat(owner, span_notice("Taking on the burdens of others has gained you piety!")) // Have we been a bad boy? else if (piety_buildup <= -1) @@ -75,10 +75,10 @@ if(ishuman(owner)) var/mob/living/carbon/human/sinner = owner sinner.Paralyze(100) - to_chat(owner, span_userdanger("Divine forces have punished you for your lack of piety!"), confidential = TRUE) + to_chat(owner, span_userdanger("You have been punished for your lack of piety!"), confidential = TRUE) clear_link() return - adjust_piety(-1) + adjust_piety(-THEOLOGIST_PIETY_TRIVIAL) to_chat(owner, span_warning("The transfer of your burdens onto others lost you piety!")) @@ -266,9 +266,9 @@ // Piety buildup increases/deductions // you can't gain piety from taking burdens from a ckey-less creature (sorry pets), but you can lose piety from dumping onto a ckey-less creature. if(taker == owner && giver.ckey) - piety_buildup += amount * THEOLOGIAN_PIETY_HEALING_COEFFICIENT + piety_buildup += amount * THEOLOGIST_PIETY_HEALING_COEFFICIENT else if(giver == owner) - piety_buildup -= amount * THEOLOGIAN_PIETY_HEALING_COEFFICIENT + piety_buildup -= amount * THEOLOGIST_PIETY_HEALING_COEFFICIENT return diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index 543aafa3e38572..47b31e6d3a3d2e 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -25,7 +25,7 @@ unset_after_click = TRUE //How much we can heal max with twisted per use. - var/healing_max = THEOLOGIAN_ROOT_HEALING + var/healing_max = THEOLOGIST_ROOT_HEALING //Tracks how much healing we did throughout the proccess. var/healing_done = 0 @@ -91,7 +91,7 @@ UnregisterSignal(owner, COMSIG_ATOM_DISPEL) // Handles piety gain - var/piety_gained = max(0, floor(healing_done * THEOLOGIAN_PIETY_HEALING_COEFFICIENT)) + var/piety_gained = max(0, floor(healing_done * THEOLOGIST_PIETY_HEALING_COEFFICIENT)) // resets for next time healing_done = 0 damage_done = 0 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/divine_protection.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/divine_protection.dm new file mode 100644 index 00000000000000..4ddf4f9901e56a --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/divine_protection.dm @@ -0,0 +1,67 @@ +/* + Grants a passive block chance equal to half your piety and diminishes it on hit (with minor gating) +*/ + +/datum/power/theologist/divine_protection + name = "Divine Protection" + desc = "You gain a block chance (separate from all other block chance) equal to half your piety; reduce Piety by 5 when this triggers." + value = 4 + + required_powers = list(/datum/power/theologist_root/) + required_allow_subtypes = TRUE + /// World time (in deciseconds) when piety drain last triggered + var/last_piety_drain = 0 + /// World time (in deciseconds) when block effect last triggered + var/last_block_effect = 0 + /// The ratio of piety to block. + var/piety_ratio = 0.5 + +/datum/power/theologist/divine_protection/add() + RegisterSignal(power_holder, COMSIG_LIVING_CHECK_BLOCK, PROC_REF(check_block)) + +/datum/power/theologist/divine_protection/remove() + UnregisterSignal(power_holder, COMSIG_LIVING_CHECK_BLOCK) + +/datum/power/theologist/divine_protection/proc/check_block(mob/living/blocking_user, atom/movable/hitby, damage, attack_text, attack_type, armour_penetration, damage_type) + SIGNAL_HANDLER + + if(!blocking_user) + return NONE + + var/datum/component/theologist_piety/piety_component = blocking_user.GetComponent(/datum/component/theologist_piety) + if(!piety_component) + return NONE + + var/block_chance = clamp(round(piety_component.piety * piety_ratio), 0, 100) + if(block_chance <= 0 || !prob(block_chance)) + return NONE + + block_effect(blocking_user, attack_text) + // We only allow piety loss once per 0.4 seconds so you don't get your piety nuked by a shotgun. + if(world.time >= last_piety_drain + 4) + piety_component.adjust_piety(-THEOLOGIST_PIETY_MINOR) + last_piety_drain = world.time + return SUCCESSFUL_BLOCK + +// Special block effect +/datum/power/theologist/divine_protection/proc/block_effect(mob/living/blocking_user, attack_text) + if(!blocking_user) + return + blocking_user.visible_message( + span_danger("[attack_text] bounces harmlessly off of [blocking_user]!"), + span_userdanger("Your Divine Protection protects you from [attack_text]!"), + ) + // don't trigger the fx more than 1 second to prevent taking ear damage from being shotgunned. + if(world.time < last_block_effect + 10) + return + last_block_effect = world.time + + var/mutable_appearance/holy_glow = mutable_appearance('icons/mob/effects/genetics.dmi', "servitude", -MUTATIONS_LAYER) + blocking_user.add_overlay(holy_glow) + addtimer(CALLBACK(blocking_user, TYPE_PROC_REF(/atom, cut_overlay), holy_glow), 1 SECONDS) + playsound(blocking_user, 'sound/effects/magic/magic_block_holy.ogg', 50, TRUE) + +/datum/power/theologist/divine_protection/proc/remove_holy_glow(mob/living/blocking_user, image/holy_glow_image) + if(!blocking_user || !holy_glow_image) + return + blocking_user.vis_contents -= holy_glow_image diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm index dd7c22c45057d3..e6fa2eb32f55f2 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm @@ -31,10 +31,6 @@ // Increase prayer cap based on various factors. // Are you the Chaplain? if(is_chaplain_job(usr.mind?.assigned_role)) - prayer_cap = 15 - return - // Do you have the religious quirk? - if(HAS_TRAIT(usr, TRAIT_SPIRITUAL)) prayer_cap = 10 return @@ -58,7 +54,7 @@ else if(istype(area, /area/station/service/chapel) || prob(check_how_religious(user))) // If you're in the chapel or if fate aligns. if(cap_warning_given) continue - adjust_piety(1) + adjust_piety(THEOLOGIST_PIETY_TRIVIAL) to_chat(user, span_notice("You feel more pious after your prayer.")) else keep_going = FALSE @@ -82,7 +78,7 @@ total_chance += 5 // Do you carry the bible on your person? if(has_bible(user)) - total_chance += 5 + total_chance += 10 // Are you standing on a blessed tile? (Blessed with holy water). if(locate(/obj/effect/blessing) in user.loc) total_chance += 15 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm index 24a25cf6b264aa..dfa51b25d220bf 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm @@ -1,17 +1,15 @@ /* Simple example of an upgrade, though more like a sidegrade here. Most of the effects are already baked into the existing power for convenience. */ -/datum/power/theologist/smiting_strike/imbue_armaments +/datum/power/theologist/imbue_armaments name = "Imbue Armaments" desc = "Changes Smiting Strike to no longer be removed when it passes hands, and allows you to have an unlimited amount of items blessed. Reduces the smite effect's knockback by 2 and damage by 5." value = 3 - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THEOLOGIST required_powers = list(/datum/power/theologist/smiting_strike) action_path = null // So we don't give em another use of the ability. -/datum/power/theologist/smiting_strike/imbue_armaments/post_add() +/datum/power/theologist/smiting_strike/post_add() . = ..() var/datum/power/theologist/smiting_strike/smiting_strike = power_holder.get_power(/datum/power/theologist/smiting_strike) var/datum/action/cooldown/power/theologist/smiting_strike/smite_action = smiting_strike.action_path // I really should find a better way to get the variables of actions. @@ -19,7 +17,7 @@ Most of the effects are already baked into the existing power for convenience. smite_action.smite_knockback -= 2 smite_action.can_imbue_multiples = TRUE -/datum/power/theologist/smiting_strike/imbue_armaments/remove() +/datum/power/theologist/smiting_strike/remove() var/datum/power/theologist/smiting_strike/smiting_strike = power_holder.get_power(/datum/power/theologist/smiting_strike) var/datum/action/cooldown/power/theologist/smiting_strike/smite_action = smiting_strike.action_path smite_action.smite_damage += 5 diff --git a/tgstation.dme b/tgstation.dme index d82379fd057df7..5ad2541d518da4 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7527,6 +7527,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_revered.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_shared.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\_theologist_root_twisted.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\divine_protection.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\entropic_mending.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\pious_prayer.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike.dm" From 8723b72cc10c9f1be08b7677a2eadf2174478c56 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 26 Feb 2026 12:13:20 +0100 Subject: [PATCH 095/212] Adds Purify, allowing you to purify and bless various things. Also has a really cool mechanic., --- code/__DEFINES/~doppler_defines/powers.dm | 10 +- .../theologist/_theologist_root_twisted.dm | 2 +- .../sorcerous/theologist/divine_protection.dm | 2 +- .../powers/sorcerous/theologist/purify.dm | 406 ++++++++++++++++++ tgstation.dme | 1 + 5 files changed, 414 insertions(+), 7 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index a0198af58d5188..5a4f6b37256821 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -143,11 +143,11 @@ #define TRAIT_HAS_SMITING_STRIKE "has_smiting_strike" // Standard Theologian costs -#define THEOLOGIST_PIETY_TRIVIAL (CULTIVATOR_DANTIAN_MAX / 100) -#define THEOLOGIST_PIETY_MINOR (CULTIVATOR_DANTIAN_MAX / 10) -#define THEOLOGIST_PIETY_MODERATE (CULTIVATOR_DANTIAN_MAX / 5) -#define THEOLOGIST_PIETY_MAJOR (CULTIVATOR_DANTIAN_MAX / 2) -#define THEOLOGIST_PIETY_CRUSHING (CULTIVATOR_DANTIAN_MAX) +#define THEOLOGIST_PIETY_TRIVIAL (THEOLOGIST_PIETY_MAX / 100) +#define THEOLOGIST_PIETY_MINOR (THEOLOGIST_PIETY_MAX / 10) +#define THEOLOGIST_PIETY_MODERATE (THEOLOGIST_PIETY_MAX / 5) +#define THEOLOGIST_PIETY_MAJOR (THEOLOGIST_PIETY_MAX / 2) +#define THEOLOGIST_PIETY_CRUSHING (THEOLOGIST_PIETY_MAX) /** * RESONANT diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index 47b31e6d3a3d2e..c658623291b9ba 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -71,7 +71,7 @@ keep_going = FALSE if(target.health < target.maxHealth) new /obj/effect/temp_visual/heal(get_turf(target), "#cf2525") - playsound(owner, 'sound/effects/magic/cosmic_expansion.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + playsound(owner, 'sound/effects/magic/cosmic_expansion.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) var/healtodmgcap = heal_random_damage(target) deal_random_damage(target, (healtodmgcap / 2)) if(healing_done >= healing_max) 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 4ddf4f9901e56a..4727c9bb91b821 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 @@ -49,7 +49,7 @@ return blocking_user.visible_message( span_danger("[attack_text] bounces harmlessly off of [blocking_user]!"), - span_userdanger("Your Divine Protection protects you from [attack_text]!"), + span_userdanger("[attack_text] is blocked by your Divine Protection!"), ) // don't trigger the fx more than 1 second to prevent taking ear damage from being shotgunned. if(world.time < last_block_effect + 10) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm new file mode 100644 index 00000000000000..da57f2d45d8513 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm @@ -0,0 +1,406 @@ +/* + Does 3 things; + - When targeting objects, it dispels it; and if it has a holy equivelant, it turns it into that equivelant. + - When targeting creatures, it dispels them. + - It will also remove all poisons contained in the item if it can hold reagents. + If it fails to do any of these 3 succesfully, it refunds the piety. +*/ + +/datum/power/theologist/purify + name = "Purify" + desc = "Cleanses impurity from objects and creatures in melee range. The chosen target is immediately dispelled and purified of all poisons. \ + If the target is an object with a holy equivelant, it turns it into that (e.g water into holy water). Has varying purity costs, but usually defaults to 5." + action_path = /datum/action/cooldown/power/theologist/purify + value = 5 + + required_powers = list(/datum/power/theologist_root/shared) + +/datum/action/cooldown/power/theologist/purify + name = "Purify" + desc = "Cleanses impurity from objects and creatures in melee range. The chosen target is immediately dispelled and purified of all poisons. \ + If the target is an object with a holy equivelant, it turns it into that (e.g water into holy water). Has varying purity costs, but usually defaults to 5." + button_icon = 'icons/obj/mining_zones/artefacts.dmi' + button_icon_state = "purified_soulstone" + cooldown_time = 60 + + target_range = 1 + click_to_activate = TRUE + /// Accumulated piety cost for this use. + var/pending_piety_cost = 0 + +/datum/action/cooldown/power/theologist/purify/InterceptClickOn(mob/living/clicker, params, atom/target) + . = ..() + // Makes it so we always override the base click. Don't want to use the item you are trying to purify. + return TRUE + +/datum/action/cooldown/power/theologist/purify/use_action(mob/living/user, atom/target) + if(!target) + return FALSE + pending_piety_cost = 0 + + // Special construct purification channel. + if(purify_construct(user, target)) + return TRUE + var/success = FALSE + + // General dispel on target + if(dispel(target, user)) + success = TRUE + + // Remove poison from a creature's bloodstream or an object's reagents. + if(target.reagents) + var/removed = target.reagents.remove_reagent(/datum/reagent/toxin, target.reagents.total_volume, include_subtypes = TRUE) + if(removed > 0) + success = TRUE + + // Holy-equivalent conversions. + if(convert_objects(user, target)) + success = TRUE + if(convert_reagents(user, target)) + success = TRUE + + if(success) + pending_piety_cost = max(pending_piety_cost, THEOLOGIST_PIETY_MINOR) + playsound(user, 'sound/effects/magic/magic_block_holy.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + else + user.balloon_alert(user, "nothing to be purified!") + return success + +/datum/action/cooldown/power/theologist/purify/proc/try_add_cost(mob/living/user, cost) + if(cost <= 0) + return TRUE + if(get_piety() < (pending_piety_cost + cost)) + user.balloon_alert(user, "needs [cost] piety!") + return FALSE + pending_piety_cost += cost + return TRUE + +/datum/action/cooldown/power/theologist/purify/on_action_success(mob/living/user, atom/target) + . = ..() + if(pending_piety_cost > 0) + adjust_piety(-pending_piety_cost) + pending_piety_cost = 0 + return + +/datum/action/cooldown/power/theologist/purify/proc/replace_target(atom/target, typepath, mob/living/user) + if(!target || !typepath) + return null + + var/obj/old_obj = target + var/mob/living/holder + var/hand_index = 0 + if(istype(old_obj, /obj/item) && ismob(old_obj.loc)) + holder = old_obj.loc + hand_index = holder.get_held_index_of_item(old_obj) + + var/obj/new_obj + if(hand_index && holder) + new_obj = new typepath(null) + holder.put_in_hand(new_obj, hand_index, forced = TRUE) + else + new_obj = new typepath(old_obj.loc) + + qdel(old_obj) + return new_obj + +/datum/action/cooldown/power/theologist/purify/proc/copy_seed_stats(obj/item/seeds/from_seed, obj/item/seeds/to_seed) + if(!from_seed || !to_seed) + return + to_seed.lifespan = from_seed.lifespan + to_seed.endurance = from_seed.endurance + to_seed.maturation = from_seed.maturation + to_seed.production = from_seed.production + to_seed.yield = from_seed.yield + to_seed.potency = from_seed.potency + to_seed.instability = from_seed.instability + to_seed.weed_rate = from_seed.weed_rate + to_seed.weed_chance = from_seed.weed_chance + +/datum/action/cooldown/power/theologist/purify/proc/convert_reagents(mob/living/user, atom/target) + if(!target?.reagents) + return FALSE + + var/converted = FALSE + + // (Unholy) Water -> Holy Water + var/water_amt = target.reagents.get_reagent_amount(/datum/reagent/water, REAGENT_STRICT_TYPE) + var/unholy_amt = target.reagents.get_reagent_amount(/datum/reagent/fuel/unholywater, REAGENT_STRICT_TYPE) + var/holy_source_amt = water_amt + unholy_amt + if(holy_source_amt > 0) + if(try_add_cost(user, THEOLOGIST_PIETY_MODERATE)) + if(water_amt > 0) + target.reagents.remove_reagent(/datum/reagent/water, water_amt) + if(unholy_amt > 0) + target.reagents.remove_reagent(/datum/reagent/fuel/unholywater, unholy_amt) + target.reagents.add_reagent(/datum/reagent/water/holywater, holy_source_amt) + to_chat(user, span_notice("The water in [target] gleams into holy water.")) + converted = TRUE + + // Blood -> Godsblood + var/blood_amt = target.reagents.get_reagent_amount(/datum/reagent/blood, REAGENT_SUB_TYPE) + if(blood_amt > 0) + var/blood_cost = CEILING(blood_amt / 5, 5) + if(try_add_cost(user, blood_cost)) + target.reagents.remove_reagent(/datum/reagent/blood, blood_amt, include_subtypes = TRUE) + target.reagents.add_reagent(/datum/reagent/medicine/omnizine/godblood, blood_amt) + to_chat(user, span_notice("The blood in [target] is sanctified into godsblood.")) + converted = TRUE + + return converted + +/datum/action/cooldown/power/theologist/purify/proc/convert_objects(mob/living/user, atom/target) + // Melon -> Holy Melon + if(istype(target, /obj/item/food/grown/watermelon) && !istype(target, /obj/item/food/grown/holymelon)) + if(!try_add_cost(user, THEOLOGIST_PIETY_MINOR)) + return FALSE + var/obj/item/food/grown/watermelon/melon = target + var/obj/item/seeds/old_seed = melon.get_plant_seed() + var/obj/item/food/grown/holymelon/new_melon = replace_target(melon, /obj/item/food/grown/holymelon, user) + if(new_melon && old_seed) + var/obj/item/seeds/new_seed = new /obj/item/seeds/watermelon/holy(null) + copy_seed_stats(old_seed, new_seed) + new_melon.seed = new_seed + to_chat(user, span_notice("Divine light transforms [melon] into a holymelon.")) + return TRUE + + // Soulstone -> Purified Soulstone + if(istype(target, /obj/item/soulstone)) + var/obj/item/soulstone/stone = target + if(stone.theme == THEME_HOLY) + return FALSE + if(!try_add_cost(user, THEOLOGIST_PIETY_MINOR)) + return FALSE + stone.required_role = null + stone.theme = THEME_HOLY + stone.update_appearance() + for(var/mob/shade_to_deconvert in stone.contents) + stone.assign_master(shade_to_deconvert, user) + UnregisterSignal(stone, COMSIG_BIBLE_SMACKED) + to_chat(user, span_notice("You purify [stone], its glow becoming serene.")) + return TRUE + + // Any book -> Bible (free) + if(istype(target, /obj/item/book) && !istype(target, /obj/item/book/bible)) + if(!try_add_cost(user, 0)) + return FALSE + replace_target(target, /obj/item/book/bible, user) + to_chat(user, span_notice("The pages reorder themselves into a bible.")) + return TRUE + + // Skateboard -> Holy Skateboard + if(istype(target, /obj/item/melee/skateboard) && !istype(target, /obj/item/melee/skateboard/holyboard)) + if(!try_add_cost(user, THEOLOGIST_PIETY_MAJOR)) + return FALSE + replace_target(target, /obj/item/melee/skateboard/holyboard, user) + to_chat(user, span_notice("The board hums and becomes a holy skateboard.")) + return TRUE + + // Bow -> Divine Bow + if(istype(target, /obj/item/gun/ballistic/bow) && !istype(target, /obj/item/gun/ballistic/bow/divine)) + if(!try_add_cost(user, THEOLOGIST_PIETY_MAJOR)) + return FALSE + replace_target(target, /obj/item/gun/ballistic/bow/divine, user) + to_chat(user, span_notice("The bow brightens, reshaping into a divine bow.")) + return TRUE + + // Arrow -> Holy Arrow + if(istype(target, /obj/item/ammo_casing/arrow) && !istype(target, /obj/item/ammo_casing/arrow/holy)) + if(!try_add_cost(user, THEOLOGIST_PIETY_MINOR)) + return FALSE + replace_target(target, /obj/item/ammo_casing/arrow/holy, user) + to_chat(user, span_notice("The arrow brightens with holy light.")) + return TRUE + + return FALSE + +/** Special interaction for hype moments and aura: Deacons (people converted by Chaplain Sects) and the Chaplain can deconvert constructs. It takes times and is interuptable. + * If there is a mind inside the construct, it retains it and is de-antag'd. + * If there isn't, it prompts a ghost to see if they want to be part of it. +**/ +/datum/action/cooldown/power/theologist/purify/proc/purify_construct(mob/living/user, atom/target) + if(!isconstruct(target)) + return FALSE + + var/mob/living/basic/construct/construct_target = target + if(construct_target.theme == THEME_HOLY) + return FALSE + if(!are_we_a_holy_man(user)) + user.balloon_alert(user, "you need to be a holy figure to purify that!") + return FALSE + if(get_piety() < THEOLOGIST_PIETY_CRUSHING) + user.balloon_alert(user, "needs [THEOLOGIST_PIETY_CRUSHING] piety!") + return FALSE + + // Piety is spent regardless of success. + adjust_piety(-THEOLOGIST_PIETY_CRUSHING) + + // End click targeting during the channel for clarity. + unset_click_ability(user, refund_cooldown = TRUE) + + var/datum/beam/link = user.Beam(construct_target, icon_state = "kinesis", override_target_pixel_x = 0) + construct_target.SetStun(15 SECONDS, ignore_canstun = TRUE) + // normally you don't use userdanger for this but its a hype moment. + user.visible_message(span_userdanger("[user] channels a beam of holy energy, attempting to purify any and all unholy qualities of [construct_target]!")) + var/channel_success = do_after(user, 15 SECONDS, target = construct_target) + construct_target.SetStun(0, ignore_canstun = TRUE) + QDEL_NULL(link) + + if(!channel_success || QDELETED(construct_target)) + return TRUE + + var/typepath = get_purified_construct_type(construct_target) + + // Fallback for the constructs that dont have a purified version e.g proteons + if(!typepath) + convert_construct_in_place(construct_target, user) + return TRUE + + var/mob/living/basic/construct/new_construct = new typepath(construct_target.loc) + if(construct_target.mind) + construct_target.mind.remove_antag_datum(/datum/antagonist/cult) + construct_target.mind.remove_antag_datum(/datum/antagonist/shade_minion) + construct_target.mind.transfer_to(new_construct, force_key_move = TRUE) + else + enable_construct_ghost_control(new_construct) + + user.visible_message(span_notice("[user] purifies [construct_target]!")) + playsound(user, 'sound/effects/his_grace/his_grace_ascend.ogg', 50, TRUE) + // Special feedback to the construct + to_chat(new_construct, span_blue("The Geometer's presence in your mind fades, what was once your own freewill slips back into the forefront. You look down at your body; and while it is still the dark steel that adorns your body, you can move it of your own free will. Your freedom is returned; but still tethered forevermore to this body.")) + // Special feedback to the caster + to_chat(user, span_blue("As you have shown yourself to be pious, to take on the burdens of others; you now take on the greatest burden of another. Whoever the vile entity responsible may be, you take it away from this enslaved tool, and bury the energy responsible deep inside you. You have freed it; but this darkness seems to be eating away at you.")) + user.apply_status_effect(/datum/status_effect/debt_to_the_geometer, src) + to_chat(user, span_cult_bold("You have been cursed; your actions carry a price, and you shall be made to pay it.")) + qdel(construct_target) + return TRUE + +// Are we a chaplain or deacon (chaplain sect convertee)? +/datum/action/cooldown/power/theologist/purify/proc/are_we_a_holy_man(mob/living/user) + if(is_chaplain_job(user.mind?.assigned_role)) + return TRUE + return user.mind?.holy_role == HOLY_ROLE_DEACON + +// Matches the purified construct type +/datum/action/cooldown/power/theologist/purify/proc/get_purified_construct_type(mob/living/basic/construct/target) + if(istype(target, /mob/living/basic/construct/artificer)) + return /mob/living/basic/construct/artificer/angelic + if(istype(target, /mob/living/basic/construct/wraith)) + return /mob/living/basic/construct/wraith/angelic + if(istype(target, /mob/living/basic/construct/juggernaut)) + return /mob/living/basic/construct/juggernaut/angelic + return null + +/datum/action/cooldown/power/theologist/purify/proc/enable_construct_ghost_control(mob/living/basic/construct/target) + target.AddComponent(\ + /datum/component/ghost_direct_control,\ + poll_candidates = TRUE,\ + poll_question = "Do you want to play as a Theologist's purified construct?",\ + role_name = "purified construct",\ + poll_ignore_key = POLL_IGNORE_CONSTRUCT,\ + assumed_control_message = "You are a purified construct, freed from the Geometer's influence. Your will is now your own.",\ + ) + +// In the event that its a construct without a purified equivelant e.g protean, we do this instead. +/datum/action/cooldown/power/theologist/purify/proc/convert_construct_in_place(mob/living/basic/construct/target, mob/living/user) + var/old_theme = target.theme + target.theme = THEME_HOLY + target.faction = list(FACTION_HOLY) + ADD_TRAIT(target, TRAIT_ANGELIC, INNATE_TRAIT) + if(target.icon_state) + target.cut_overlay("glow_[target.icon_state]_[old_theme]") + target.add_overlay("glow_[target.icon_state]_[target.theme]") + target.update_appearance() + target.mind?.remove_antag_datum(/datum/antagonist/cult) + target.mind?.remove_antag_datum(/datum/antagonist/shade_minion) + if(!target.mind) + enable_construct_ghost_control(target) + user.visible_message(span_notice("[user] purifies [target]!")) + return + +// Purifying constructs invokes you a curse. You have to pay the bloodtithe; all of your blood. Payed in installments. +/datum/status_effect/debt_to_the_geometer + id = "burden_revered" + alert_type = /atom/movable/screen/alert/status_effect/debt_to_the_geometer + /// Total blood required to pay off the debt. + var/debt_goal = 600 + /// Blood paid so far. + var/debt_paid = 0 + /// Blood lost per second while the effect is active. + var/bleed_per_second = 3 + +/datum/status_effect/debt_to_the_geometer/on_apply() + . = ..() + if(!.) + return FALSE + RegisterSignal(owner, COMSIG_LIVING_DEATH, PROC_REF(on_owner_death)) + return TRUE + +/datum/status_effect/debt_to_the_geometer/tick(seconds_between_ticks) + if(!owner) + return + // You pissed off the Geometer herself. If Nar'Sie exists, ensure we are her current target. + if(GLOB.cult_narsie) + var/turf/owner_turf = get_turf(owner) + if(owner_turf && owner_turf.z == GLOB.cult_narsie.z) + var/datum/component/singularity/singularity_component = GLOB.cult_narsie.singularity?.resolve() + if(singularity_component && singularity_component.target != owner) + GLOB.cult_narsie.acquire(owner) + + // Visual feedback. + spawn_blood_splatter() + + // Periodic blood loss and tracking. + if(istype(owner, /mob/living/carbon)) + var/mob/living/carbon/carbon_owner = owner + // Because ticks & blood are fucky we do before and after for cost mapping + var/before = carbon_owner.blood_volume + carbon_owner.bleed(bleed_per_second * seconds_between_ticks) + var/removed = max(before - carbon_owner.blood_volume, 0) + debt_paid += removed + + if(debt_paid >= debt_goal) + qdel(src) + return + +/datum/status_effect/debt_to_the_geometer/proc/spawn_blood_splatter() + var/turf/bleed_turf = get_turf(owner) + if(!bleed_turf) + return + var/amt + // random blood splatters + switch(rand(1, 100)) + if(1 to 80) + amt = rand(1, 8) + if(81 to 95) + amt = rand(9, 15) + else + amt = rand(16, 25) + owner.add_splatter_floor(bleed_turf, amt) + +/datum/status_effect/debt_to_the_geometer/on_remove() + if(owner) + to_chat(owner, span_cult_bold("The Geometer's notice is no longer upon you.")) + return ..() + +/datum/status_effect/debt_to_the_geometer/proc/on_owner_death(datum/source, gibbed) + SIGNAL_HANDLER + if(!owner) + return + if(istype(owner, /mob/living/carbon)) + var/mob/living/carbon/carbon_owner = owner + carbon_owner.blood_volume = 0 + + var/turf/center = get_turf(owner) + if(center) + for(var/turf/T in range(1, center)) + owner.add_splatter_floor(T, FALSE) + user.visible_message(span_boldwarning("[owner]'s body bursts open, showering blood everywhere!")) + playsound(user, 'sound/effects/wounds/splatter.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + qdel(src) + +/atom/movable/screen/alert/status_effect/debt_to_the_geometer + name = "Debt to the Geometer" + desc = "The Geometer demands you pay the blood price for your actions." + icon = 'icons/obj/mining_zones/artefacts.dmi' + icon_state = "soulstone2" // Placeholder + alerttooltipstyle = "cult" diff --git a/tgstation.dme b/tgstation.dme index 5ad2541d518da4..f0634c443f9419 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7530,6 +7530,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\divine_protection.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\entropic_mending.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\pious_prayer.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\purify.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike_upgrades.dm" #include "modular_doppler\modular_powers\code\security\resonant_cuffs.dm" From c13e1b9db663d04d8297e96a8878e1096d7c611d Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 26 Feb 2026 12:30:31 +0100 Subject: [PATCH 096/212] Fixed a few bugs and quirks with purify. --- .../powers/sorcerous/theologist/purify.dm | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm index da57f2d45d8513..324e58a4dd98b6 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm @@ -253,6 +253,7 @@ // Fallback for the constructs that dont have a purified version e.g proteons if(!typepath) convert_construct_in_place(construct_target, user) + post_conversion(user, construct_target) return TRUE var/mob/living/basic/construct/new_construct = new typepath(construct_target.loc) @@ -263,16 +264,20 @@ else enable_construct_ghost_control(new_construct) - user.visible_message(span_notice("[user] purifies [construct_target]!")) + post_conversion(user, new_construct) + qdel(construct_target) + return TRUE + +// Applies post conversion fluff + curse. +/datum/action/cooldown/power/theologist/purify/proc/post_conversion(mob/living/user, mob/living/target) + user.visible_message(span_notice("[user] purifies [target]!")) playsound(user, 'sound/effects/his_grace/his_grace_ascend.ogg', 50, TRUE) // Special feedback to the construct - to_chat(new_construct, span_blue("The Geometer's presence in your mind fades, what was once your own freewill slips back into the forefront. You look down at your body; and while it is still the dark steel that adorns your body, you can move it of your own free will. Your freedom is returned; but still tethered forevermore to this body.")) + to_chat(target, span_blue("The Geometer's presence in your mind fades, what was once your own freewill slips back into the forefront. You look down at your body; and while it is still the dark steel that adorns your body, you can move it of your own free will. Your freedom is returned; but still tethered forevermore to this body.")) // Special feedback to the caster to_chat(user, span_blue("As you have shown yourself to be pious, to take on the burdens of others; you now take on the greatest burden of another. Whoever the vile entity responsible may be, you take it away from this enslaved tool, and bury the energy responsible deep inside you. You have freed it; but this darkness seems to be eating away at you.")) user.apply_status_effect(/datum/status_effect/debt_to_the_geometer, src) to_chat(user, span_cult_bold("You have been cursed; your actions carry a price, and you shall be made to pay it.")) - qdel(construct_target) - return TRUE // Are we a chaplain or deacon (chaplain sect convertee)? /datum/action/cooldown/power/theologist/purify/proc/are_we_a_holy_man(mob/living/user) @@ -306,6 +311,12 @@ target.theme = THEME_HOLY target.faction = list(FACTION_HOLY) ADD_TRAIT(target, TRAIT_ANGELIC, INNATE_TRAIT) + // Neutralize hostile AI (e.g., lavaland proteons). + if(target.ai_controller) + target.ai_controller = null + var/datum/component/ai_retaliate_advanced/retaliate = target.GetComponent(/datum/component/ai_retaliate_advanced) + if(retaliate) + qdel(retaliate) if(target.icon_state) target.cut_overlay("glow_[target.icon_state]_[old_theme]") target.add_overlay("glow_[target.icon_state]_[target.theme]") @@ -394,8 +405,8 @@ if(center) for(var/turf/T in range(1, center)) owner.add_splatter_floor(T, FALSE) - user.visible_message(span_boldwarning("[owner]'s body bursts open, showering blood everywhere!")) - playsound(user, 'sound/effects/wounds/splatter.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + owner.visible_message(span_boldwarning("[owner]'s body bursts open, showering blood everywhere!")) + playsound(owner, 'sound/effects/wounds/splatter.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) qdel(src) /atom/movable/screen/alert/status_effect/debt_to_the_geometer From 694f430f901b6fc50892ecfe36f8e68980f9804e Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 26 Feb 2026 21:16:21 +0100 Subject: [PATCH 097/212] Minor tweaks to the curse and divine protection. --- .../sorcerous/theologist/divine_protection.dm | 10 ++++++++-- .../code/powers/sorcerous/theologist/purify.dm | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) 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 4727c9bb91b821..ee3d41e011e0ba 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 @@ -36,7 +36,7 @@ if(block_chance <= 0 || !prob(block_chance)) return NONE - block_effect(blocking_user, attack_text) + block_effect(blocking_user, attack_text, hitby, attack_type) // We only allow piety loss once per 0.4 seconds so you don't get your piety nuked by a shotgun. if(world.time >= last_piety_drain + 4) piety_component.adjust_piety(-THEOLOGIST_PIETY_MINOR) @@ -44,13 +44,19 @@ return SUCCESSFUL_BLOCK // Special block effect -/datum/power/theologist/divine_protection/proc/block_effect(mob/living/blocking_user, attack_text) +/datum/power/theologist/divine_protection/proc/block_effect(mob/living/blocking_user, attack_text, atom/movable/hitby, attack_type) if(!blocking_user) return blocking_user.visible_message( span_danger("[attack_text] bounces harmlessly off of [blocking_user]!"), span_userdanger("[attack_text] is blocked by your Divine Protection!"), ) + var/mob/living/attacker = GET_ASSAILANT(hitby) + if(attacker && (attack_type == MELEE_ATTACK || attack_type == UNARMED_ATTACK || attack_type == LEAP_ATTACK || attack_type == OVERWHELMING_ATTACK)) + if(istype(hitby, /obj/item)) + attacker.do_attack_animation(blocking_user, used_item = hitby) + else + attacker.do_attack_animation(blocking_user) // don't trigger the fx more than 1 second to prevent taking ear damage from being shotgunned. if(world.time < last_block_effect + 10) return diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm index 324e58a4dd98b6..81c8f72910d512 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm @@ -330,14 +330,16 @@ // Purifying constructs invokes you a curse. You have to pay the bloodtithe; all of your blood. Payed in installments. /datum/status_effect/debt_to_the_geometer - id = "burden_revered" + id = "debt_to_the_geometer" alert_type = /atom/movable/screen/alert/status_effect/debt_to_the_geometer /// Total blood required to pay off the debt. var/debt_goal = 600 /// Blood paid so far. var/debt_paid = 0 /// Blood lost per second while the effect is active. - var/bleed_per_second = 3 + var/bleed_per_second = 5 + // The curse is starting to tithe blood. + var/curse_has_started = FALSE /datum/status_effect/debt_to_the_geometer/on_apply() . = ..() @@ -349,6 +351,15 @@ /datum/status_effect/debt_to_the_geometer/tick(seconds_between_ticks) if(!owner) return + + // We give a bit of an unpredictable buffer before we start bleeding the person. A bit of space to have them RP. + if(!curse_has_started) + if(prob(0.5)) + curse_has_started = TRUE + to_chat(owner, span_cult_bold("Blood is starting to ooze from every part of your body!")) + else + return + // You pissed off the Geometer herself. If Nar'Sie exists, ensure we are her current target. if(GLOB.cult_narsie) var/turf/owner_turf = get_turf(owner) From caed6d7d406e08878aeb83959b74823a7c13b39d Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 27 Feb 2026 08:40:36 +0100 Subject: [PATCH 098/212] Few minor tweaks --- .../aberrant/_aberrant_root_beastial.dm | 21 +++++++++++++++++++ .../powers/sorcerous/theologist/purify.dm | 1 + tgstation.dme | 1 + 3 files changed, 23 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_beastial.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_beastial.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_beastial.dm new file mode 100644 index 00000000000000..9bc005b1b58d4e --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_beastial.dm @@ -0,0 +1,21 @@ +/datum/power/aberrant_root/beastial + name = "Beastkindred" + desc = "You have the traits of an animal; and with it, the apetite of one. In addition to your species normal preferences, you now like the following food based on your choice.\ + Herbivore: Vegetables & Fruit. \ + Carnivore: Raw, Gore, Meat & Bugs." + value = 2 + +/datum/power/aberrant_root/beastial/add(client/client_source) + var/obj/item/organ/tongue/tongue = power_holder.get_organ_slot(ORGAN_SLOT_TONGUE) + if(!tongue) + return + var/liked_foodtypes = tongue.liked_foodtypes + tongue.liked_foodtypes |= RAW | GORE + tongue.disliked_foodtypes = NONE + +/datum/power/aberrant_root/beastial/remove() + var/obj/item/organ/tongue/tongue = power_holder.get_organ_slot(ORGAN_SLOT_TONGUE) + if(!tongue) + return + tongue.liked_foodtypes = initial(tongue.liked_foodtypes) + tongue.disliked_foodtypes = initial(tongue.disliked_foodtypes) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm index 81c8f72910d512..8331da7003dbec 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm @@ -240,6 +240,7 @@ var/datum/beam/link = user.Beam(construct_target, icon_state = "kinesis", override_target_pixel_x = 0) construct_target.SetStun(15 SECONDS, ignore_canstun = TRUE) // normally you don't use userdanger for this but its a hype moment. + playsound(user, 'sound/effects/magic/forcewall.ogg', 50, TRUE) user.visible_message(span_userdanger("[user] channels a beam of holy energy, attempting to purify any and all unholy qualities of [construct_target]!")) var/channel_success = do_after(user, 15 SECONDS, target = construct_target) construct_target.SetStun(0, ignore_canstun = TRUE) diff --git a/tgstation.dme b/tgstation.dme index f0634c443f9419..ca15c9ed759cfc 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7464,6 +7464,7 @@ #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\resonant_immune.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" From e2788425a0e7dd2d0f1fcb4116ed3eb180c59ac9 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 27 Feb 2026 09:26:33 +0100 Subject: [PATCH 099/212] Allows for choiced preferences to be added. Adds beastial root. --- modular_doppler/modular_powers/code/_power.dm | 71 +++++++++++++++ .../aberrant/_aberrant_root_beastial.dm | 48 ++++++++-- .../code/powers_prefs_middleware.dm | 4 + .../interfaces/PreferencesMenu/PowersMenu.tsx | 91 ++++++++++++++++++- .../powers/beastial_diet.tsx | 8 ++ .../tgui/interfaces/PreferencesMenu/types.ts | 2 + 6 files changed, 216 insertions(+), 8 deletions(-) create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/beastial_diet.tsx diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index 4b1a40c25a9949..fa39a81a2c87fa 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -199,6 +199,77 @@ return new_action +/// Constructs [GLOB.all_power_constant_data] by iterating through a typecache of pregen data, ignoring abstract types, and instantiating the rest. +/proc/generate_power_constant_data() + RETURN_TYPE(/list/datum/power_constant_data) + + var/list/datum/power_constant_data/all_constant_data = list() + + for (var/datum/power_constant_data/iterated_path as anything in typecacheof(path = /datum/power_constant_data, ignore_root_path = TRUE)) + if (initial(iterated_path.abstract_type) == iterated_path) + continue + + if (!isnull(all_constant_data[initial(iterated_path.associated_typepath)])) + stack_trace("pre-existing pregen data for [initial(iterated_path.associated_typepath)] when [iterated_path] was being considered: [all_constant_data[initial(iterated_path.associated_typepath)]]. \ + this is definitely a bug, and is probably because one of the two pregen data have the wrong power typepath defined. [iterated_path] will not be instantiated") + continue + + var/datum/power_constant_data/pregen_data = new iterated_path + all_constant_data[pregen_data.associated_typepath] = pregen_data + + return all_constant_data + +GLOBAL_LIST_INIT_TYPED(all_power_constant_data, /datum/power_constant_data, generate_power_constant_data()) + +/// A singleton datum representing constant data and procs used by powers. +/datum/power_constant_data + var/abstract_type = /datum/power_constant_data + + /// The typepath of the power we will be associated with in the global list. + var/datum/power/associated_typepath + + /// A lazylist of preference datum typepaths. Any character pref put in here will be rendered in the powers page under a dropdown. + var/list/datum/preference/customization_options + +/datum/power_constant_data/New() + . = ..() + + ASSERT(abstract_type != type && !isnull(associated_typepath), "associated_typepath null - please set it! occurred on: [src.type]") + +/// Returns a list of savefile_keys derived from the preference typepaths in [customization_options]. Used in powers middleware to supply the preferences to render. +/datum/power_constant_data/proc/get_customization_data() + RETURN_TYPE(/list) + + var/list/customization_data = list() + + for (var/datum/preference/pref_type as anything in customization_options) + var/datum/preference/pref_instance = GLOB.preference_entries[pref_type] + if (isnull(pref_instance)) + stack_trace("get_customization_data was called before instantiation of [pref_type]!") + continue // just in case its a fluke and its only this one that's not instantiated, we'll check the other pref entries + + customization_data += pref_instance.savefile_key + + return customization_data + +/// Is this power customizable? If true, a button will appear within the power's description box in the powers page, and upon clicking it, +/// will open a customization menu for the power. +/datum/power_constant_data/proc/is_customizable() + return LAZYLEN(customization_options) > 0 + +/datum/power_constant_data/Destroy(force) + var/error_message = "[src], a singleton power constant data instance, was destroyed! This should not happen!" + if (force) + error_message += " NOTE: This Destroy() was called with force == TRUE. This instance will be deleted and replaced with a new one." + stack_trace(error_message) + + if (!force) + return QDEL_HINT_LETMELIVE + + . = ..() + + GLOB.all_power_constant_data[associated_typepath] = new src.type //recover + /// Returns if the power holder should process currently or not. /datum/power/proc/should_process() SHOULD_CALL_PARENT(TRUE) 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 9bc005b1b58d4e..de9d2c5e7c6a39 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 @@ -1,17 +1,27 @@ /datum/power/aberrant_root/beastial name = "Beastkindred" - desc = "You have the traits of an animal; and with it, the apetite of one. In addition to your species normal preferences, you now like the following food based on your choice.\ - Herbivore: Vegetables & Fruit. \ - Carnivore: Raw, Gore, Meat & Bugs." + desc = "You have the traits of an animal; and with it, the apetite of one. In addition to your species normal preferences, you now like the following food based on your choice of Herbivore or Carnivore (including making it non-toxic)\ + \nHerbivore: Vegetables, Fruit & Nuts. \ + \nCarnivore: Raw, Gore, Meat, Bugs & Seafood." value = 2 /datum/power/aberrant_root/beastial/add(client/client_source) var/obj/item/organ/tongue/tongue = power_holder.get_organ_slot(ORGAN_SLOT_TONGUE) if(!tongue) return - var/liked_foodtypes = tongue.liked_foodtypes - tongue.liked_foodtypes |= RAW | GORE - tongue.disliked_foodtypes = NONE + var/diet_choice = client_source?.prefs?.read_preference(/datum/preference/choiced/beastial_diet) + if(isnull(diet_choice)) + diet_choice = "None" + + switch(diet_choice) + if("Herbivore") + tongue.liked_foodtypes |= VEGETABLES | FRUIT | NUTS + tongue.disliked_foodtypes &= ~(VEGETABLES | FRUIT | NUTS) + tongue.toxic_foodtypes &= ~(VEGETABLES | FRUIT | NUTS) + if("Carnivore") + tongue.liked_foodtypes |= RAW | GORE | MEAT | BUGS | SEAFOOD + tongue.disliked_foodtypes &= ~(RAW | GORE | MEAT | BUGS | SEAFOOD) + tongue.toxic_foodtypes &= ~(RAW | GORE | MEAT | BUGS | SEAFOOD) /datum/power/aberrant_root/beastial/remove() var/obj/item/organ/tongue/tongue = power_holder.get_organ_slot(ORGAN_SLOT_TONGUE) @@ -19,3 +29,29 @@ return tongue.liked_foodtypes = initial(tongue.liked_foodtypes) tongue.disliked_foodtypes = initial(tongue.disliked_foodtypes) + tongue.toxic_foodtypes = initial(tongue.toxic_foodtypes) + +// Preference choice for Beastkindred diet selection. +/datum/preference/choiced/beastial_diet + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "beastial_diet" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/beastial_diet/create_default_value() + return "None" + +/datum/preference/choiced/beastial_diet/init_possible_values() + return list("None", "Herbivore", "Carnivore") + +/datum/preference/choiced/beastial_diet/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return TRUE + +/datum/preference/choiced/beastial_diet/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/power_constant_data/beastial + associated_typepath = /datum/power/aberrant_root/beastial + customization_options = list(/datum/preference/choiced/beastial_diet) diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index c486a6ec82b4d7..29c09d73c4f508 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -85,6 +85,8 @@ 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() var/final_list = list(list( "description" = power_type.desc, @@ -97,6 +99,8 @@ "powertype" = powertype, "rootpower" = rootpower, "augment" = augment_info, + "customizable" = constant_data?.is_customizable(), + "customization_options" = customization_options, )) switch(power_type.path) diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx index 1082e66e66a812..fcdd361f01d313 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx @@ -1,7 +1,10 @@ +import { filter } from 'es-toolkit/compat'; +import { useState } from 'react'; import { Box, Button, Dropdown, + Floating, Image, Section, Stack, @@ -9,10 +12,38 @@ import { import { resolveAsset } from '../../assets'; import { useBackend } from '../../backend'; +import { PreferenceList } from './CharacterPreferences/MainPage'; import type { PreferencesMenuData } from './types'; +function getCorrespondingPreferences( + customizationOptions: string[], + relevantPreferences: Record, +) { + return Object.fromEntries( + filter(Object.entries(relevantPreferences), ([key]) => + customizationOptions.includes(key), + ), + ); +} + export const Powers = (props) => { - const { act } = useBackend(); + const { act, data } = useBackend(); + const [customizationExpanded, setCustomizationExpanded] = useState(false); + + const customizationOptions = props.power.customization_options || []; + const hasCustomization = + props.power.customizable && + props.power.has_power && + customizationOptions.length > 0; + const customizationPreferences = hasCustomization + ? getCorrespondingPreferences( + customizationOptions, + data.character_preferences.manually_rendered_features, + ) + : {}; + const hasExpandableCustomization = + hasCustomization && Object.entries(customizationPreferences).length > 0; + return ( { }} >
- {props.power.description} + {/* Allows for newlines in power descs */} + {String(props.power.description) + .split('\n') + .map((line, i, lines) => ( + + {line} + {i < lines.length - 1 &&
} +
+ ))}

{'Cost: ' + props.power.cost} @@ -76,6 +115,54 @@ export const Powers = (props) => { ) : null} + {/* Customization cogwheel for powers */} + + {hasCustomization ? ( + { + e.stopPropagation(); + }} + style={{ + boxShadow: '0px 4px 8px 3px rgba(0, 0, 0, 0.7)', + }} + > + + + + + + + ) + } + > +
+
+
+ ) : null} +
); diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/beastial_diet.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/beastial_diet.tsx new file mode 100644 index 00000000000000..90016069b5484b --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/beastial_diet.tsx @@ -0,0 +1,8 @@ +import type { FeatureChoiced } from '../../base'; +import { FeatureDropdownInput } from '../../dropdowns'; + +export const beastial_diet: FeatureChoiced = { + name: 'Beastkindred Diet', + description: 'Diet preference.', + component: FeatureDropdownInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts b/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts index e033427d2ef7fb..15f1cad4341277 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/types.ts @@ -112,6 +112,8 @@ export type Power = { color: string; powertype: (string | null)[]; rootpower: (string | null)[]; + customizable?: boolean; + customization_options?: string[]; augment?: { location?: string | null; is_arm?: boolean; From 83c9c0a5c3599d13dc052b0e305c74412d824323 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 27 Feb 2026 11:41:22 +0100 Subject: [PATCH 100/212] Adds shapechanging to aberrant. There's two wolves inside you. --- .../resonant/aberrant/_aberrant_action.dm | 4 +- .../powers/resonant/aberrant/shapechange.dm | 265 ++++++++++++++++++ .../resonant/aberrant/shapechange_wolf.dm | 34 +++ .../modular_powers/code/powers_action.dm | 1 + tgstation.dme | 2 + .../powers/shapechange_form.tsx | 8 + 6 files changed, 313 insertions(+), 1 deletion(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_form.tsx 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 083a2e9a993ec9..96374511dc4767 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,2 +1,4 @@ -/datum/action/cooldown/power/aberant +/datum/action/cooldown/power/aberrant name = "abstract aberrant power action - ahelp this" + background_icon_state = "bg_alien" + overlay_icon_state = "bg_alien_border" diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm new file mode 100644 index 00000000000000..97aefb1a09f106 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm @@ -0,0 +1,265 @@ +/** Lets us shapeshift into other mobs! + * Health damage carries over. + * Prones on exit +**/ +/datum/power/aberrant/shapechange + name = "Shapechange" + desc = "You can adjust your body to turn into a specific type of animal (chosen in the power).\ + \n Activating the ability transforms you into the chosen animal. It does not have your name or any other identifying traits, but the number is always the same when you use it (and the security record for this power elaborates on what creature and numbers). \ + \n Using the ability makes you hungry, and cannot be used while you're starving.\ + \n If the creature dies or the effect ends, you are reverted to your normal form (prone on the ground), and all damage taken is transfered to your original form." + value = 5 + + required_powers = list(/datum/power/aberrant_root/beastial) + action_path = /datum/action/cooldown/power/aberrant/shapechange + +/datum/action/cooldown/power/aberrant/shapechange + name = "Shapechange" + desc = "Change into your chosen animal form!" + button_icon = 'icons/mob/simple/pets.dmi' + button_icon_state = "corgi" + + cooldown_time = 300 + + // We're an animorph; this would lock us out if its true. + human_only = FALSE + /// Amount of time it takes to transform. + use_time = 2 SECONDS + /// Nutrition cost when changing into animal form. + var/hunger_cost = 50 + /// Tracks if the current activation performed a shift (not a revert). + var/just_shifted = FALSE + /// Persistent identifier used for the shapeshifted form. + var/shape_identifier = 0 + // The chosen animal form. + var/animal_form + +// Special checks to do with hunger. +/datum/action/cooldown/power/aberrant/shapechange/can_use(mob/living/user, atom/target) + . = ..() + if(!.) + return FALSE + if(user.IsStun() || user.IsKnockdown()) + owner.balloon_alert(user, "stunned!") + return FALSE + // Allow reverting even if starving. + if(user.has_status_effect(/datum/status_effect/shapechange_mob/aberrant)) + return TRUE + if(user.nutrition <= NUTRITION_LEVEL_STARVING) + owner.balloon_alert(user, "too hungry!") + return FALSE + return TRUE + +/datum/action/cooldown/power/aberrant/shapechange/use_action(mob/living/user, atom/target) + var/datum/status_effect/shapechange_mob/aberrant/shapechange = user.has_status_effect(/datum/status_effect/shapechange_mob/aberrant) + if(shapechange) // we don't check for active since that doesn't carry over. + user.remove_status_effect(/datum/status_effect/shapechange_mob/aberrant) + active = FALSE + return TRUE + + just_shifted = FALSE + if(!animal_form) + animal_form = get_shapechange_type(user?.client) + // Makes the icon look like your form after use. + if(ispath(animal_form)) + var/atom/shape_path = animal_form + button_icon = initial(shape_path.icon) + button_icon_state = initial(shape_path.icon_state) + build_all_button_icons(UPDATE_BUTTON_ICON) + var/mob/living/new_shape = create_shapechange_mob(user) + if(!new_shape) + return FALSE + + user.buckled?.unbuckle_mob(user, force = TRUE) + var/datum/status_effect/shapechange_mob/aberrant/applied = new_shape.apply_status_effect(/datum/status_effect/shapechange_mob/aberrant, user, src) + if(!applied) + to_chat(user, span_warning("Unable to shapechange!")) + qdel(new_shape) + return FALSE + just_shifted = TRUE + active = TRUE + return TRUE + +// Override do_use_time for a custom effect. +/datum/action/cooldown/power/aberrant/shapechange/do_use_time(mob/living/user, atom/target) + // Skip the wind-up if we're reverting. + if(user.has_status_effect(/datum/status_effect/shapechange_mob/aberrant)) + return TRUE + if(use_time <= 0) + return TRUE + if(DOING_INTERACTION_WITH_TARGET(user, user)) + return FALSE + + var/old_transform = user.transform + var/animate_step = use_time / 6 + playsound(user, 'sound/effects/wounds/crack1.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + animate(user, transform = matrix() * 1.1, time = animate_step, easing = SINE_EASING) + animate(transform = matrix() * 0.9, time = animate_step, easing = SINE_EASING) + animate(transform = matrix() * 1.2, time = animate_step, easing = SINE_EASING) + animate(transform = matrix() * 0.8, time = animate_step, easing = SINE_EASING) + animate(transform = matrix() * 1.3, time = animate_step, easing = SINE_EASING) + animate(transform = matrix() * 0.1, time = animate_step, easing = SINE_EASING) + + user.balloon_alert(user, "transforming...") + if(!do_after(user, delay = use_time, target = user)) + animate(user, transform = matrix(), time = 0, easing = SINE_EASING) + user.transform = old_transform + return FALSE + user.visible_message(span_warning("[user]'s body rearranges itself with a horrible crunching sound!")) + playsound(user, 'sound/effects/magic/demon_consume.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + return TRUE + +// Subtract hunger on succesful use +/datum/action/cooldown/power/aberrant/shapechange/on_action_success(mob/living/user, atom/target) + . = ..() + if(just_shifted) + user.adjust_nutrition(-hunger_cost) + just_shifted = FALSE + +/datum/action/cooldown/power/aberrant/shapechange/proc/create_shapechange_mob(mob/living/user) + var/shape_type = animal_form + if(!ispath(shape_type)) + return null + var/mob/living/new_shape = new shape_type(user.loc) + apply_shape_identifier(new_shape) + return new_shape + +// Creates the persistent random number for the shapechanger. +/datum/action/cooldown/power/aberrant/shapechange/proc/apply_shape_identifier(mob/living/shape) + if(!shape) + return + if(!shape_identifier) + shape_identifier = rand(1, 999) + shape.identifier = shape_identifier + shape.name = initial(shape.name) + shape.set_name() + +/datum/action/cooldown/power/aberrant/shapechange/proc/get_shapechange_type(client/client_source) + var/choice = client_source?.prefs?.read_preference(/datum/preference/choiced/shapechange_form) + // defaults to parrot incase something is wrong so we don't runtime everything. + if(isnull(choice)) + choice = "Parrot" + switch(choice) + if("Parrot") + return /mob/living/basic/parrot + if("Penguin") + return /mob/living/basic/pet/penguin/emperor + if("Stoat") + return /mob/living/basic/stoat + if("Fox") + return /mob/living/basic/pet/fox + if("Cat") + return /mob/living/basic/pet/cat + if("Corgi") + return /mob/living/basic/pet/dog/corgi + if("Mouse") + return /mob/living/basic/mouse + return /mob/living/basic/parrot + +//Shapechange status effect for aberrant power. We make our own to prevent gibbed RR. +/datum/status_effect/shapechange_mob/aberrant + id = "shapechange_aberrant" + /// The power action that caused the change + var/datum/weakref/source_weakref + /// Whether the shifted body was gibbed when it died + var/last_gibbed = FALSE + +/datum/status_effect/shapechange_mob/aberrant/on_creation(mob/living/new_owner, mob/living/caster, datum/action/cooldown/power/aberrant/shapechange/source_action) + if(!istype(source_action)) + stack_trace("Mob shapechange \"aberrant\" status effect applied without a source action.") + qdel(src) + return + + source_weakref = WEAKREF(source_action) + return ..() + +/datum/status_effect/shapechange_mob/aberrant/on_apply() + var/datum/action/cooldown/power/aberrant/shapechange/source_action = source_weakref.resolve() + if(!QDELETED(source_action) && source_action.owner == caster_mob) + source_action.Grant(owner) + return ..() + +/datum/status_effect/shapechange_mob/aberrant/restore_caster(kill_caster_after) + var/datum/action/cooldown/power/aberrant/shapechange/source_action = source_weakref.resolve() + if(!QDELETED(source_action) && source_action.owner == owner) + source_action.Grant(caster_mob) + + if(owner?.contents) + // Prevent round removal and consuming stuff when losing shapechange + for(var/atom/movable/thing as anything in owner.contents) + if(thing == caster_mob || HAS_TRAIT(thing, TRAIT_NOT_BARFABLE)) + continue + thing.forceMove(get_turf(owner)) + + return ..() + +/datum/status_effect/shapechange_mob/aberrant/on_shape_death(datum/source, gibbed) + last_gibbed = gibbed + if(QDELETED(owner)) + return + restore_caster() + return + +/datum/status_effect/shapechange_mob/aberrant/after_unchange() + . = ..() + if(QDELETED(caster_mob) || QDELETED(owner)) + return + + // Ensure any transform scaling from the shift animation is cleared. + caster_mob.transform = matrix() + + // Transfer damage from the shifted body back to the caster. + var/brute = owner.getBruteLoss() + var/burn = owner.getFireLoss() + var/tox = owner.getToxLoss() + var/oxy = owner.getOxyLoss() + if(brute) + caster_mob.apply_damage(brute, BRUTE, forced = TRUE) + if(burn) + caster_mob.apply_damage(burn, BURN, forced = TRUE) + if(tox) + caster_mob.apply_damage(tox, TOX, forced = TRUE) + if(oxy) + caster_mob.apply_damage(oxy, OXY, forced = TRUE) + + caster_mob.Knockdown(6 SECONDS, ignore_canstun = TRUE) + + // If we died by being gibbed, we instead apply a lot of damage and lob off a limb. + if(last_gibbed) + if(iscarbon(caster_mob)) + var/mob/living/carbon/carbon_caster = caster_mob + var/zone = carbon_caster.get_random_valid_zone(even_weights = TRUE) + var/obj/item/bodypart/part = carbon_caster.get_bodypart(zone) + carbon_caster.apply_damage(150, BRUTE, part ? part : zone, forced = TRUE) + // MY LEG + if(part && part.body_zone != BODY_ZONE_HEAD && part.body_zone != BODY_ZONE_CHEST) + if(part.can_dismember() && !(part.bodypart_flags & BODYPART_UNREMOVABLE)) + part.dismember() + else + caster_mob.apply_damage(150, BRUTE, forced = TRUE) + last_gibbed = FALSE + +// Preference choice for Shapechange form selection. +/datum/preference/choiced/shapechange_form + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "shapechange_form" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/shapechange_form/create_default_value() + return "Parrot" + +/datum/preference/choiced/shapechange_form/init_possible_values() + return list("Parrot", "Penguin", "Stoat", "Fox", "Cat", "Corgi", "Mouse") + +/datum/preference/choiced/shapechange_form/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return TRUE + +/datum/preference/choiced/shapechange_form/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/power_constant_data/shapechange + associated_typepath = /datum/power/aberrant/shapechange + customization_options = list(/datum/preference/choiced/shapechange_form) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm new file mode 100644 index 00000000000000..4090d71dbad1aa --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm @@ -0,0 +1,34 @@ +/** Inside you are two wolves. This one's an example of how to override the shapechange with special mobs. +**/ +/datum/power/aberrant/shapechange_wolf + name = "Shapechange: Wolf" + desc = "Overrides your chosen Shapechange form with a Wolf; a sturdy creature with a strong bite attack." + value = 1 + + required_powers = list(/datum/power/aberrant/shapechange) + /// Saved form so we can restore on removal. + var/previous_form + +/datum/power/aberrant/shapechange_wolf/post_add() + . = ..() + var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action() + if(!shape_action) + return + previous_form = shape_action.animal_form + shape_action.animal_form = /mob/living/basic/mining/wolf + +/datum/power/aberrant/shapechange_wolf/remove() + var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action() + if(shape_action) + shape_action.animal_form = previous_form + previous_form = null + return ..() + +/datum/power/aberrant/shapechange_wolf/proc/get_shapechange_action() + if(!power_holder?.powers) + return null + for(var/datum/power/aberrant/shapechange/shape_power in power_holder.powers) + var/datum/action/cooldown/power/aberrant/shapechange/shape_action = shape_power.action_path + if(istype(shape_action)) + return shape_action + return null diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 081d90cbef8bdd..54e4b01a1dab29 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -90,6 +90,7 @@ if(QDELETED(user)) return FALSE if(!ishuman(user) && human_only) + owner.balloon_alert(user, "not a human!") return FALSE return TRUE diff --git a/tgstation.dme b/tgstation.dme index ca15c9ed759cfc..9f32a0f3ec4751 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7466,6 +7466,8 @@ #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\resonant_immune.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\shapechange.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\shapechange_wolf.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_dantian.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_form.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_form.tsx new file mode 100644 index 00000000000000..52f2be51800649 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_form.tsx @@ -0,0 +1,8 @@ +import type { FeatureChoiced } from '../../base'; +import { FeatureDropdownInput } from '../../dropdowns'; + +export const shapechange_form: FeatureChoiced = { + name: 'Shapechange', + description: 'Chosen animal form.', + component: FeatureDropdownInput, +}; From cb96b8d3fcc4e67c847f4e3acacb931b714f5a9f Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 27 Feb 2026 12:14:09 +0100 Subject: [PATCH 101/212] Adds spiders because spiders are cool. --- .../powers/resonant/aberrant/shapechange.dm | 12 +++- .../resonant/aberrant/shapechange_spider.dm | 71 +++++++++++++++++++ .../resonant/aberrant/shapechange_wolf.dm | 3 +- tgstation.dme | 2 +- .../powers/shapechange_spider_form.tsx | 8 +++ 5 files changed, 92 insertions(+), 4 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_spider.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_spider_form.tsx 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 97aefb1a09f106..9551552065829b 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm @@ -154,6 +154,16 @@ return /mob/living/basic/pet/dog/corgi if("Mouse") return /mob/living/basic/mouse + if("Lizard") + return /mob/living/basic/lizard + if("Snake") + return /mob/living/basic/snake + if("Cockroach") + return /mob/living/basic/cockroach + if("Duct Spider") + return /mob/living/basic/spider/maintenance + if("Butterfly") + return /mob/living/basic/butterfly return /mob/living/basic/parrot //Shapechange status effect for aberrant power. We make our own to prevent gibbed RR. @@ -249,7 +259,7 @@ return "Parrot" /datum/preference/choiced/shapechange_form/init_possible_values() - return list("Parrot", "Penguin", "Stoat", "Fox", "Cat", "Corgi", "Mouse") + return list("Parrot", "Penguin", "Stoat", "Fox", "Cat", "Corgi", "Mouse", "Lizard", "Snake", "Cockroach", "Duct Spider", "Butterfly") /datum/preference/choiced/shapechange_form/is_accessible(datum/preferences/preferences) if (!..(preferences)) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_spider.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_spider.dm new file mode 100644 index 00000000000000..f0ef1097c242e4 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_spider.dm @@ -0,0 +1,71 @@ +// Shapechange spider override. +/datum/power/aberrant/shapechange_spider + name = "Shapechange: Spider" + desc = "Overrides your chosen Shapechange form with a spider variant. \n Hunters are fast but fragile, guards are slow and sturdy and ambush spiders are very slow, but have strong grabs, hard-hitting attacks and invisiblity in webs." + value = 3 + + required_powers = list(/datum/power/aberrant/shapechange) + /// Saved form so we can restore on removal. + var/previous_form + +/datum/power/aberrant/shapechange_spider/post_add() + . = ..() + var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action() + if(!shape_action) + return + previous_form = shape_action.animal_form + shape_action.animal_form = get_spider_form() + +/datum/power/aberrant/shapechange_spider/remove() + var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action() + if(shape_action) + shape_action.animal_form = previous_form + previous_form = null + return ..() + +/datum/power/aberrant/shapechange_spider/proc/get_shapechange_action() + if(!power_holder?.powers) + return null + for(var/datum/power/aberrant/shapechange/shape_power in power_holder.powers) + var/datum/action/cooldown/power/aberrant/shapechange/shape_action = shape_power.action_path + if(istype(shape_action)) + return shape_action + return null + +/datum/power/aberrant/shapechange_spider/proc/get_spider_form() + var/choice = power_holder?.client?.prefs?.read_preference(/datum/preference/choiced/shapechange_spider_form) + if(isnull(choice)) + choice = "Guard" + switch(choice) + if("Hunter") + return /mob/living/basic/spider/giant/hunter + if("Ambush") + return /mob/living/basic/spider/giant/ambush + if("Guard") + return /mob/living/basic/spider/giant/guard + return /mob/living/basic/spider/giant/guard + +// Preference choice for Shapechange spider form selection. +/datum/preference/choiced/shapechange_spider_form + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "shapechange_spider_form" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/shapechange_spider_form/create_default_value() + return "Guard" + +/datum/preference/choiced/shapechange_spider_form/init_possible_values() + return list("Hunter", "Guard", "Ambush") + +/datum/preference/choiced/shapechange_spider_form/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return TRUE + +/datum/preference/choiced/shapechange_spider_form/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/power_constant_data/shapechange_spider + associated_typepath = /datum/power/aberrant/shapechange_spider + customization_options = list(/datum/preference/choiced/shapechange_spider_form) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm index 4090d71dbad1aa..88e9f4ec2826d8 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 @@ -1,5 +1,4 @@ -/** Inside you are two wolves. This one's an example of how to override the shapechange with special mobs. -**/ +// Inside you are two wolves. This one's an example of how to override the shapechange with special mobs. /datum/power/aberrant/shapechange_wolf name = "Shapechange: Wolf" desc = "Overrides your chosen Shapechange form with a Wolf; a sturdy creature with a strong bite attack." diff --git a/tgstation.dme b/tgstation.dme index 9f32a0f3ec4751..08d7b6f9c9f859 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -5256,7 +5256,6 @@ #include "code\modules\mob\living\basic\lavaland\watcher\watcher_gaze.dm" #include "code\modules\mob\living\basic\lavaland\watcher\watcher_overwatch.dm" #include "code\modules\mob\living\basic\lavaland\watcher\watcher_projectiles.dm" -#include "code\modules\mob\living\basic\minebots\minebot.dm" #include "code\modules\mob\living\basic\minebots\minebot_abilities.dm" #include "code\modules\mob\living\basic\minebots\minebot_ai.dm" #include "code\modules\mob\living\basic\minebots\minebot_remote_control.dm" @@ -7467,6 +7466,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root_beastial.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\cultivator\_cultivator_action.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_alignment.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_spider_form.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_spider_form.tsx new file mode 100644 index 00000000000000..85158014827b5a --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/shapechange_spider_form.tsx @@ -0,0 +1,8 @@ +import type { FeatureChoiced } from '../../base'; +import { FeatureDropdownInput } from '../../dropdowns'; + +export const shapechange_spider_form: FeatureChoiced = { + name: 'Shapechange: Spider', + description: 'Chosen spider form.', + component: FeatureDropdownInput, +}; From 2d6fbcdf482041b897074d6641baca6d44ec77c7 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 27 Feb 2026 12:31:13 +0100 Subject: [PATCH 102/212] I am stupid and accidentally'd minebots in the .dme --- .../powers/resonant/aberrant/shapechange.dm | 18 ++++++++++++------ tgstation.dme | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) 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 9551552065829b..9dcc6acca685cd 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm @@ -1,5 +1,5 @@ /** Lets us shapeshift into other mobs! - * Health damage carries over. + * Health damage carries over; halved if transforming back manually. * Prones on exit **/ /datum/power/aberrant/shapechange @@ -7,7 +7,7 @@ desc = "You can adjust your body to turn into a specific type of animal (chosen in the power).\ \n Activating the ability transforms you into the chosen animal. It does not have your name or any other identifying traits, but the number is always the same when you use it (and the security record for this power elaborates on what creature and numbers). \ \n Using the ability makes you hungry, and cannot be used while you're starving.\ - \n If the creature dies or the effect ends, you are reverted to your normal form (prone on the ground), and all damage taken is transfered to your original form." + \n If the creature dies or the effect ends, you are reverted to your normal form (prone on the ground), and all damage taken is transfered to your original form (halved if reverting back manually)." value = 5 required_powers = list(/datum/power/aberrant_root/beastial) @@ -53,6 +53,7 @@ /datum/action/cooldown/power/aberrant/shapechange/use_action(mob/living/user, atom/target) var/datum/status_effect/shapechange_mob/aberrant/shapechange = user.has_status_effect(/datum/status_effect/shapechange_mob/aberrant) if(shapechange) // we don't check for active since that doesn't carry over. + shapechange.manual_revert = TRUE user.remove_status_effect(/datum/status_effect/shapechange_mob/aberrant) active = FALSE return TRUE @@ -173,6 +174,8 @@ var/datum/weakref/source_weakref /// Whether the shifted body was gibbed when it died var/last_gibbed = FALSE + /// Whether the revert was manually triggered. + var/manual_revert = FALSE /datum/status_effect/shapechange_mob/aberrant/on_creation(mob/living/new_owner, mob/living/caster, datum/action/cooldown/power/aberrant/shapechange/source_action) if(!istype(source_action)) @@ -205,6 +208,7 @@ /datum/status_effect/shapechange_mob/aberrant/on_shape_death(datum/source, gibbed) last_gibbed = gibbed + manual_revert = FALSE if(QDELETED(owner)) return restore_caster() @@ -219,10 +223,11 @@ caster_mob.transform = matrix() // Transfer damage from the shifted body back to the caster. - var/brute = owner.getBruteLoss() - var/burn = owner.getFireLoss() - var/tox = owner.getToxLoss() - var/oxy = owner.getOxyLoss() + var/damage_mult = manual_revert ? 0.5 : 1 + var/brute = owner.getBruteLoss() * damage_mult + var/burn = owner.getFireLoss() * damage_mult + var/tox = owner.getToxLoss() * damage_mult + var/oxy = owner.getOxyLoss() * damage_mult if(brute) caster_mob.apply_damage(brute, BRUTE, forced = TRUE) if(burn) @@ -248,6 +253,7 @@ else caster_mob.apply_damage(150, BRUTE, forced = TRUE) last_gibbed = FALSE + manual_revert = FALSE // Preference choice for Shapechange form selection. /datum/preference/choiced/shapechange_form diff --git a/tgstation.dme b/tgstation.dme index 08d7b6f9c9f859..7389313f28082c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -5256,6 +5256,7 @@ #include "code\modules\mob\living\basic\lavaland\watcher\watcher_gaze.dm" #include "code\modules\mob\living\basic\lavaland\watcher\watcher_overwatch.dm" #include "code\modules\mob\living\basic\lavaland\watcher\watcher_projectiles.dm" +#include "code\modules\mob\living\basic\minebots\minebot.dm" #include "code\modules\mob\living\basic\minebots\minebot_abilities.dm" #include "code\modules\mob\living\basic\minebots\minebot_ai.dm" #include "code\modules\mob\living\basic\minebots\minebot_remote_control.dm" From 5af5f7a395b79d6fa26a246989733054a3213900 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 27 Feb 2026 18:13:34 +0100 Subject: [PATCH 103/212] Added vent crawling and bioluminesence as powers. Shifted preference/choiced switches to global lists (thank Ephe). --- code/__DEFINES/~doppler_defines/powers.dm | 1 + .../_globalvars/~doppler_globalvars/powers.dm | 22 +++ .../mortal/augmented/simple_augments.dm | 4 +- .../resonant/aberrant/bioluminescence.dm | 169 ++++++++++++++++++ .../powers/resonant/aberrant/shapechange.dm | 54 +++--- .../resonant/aberrant/shapechange_spider.dm | 17 +- .../powers/resonant/aberrant/vent_crawl.dm | 80 +++++++++ .../modular_powers/code/powers_subsystem.dm | 2 +- tgstation.dme | 3 + .../powers/bioluminescence_color.tsx | 7 + .../powers/bioluminescence_size.tsx | 8 + 11 files changed, 328 insertions(+), 39 deletions(-) create mode 100644 code/_globalvars/~doppler_globalvars/powers.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/vent_crawl.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_color.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_size.tsx diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 5a4f6b37256821..bd56c68bc48621 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -157,6 +157,7 @@ /// Trait held by all under the resonant archetype. #define TRAIT_ARCHETYPE_RESONANT "archetype_resonant" + /** * RESONANT: CULTIVATOR * All defines related to the cultivator powers. diff --git a/code/_globalvars/~doppler_globalvars/powers.dm b/code/_globalvars/~doppler_globalvars/powers.dm new file mode 100644 index 00000000000000..7db8ad3746d74d --- /dev/null +++ b/code/_globalvars/~doppler_globalvars/powers.dm @@ -0,0 +1,22 @@ +// Shapechanger power. +GLOBAL_LIST_INIT(shapechange_form_types, list( + "Parrot" = /mob/living/basic/parrot, + "Penguin" = /mob/living/basic/pet/penguin/emperor, + "Stoat" = /mob/living/basic/stoat, + "Fox" = /mob/living/basic/pet/fox, + "Cat" = /mob/living/basic/pet/cat, + "Corgi" = /mob/living/basic/pet/dog/corgi, + "Mouse" = /mob/living/basic/mouse, + "Lizard" = /mob/living/basic/lizard, + "Snake" = /mob/living/basic/snake, + "Cockroach" = /mob/living/basic/cockroach, + "Duct Spider" = /mob/living/basic/spider/maintenance, + "Butterfly" = /mob/living/basic/butterfly, +)) + +// Shapechanger: Spider power. +GLOBAL_LIST_INIT(shapechange_spider_form_types, list( + "Hunter" = /mob/living/basic/spider/giant/hunter, + "Guard" = /mob/living/basic/spider/giant/guard, + "Ambush" = /mob/living/basic/spider/giant/ambush, +)) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm index abdff9c564bc82..859e55b1aff0b6 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm @@ -52,7 +52,7 @@ ARMS desc = "When implanted, this cybernetic implant will enhance the muscles of the arm to deliver more power-per-action. Install one in each arm \ to pry open doors with your bare hands!" - value = 8 + value = 10 // door forcing + unarmed stacking with cultivator make this a potential balance hazard. augment = /obj/item/organ/cyberimp/arm/strongarm /* @@ -83,7 +83,7 @@ Keep in mind these are HUDS. Not actual eye replacements. name = "Medical HUD Implant" desc = "These cybernetic eye implants will display a medical HUD over everything you see." - value = 5 + value = 4 augment = /obj/item/organ/cyberimp/eyes/hud/medical disable_if_prisoner = FALSE diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm new file mode 100644 index 00000000000000..a1ceb158e65c69 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm @@ -0,0 +1,169 @@ +// he be glowin. can be toggled on or off. +/datum/power/aberrant/bioluminescence + name = "Bioluminescence" + desc = "You can glow! You passively emit the chosen light color; which can be toggled on or off at will. Very slightly increases passive hunger when enabling or disabling the light." + value = 1 + + required_powers = list(/datum/power/aberrant_root/beastial) + action_path = /datum/action/cooldown/power/aberrant/bioluminescence + +/datum/action/cooldown/power/aberrant/bioluminescence + name = "Bioluminescence" + desc = "Toggle on or off your natural light!" + button_icon = 'icons/obj/lighting.dmi' + button_icon_state = "lantern-blue-on" + + cooldown_time = 5 + // start with da pretty lights on + active = TRUE + + var/obj/effect/dummy/lighting_obj/moblight/biolum_light + // Choiced components + var/biolum_range = 3 + var/biolum_power = 1 + var/biolum_color = "#66c5dd" + var/biolum_bonus_range = 0 + var/biolum_size_choice + +/datum/action/cooldown/power/aberrant/bioluminescence/Grant(mob/granted_to) + . = ..() + RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + RegisterSignal(granted_to, COMSIG_CARBON_HELP_ACT, PROC_REF(on_help_act)) + init_biolum_settings_from_prefs() + if(active) + enable_bioluminescence() + +/datum/action/cooldown/power/aberrant/bioluminescence/Remove(mob/removed_from) + . = ..() + UnregisterSignal(removed_from, list(COMSIG_ATOM_DISPEL, COMSIG_CARBON_HELP_ACT)) + disable_bioluminescence() + +/datum/action/cooldown/power/aberrant/bioluminescence/use_action(mob/living/user, atom/target) + active = !active + if(active) + enable_bioluminescence() + else + disable_bioluminescence() + user.adjust_nutrition(-2) + owner.balloon_alert(owner, active ? "bioluminescence on" : "bioluminescence off") + build_all_button_icons(UPDATE_BUTTON_STATUS) + return TRUE + +// Applies the appropriate size from the choiced component. +/datum/action/cooldown/power/aberrant/bioluminescence/proc/apply_biolum_size_settings() + if(isnull(biolum_size_choice)) + biolum_size_choice = "Medium" + switch(biolum_size_choice) + if("Small") + biolum_range = 2 + if("Medium") + biolum_range = 3 + if("Large") + biolum_range = 4 + else + biolum_range = 3 + +// Gets the size and color and applies it to the mob. +/datum/action/cooldown/power/aberrant/bioluminescence/proc/init_biolum_settings_from_prefs() + if(!owner) + return + var/color_choice = owner?.client?.prefs?.read_preference(/datum/preference/color/bioluminescence_color) + var/size_choice = owner?.client?.prefs?.read_preference(/datum/preference/choiced/bioluminescence_size) + if(isnull(color_choice)) + color_choice = "66c5dd" + if(isnull(size_choice)) + size_choice = "Medium" + biolum_size_choice = size_choice + biolum_color = color_choice + if(!isnull(biolum_color) && !findtext(biolum_color, "#", 1, 2)) + biolum_color = "#[biolum_color]" + apply_biolum_size_settings() + +// We turn the light on. +/datum/action/cooldown/power/aberrant/bioluminescence/proc/enable_bioluminescence() + if(!owner || !isliving(owner)) + return + var/mob/living/glowstick_person = owner + QDEL_NULL(biolum_light) + biolum_light = glowstick_person.mob_light( + range = biolum_range + biolum_bonus_range, + power = biolum_power, + color = biolum_color + ) + +// We turn the light off. +/datum/action/cooldown/power/aberrant/bioluminescence/proc/disable_bioluminescence() + QDEL_NULL(biolum_light) + +/datum/action/cooldown/power/aberrant/bioluminescence/proc/on_dispel(mob/owner, atom/dispeller) + SIGNAL_HANDLER + if(!active) + return DISPEL_RESULT_DISPELLED + active = FALSE + disable_bioluminescence() + build_all_button_icons(UPDATE_BUTTON_STATUS) + return DISPEL_RESULT_DISPELLED + +// You can shake em like glowsticks to make em glow MORE. +/datum/action/cooldown/power/aberrant/bioluminescence/proc/on_help_act(mob/living/carbon/source, mob/living/carbon/helper) + SIGNAL_HANDLER + if(!active || !owner || source != owner) + return + if(biolum_bonus_range >= 2) + return + biolum_bonus_range++ + enable_bioluminescence() + addtimer(CALLBACK(src, PROC_REF(decay_biolum_bonus)), 60 SECONDS) + +/datum/action/cooldown/power/aberrant/bioluminescence/proc/decay_biolum_bonus() + if(biolum_bonus_range <= 0) + return + biolum_bonus_range-- + if(active) + enable_bioluminescence() + +// Preference choice for Bioluminescence color selection. +/datum/preference/color/bioluminescence_color + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "bioluminescence_color" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/color/bioluminescence_color/create_default_value() + return "66c5dd" + +/datum/preference/color/bioluminescence_color/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return TRUE + +/datum/preference/color/bioluminescence_color/apply_to_human(mob/living/carbon/human/target, value) + return + +// Preference choice for Bioluminescence size selection. +/datum/preference/choiced/bioluminescence_size + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "bioluminescence_size" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/bioluminescence_size/create_default_value() + return "Medium" + +/datum/preference/choiced/bioluminescence_size/init_possible_values() + return list("Small", "Medium", "Large") + +/datum/preference/choiced/bioluminescence_size/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + + return TRUE + +/datum/preference/choiced/bioluminescence_size/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/power_constant_data/bioluminescence + associated_typepath = /datum/power/aberrant/bioluminescence + customization_options = list( + /datum/preference/color/bioluminescence_color, + /datum/preference/choiced/bioluminescence_size + ) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm index 9dcc6acca685cd..c7a46e5e937d39 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm @@ -34,6 +34,25 @@ // The chosen animal form. var/animal_form +// Register dispel listener on the owner +/datum/action/cooldown/power/aberrant/shapechange/Grant(mob/granted_to) + . = ..() + RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + +/datum/action/cooldown/power/aberrant/shapechange/Remove(mob/removed_from) + . = ..() + UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL) + +/datum/action/cooldown/power/aberrant/shapechange/proc/on_dispel(mob/living/user, atom/dispeller) + SIGNAL_HANDLER + if(user?.has_status_effect(/datum/status_effect/shapechange_mob/aberrant)) + user.remove_status_effect(/datum/status_effect/shapechange_mob/aberrant) + active = FALSE + to_chat(user, span_userdanger("You have been forced out of your shapeshifted form!")) + StartCooldown(300) + return DISPEL_RESULT_DISPELLED + return NONE + // Special checks to do with hunger. /datum/action/cooldown/power/aberrant/shapechange/can_use(mob/living/user, atom/target) . = ..() @@ -140,32 +159,10 @@ // defaults to parrot incase something is wrong so we don't runtime everything. if(isnull(choice)) choice = "Parrot" - switch(choice) - if("Parrot") - return /mob/living/basic/parrot - if("Penguin") - return /mob/living/basic/pet/penguin/emperor - if("Stoat") - return /mob/living/basic/stoat - if("Fox") - return /mob/living/basic/pet/fox - if("Cat") - return /mob/living/basic/pet/cat - if("Corgi") - return /mob/living/basic/pet/dog/corgi - if("Mouse") - return /mob/living/basic/mouse - if("Lizard") - return /mob/living/basic/lizard - if("Snake") - return /mob/living/basic/snake - if("Cockroach") - return /mob/living/basic/cockroach - if("Duct Spider") - return /mob/living/basic/spider/maintenance - if("Butterfly") - return /mob/living/basic/butterfly - return /mob/living/basic/parrot + var/shape_type = GLOB.shapechange_form_types[choice] + if(ispath(shape_type)) + return shape_type + return GLOB.shapechange_form_types["Parrot"] //Shapechange status effect for aberrant power. We make our own to prevent gibbed RR. /datum/status_effect/shapechange_mob/aberrant @@ -265,7 +262,10 @@ return "Parrot" /datum/preference/choiced/shapechange_form/init_possible_values() - return list("Parrot", "Penguin", "Stoat", "Fox", "Cat", "Corgi", "Mouse", "Lizard", "Snake", "Cockroach", "Duct Spider", "Butterfly") + var/list/values = list() + for(var/choice in GLOB.shapechange_form_types) + values += choice + return values /datum/preference/choiced/shapechange_form/is_accessible(datum/preferences/preferences) if (!..(preferences)) 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 f0ef1097c242e4..c2b30ec46ac15e 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 @@ -36,14 +36,10 @@ var/choice = power_holder?.client?.prefs?.read_preference(/datum/preference/choiced/shapechange_spider_form) if(isnull(choice)) choice = "Guard" - switch(choice) - if("Hunter") - return /mob/living/basic/spider/giant/hunter - if("Ambush") - return /mob/living/basic/spider/giant/ambush - if("Guard") - return /mob/living/basic/spider/giant/guard - return /mob/living/basic/spider/giant/guard + var/spider_type = GLOB.shapechange_spider_form_types[choice] + if(ispath(spider_type)) + return spider_type + return GLOB.shapechange_spider_form_types["Guard"] // Preference choice for Shapechange spider form selection. /datum/preference/choiced/shapechange_spider_form @@ -55,7 +51,10 @@ return "Guard" /datum/preference/choiced/shapechange_spider_form/init_possible_values() - return list("Hunter", "Guard", "Ambush") + var/list/values = list() + for(var/choice in GLOB.shapechange_spider_form_types) + values += choice + return values /datum/preference/choiced/shapechange_spider_form/is_accessible(datum/preferences/preferences) if (!..(preferences)) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/vent_crawl.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/vent_crawl.dm new file mode 100644 index 00000000000000..efafcebcbb2266 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/vent_crawl.dm @@ -0,0 +1,80 @@ +// Vent crawling with caveats. Beware; this is quite jerry-rigged just to keep it modular. +/datum/power/aberrant/vent_crawl + name = "Vent Crawl" + desc = "Your anatomy is capable of fitting in tight spaces. You can crawl into vents if you are not wearing anything in your back slot, helmet slot or suit slot. \ + \nIf you are undersized, you can crawl in vents while wearing your normal equipment. Does not work on oversized mobs." + value = 5 + power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES + +/datum/power/aberrant/vent_crawl/add(client/client_source) + . = ..() + if(!power_holder) + return + if(!can_use_ventcrawl(power_holder, provide_feedback = FALSE)) + return + ADD_TRAIT(power_holder, TRAIT_VENTCRAWLER_ALWAYS, src) + RegisterSignal(power_holder, COMSIG_MOB_ALTCLICKON, PROC_REF(on_altclick)) + +/datum/power/aberrant/vent_crawl/remove() + if(power_holder) + REMOVE_TRAIT(power_holder, TRAIT_VENTCRAWLER_ALWAYS, src) + REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src) + UnregisterSignal(power_holder, COMSIG_MOB_ALTCLICKON) + return ..() + +/** Ventcrawling only has two states; always and nude. This is kind-of cringe; but I don't want to tweak ventcrawling.dm unncessarily just for one power. + * We process and check if they're vent_crawling; if true, we check for restricted gear. If true, we immobilize them til they fukken undress. + * It's gross but it's either mr riot suit crawling out of the vents or everyone being buck naked. +**/ +/datum/power/aberrant/vent_crawl/process(seconds_per_tick) + if(!power_holder) + return + // If a different source grants always-ventcrawling, don't enforce restrictions here. + if(HAS_TRAIT(power_holder, TRAIT_VENTCRAWLER_ALWAYS) && !HAS_TRAIT_FROM_ONLY(power_holder, TRAIT_VENTCRAWLER_ALWAYS, src)) + REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src) + return + // Disqualifies for gear check if not ventcrawling + if(!(power_holder.movement_type & VENTCRAWLING) || !HAS_TRAIT(power_holder, TRAIT_MOVE_VENTCRAWLING)) + REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src) + return + // Disqualifies for gear check if undersized + if(HAS_TRAIT(power_holder, TRAIT_UNDERSIZED)) + REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src) + return + + // Check if they are wearing a back slot, helmet slot or suit slot. Hands are fine. + if(has_restricted_gear(power_holder)) + ADD_TRAIT(power_holder, TRAIT_IMMOBILIZED, src) + // Clear it incase they don't and are immobilized from this. + else + REMOVE_TRAIT(power_holder, TRAIT_IMMOBILIZED, src) + +// Alt clicking on vents. Prevent them from venting if they're wearing too much crap. +/datum/power/aberrant/vent_crawl/proc/on_altclick(mob/living/source, atom/target) + SIGNAL_HANDLER + if(!can_use_ventcrawl(source)) + if(istype(target, /obj/machinery/atmospherics/components/unary)) + return COMSIG_MOB_CANCEL_CLICKON + return + if(!istype(target, /obj/machinery/atmospherics/components/unary)) + return + if(HAS_TRAIT(source, TRAIT_UNDERSIZED)) + return + if(!has_restricted_gear(source)) + return + source.balloon_alert(source, "Need empty back, helmet & suit slot!") + to_chat(source, span_warning("You need to remove your backpack, helmet, and suit to ventcrawl!")) + return COMSIG_MOB_CANCEL_CLICKON + +// Are you TOO FUKKEN BIG? +/datum/power/aberrant/vent_crawl/proc/can_use_ventcrawl(mob/living/source, provide_feedback = TRUE) + if(HAS_TRAIT(source, TRAIT_OVERSIZED)) + if(provide_feedback) + to_chat(source, span_warning("You're too large to fit into the ventilation ducts!")) + return FALSE + return TRUE + +// Checks for back slot, head slot and suit slot +/datum/power/aberrant/vent_crawl/proc/has_restricted_gear(mob/living/source) + var/mob/living/carbon/carbon_source = source + return carbon_source.get_item_by_slot(ITEM_SLOT_BACK) || carbon_source.get_item_by_slot(ITEM_SLOT_HEAD) || carbon_source.get_item_by_slot(ITEM_SLOT_OCLOTHING) diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index dc3684e0dd151f..ebb62adefbd44a 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -1,7 +1,7 @@ // Both of these lists are shifted to glob so they are generated at world start instead of risking players doing preference stuff before the subsystem inits. GLOBAL_LIST_INIT_TYPED(powers_blacklist, /list/datum/power, list( - //list(/datum/power/item_power/thaumaturge_root, /datum/power/enigmatist_root), + list(/datum/power/aberrant/shapechange_spider, /datum/power/aberrant/shapechange_wolf), )) GLOBAL_LIST_INIT(powers_requirements_list, generate_powers_requirements_list()) diff --git a/tgstation.dme b/tgstation.dme index 7389313f28082c..6e5e342a959809 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -654,6 +654,7 @@ #include "code\_globalvars\~doppler_globalvars\bitfields.dm" #include "code\_globalvars\~doppler_globalvars\configuration.dm" #include "code\_globalvars\~doppler_globalvars\objective.dm" +#include "code\_globalvars\~doppler_globalvars\powers.dm" #include "code\_globalvars\~doppler_globalvars\regexes.dm" #include "code\_globalvars\~doppler_globalvars\religion.dm" #include "code\_globalvars\~doppler_globalvars\text.dm" @@ -7465,10 +7466,12 @@ #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\bioluminescence.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\vent_crawl.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_dantian.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_color.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_color.tsx new file mode 100644 index 00000000000000..a3f254cf1a2e1f --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_color.tsx @@ -0,0 +1,7 @@ +import { FeatureColorInput, type Feature } from '../../base'; + +export const bioluminescence_color: Feature = { + name: 'Bioluminescence Color', + description: 'Chosen glow color.', + component: FeatureColorInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_size.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_size.tsx new file mode 100644 index 00000000000000..db2675e99d6ee1 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/bioluminescence_size.tsx @@ -0,0 +1,8 @@ +import type { FeatureChoiced } from '../../base'; +import { FeatureDropdownInput } from '../../dropdowns'; + +export const bioluminescence_size: FeatureChoiced = { + name: 'Bioluminescence Size', + description: 'Chosen glow size.', + component: FeatureDropdownInput, +}; From c9751db521a44d6ab4a8191285cec1a849017491 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 28 Feb 2026 08:48:05 +0100 Subject: [PATCH 104/212] Adds bloodhound, makes bioluminescence a global list, makes shapechange wolf's damage not ass. --- .../_globalvars/~doppler_globalvars/powers.dm | 7 + .../resonant/aberrant/bioluminescence.dm | 19 +- .../powers/resonant/aberrant/bloodhound.dm | 178 ++++++++++++++++++ .../resonant/aberrant/shapechange_wolf.dm | 9 +- tgstation.dme | 1 + 5 files changed, 203 insertions(+), 11 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm diff --git a/code/_globalvars/~doppler_globalvars/powers.dm b/code/_globalvars/~doppler_globalvars/powers.dm index 7db8ad3746d74d..b589a8879e3674 100644 --- a/code/_globalvars/~doppler_globalvars/powers.dm +++ b/code/_globalvars/~doppler_globalvars/powers.dm @@ -20,3 +20,10 @@ GLOBAL_LIST_INIT(shapechange_spider_form_types, list( "Guard" = /mob/living/basic/spider/giant/guard, "Ambush" = /mob/living/basic/spider/giant/ambush, )) + +// Light sizes for bioluminescene +GLOBAL_LIST_INIT(bioluminescence_sizes, list( + "Small" = 2, + "Medium" = 3, + "Large" = 4, +)) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm index a1ceb158e65c69..5e88ecab31fd3d 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm @@ -53,15 +53,11 @@ /datum/action/cooldown/power/aberrant/bioluminescence/proc/apply_biolum_size_settings() if(isnull(biolum_size_choice)) biolum_size_choice = "Medium" - switch(biolum_size_choice) - if("Small") - biolum_range = 2 - if("Medium") - biolum_range = 3 - if("Large") - biolum_range = 4 - else - biolum_range = 3 + var/size_range = GLOB.bioluminescence_sizes[biolum_size_choice] + if(isnum(size_range)) + biolum_range = size_range + else + biolum_range = GLOB.bioluminescence_sizes["Medium"] // Gets the size and color and applies it to the mob. /datum/action/cooldown/power/aberrant/bioluminescence/proc/init_biolum_settings_from_prefs() @@ -150,7 +146,10 @@ return "Medium" /datum/preference/choiced/bioluminescence_size/init_possible_values() - return list("Small", "Medium", "Large") + var/list/values = list() + for(var/choice in GLOB.bioluminescence_sizes) + values += choice + return values /datum/preference/choiced/bioluminescence_size/is_accessible(datum/preferences/preferences) if (!..(preferences)) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm new file mode 100644 index 00000000000000..efb578b470e6fa --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm @@ -0,0 +1,178 @@ +// Lets you sniff someone out based on their blood. Sorta similar toa pinpointer. +/datum/power/aberrant/bloodhound + name = "Bloodhound" + desc = "A whiff of someone's blood, and you're right on their tail. Select a source of blood and it will be your currently active scent. You can only have one active source of scent, and it only lasts for a few minutes.\ + \n Whilst you have someone's blood, you have an indicator of your quarry's direction. Does not work on scrying immune creatures." + value = 10 + + required_powers = list(/datum/power/aberrant_root/beastial) + action_path = /datum/action/cooldown/power/aberrant/bloodhound + +/datum/action/cooldown/power/aberrant/bloodhound + name = "Bloodhound" + desc = "Track someone using a sample their blood. By targeting a source of blood, you acquire your quarry, allowing you to track their direction for a limited time." + button_icon = 'icons/mob/actions/actions_ecult.dmi' + button_icon_state = "cleave" + click_to_activate = TRUE + target_range = 1 + + // How long you can keep a mob's scent. + var/scent_duration = 2 MINUTES + +/datum/action/cooldown/power/aberrant/bloodhound/use_action(mob/living/user, atom/target) + var/list/dna_samples = get_blood_dna_list_from_target(target) + if(!length(dna_samples)) + user.balloon_alert(user, "You need blood to focus your scent.") + return FALSE + + // If your list of dna samples has multiples then my man you gotta clean your samples. Chooses a random one. + var/selected_dna = pick(dna_samples) + var/mob/living/chosen_target = find_target_from_dna(selected_dna) + if(!chosen_target) + user.balloon_alert(user, "No scent to follow.") + return FALSE + + if(!can_affect_bloodhound(chosen_target)) + user.balloon_alert(user, "No scent to follow.") + return FALSE + + var/datum/status_effect/power/bloodhound_scent/applied = user.apply_status_effect(/datum/status_effect/power/bloodhound_scent, scent_duration, chosen_target) + if(!applied) + return FALSE + + user.emote("sniff") + to_chat(user, span_notice("You catch someone's scent!")) + return TRUE + +// Checks if the target can be affected by bloodhound tracking. Basically magic resistance + scrying immunity. +/datum/action/cooldown/power/aberrant/bloodhound/proc/can_affect_bloodhound(mob/living/target) + if(target.can_block_magic(MAGIC_RESISTANCE)) + return FALSE + if(target.can_block_resonance()) + return FALSE + if(HAS_TRAIT(target, TRAIT_ANTIRESONANCE_SCRYING)) + return FALSE + return TRUE + +// Gets DNA from blood +/datum/action/cooldown/power/aberrant/bloodhound/proc/get_blood_dna_list_from_target(atom/target) + if(isnull(target)) + return null + + var/list/dna_list = list() + + if(ismob(target)) + return dna_list + + // Gets dna from a blood decal. + if(istype(target, /obj/effect/decal/cleanable/blood)) + var/obj/effect/decal/cleanable/blood/blood_decal = target + if(blood_decal.dried || blood_decal.bloodiness <= 0) // we don't count dry blood. The trail has gone cold. + return dna_list + var/list/blood = GET_ATOM_BLOOD_DNA(target) + for(var/dna in blood) + dna_list += dna + return dna_list + + // Gets dna from blood from reagent containers. Note: There's a bug with scraping blood not saving DNA; so if it acts weirds its likely that (as of 20/02/26) + if(istype(target, /obj/item/reagent_containers)) + for(var/datum/reagent/present_reagent as anything in target.reagents?.reagent_list) + if(!istype(present_reagent, /datum/reagent/blood)) + continue + var/blood_dna = present_reagent.data?["blood_DNA"] + if(isnull(blood_dna)) + continue + if(islist(blood_dna)) + for(var/dna in blood_dna) + dna_list += dna + else + dna_list += blood_dna + + // Any non-mob atom with forensics blood on it (e.g. clothes, tools) + var/list/blood = GET_ATOM_BLOOD_DNA(target) + if(length(blood)) + for(var/dna in blood) + dna_list += dna + + return dna_list + +// Checks the blood for a dna match. +/datum/action/cooldown/power/aberrant/bloodhound/proc/find_target_from_dna(selected_dna) + if(!selected_dna) + return null + + for(var/mob/living/target in GLOB.mob_list) + if(isobserver(target)) + continue + var/list/blood_dna = target.get_blood_dna_list() + if(blood_dna && blood_dna[selected_dna]) + return target + return null + +// Status effect meant for bloodhound +/datum/status_effect/power/bloodhound_scent + id = "bloodhound_scent" + status_type = STATUS_EFFECT_REPLACE + show_duration = TRUE + tick_interval = STATUS_EFFECT_AUTO_TICK + alert_type = /atom/movable/screen/alert/status_effect/bloodhound_scent + + var/datum/weakref/target_ref + +/datum/status_effect/power/bloodhound_scent/on_creation(mob/living/new_owner, passed_duration, mob/living/target) + if(isnum(passed_duration)) + duration = passed_duration + else // we should always pass a duration so something went wrong. We fall back on this. + duration = 1 MINUTES + + if(ismob(target)) + target_ref = WEAKREF(target) + . = ..() + update_direction_indicator() + +// If we have no target we nuke the power. +/datum/status_effect/power/bloodhound_scent/on_apply() + var/mob/living/target = target_ref?.resolve() + if(!target) + return FALSE + return TRUE + +/datum/status_effect/power/bloodhound_scent/tick(seconds_between_ticks) + if(prob(1)) + owner.emote("sniff") + update_direction_indicator() + +// Updates the direction indicator on the status effect (what we use to convey direction) +/datum/status_effect/power/bloodhound_scent/proc/update_direction_indicator() + if(!owner || QDELETED(owner)) + qdel(src) + return + + var/mob/living/target = target_ref?.resolve() + if(!target || QDELETED(target)) + qdel(src) + return + + var/turf/here = get_turf(owner) + var/turf/there = get_turf(target) + if(!here || !there || here.z != there.z) + return + + var/dir_to_target = get_dir(here, there) + if(!dir_to_target) + return + + var/dir_text = uppertext(dir2text(dir_to_target)) + var/image/dir_image = GLOB.all_radial_directions[dir_text] // this is literally the best list of direction indicators I could find lmao + if(!dir_image || !linked_alert) + return + + linked_alert.icon = dir_image.icon + linked_alert.icon_state = dir_image.icon_state + linked_alert.dir = dir_image.dir + +/atom/movable/screen/alert/status_effect/bloodhound_scent + name = "Bloodhound" + desc = "Your senses point the way to your quarry." + icon = 'icons/testing/turf_analysis.dmi' + icon_state = "red_arrow" diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm index 88e9f4ec2826d8..076c3aa29d4d55 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 @@ -14,12 +14,14 @@ if(!shape_action) return previous_form = shape_action.animal_form - shape_action.animal_form = /mob/living/basic/mining/wolf + shape_action.animal_form = /mob/living/basic/mining/wolf/shapechange /datum/power/aberrant/shapechange_wolf/remove() var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action() if(shape_action) shape_action.animal_form = previous_form + shape_action.melee_damage_lower = initial(shape_action.melee_damage_lower) + shape_action.melee_damage_upper = initial(shape_action.melee_damage_upper) previous_form = null return ..() @@ -31,3 +33,8 @@ if(istype(shape_action)) return shape_action return null + +// Wolves are pack animals and only deal 7dmg wich is SAD. We have a special verison +/mob/living/basic/mining/wolf/shapechange + melee_damage_lower = 10 + melee_damage_upper = 20 diff --git a/tgstation.dme b/tgstation.dme index 6e5e342a959809..b5df12578c075e 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7467,6 +7467,7 @@ #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\bioluminescence.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bloodhound.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" From 753d4dadc85b2f4e7880410811d4f0c5c026199d Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 28 Feb 2026 12:00:40 +0100 Subject: [PATCH 105/212] Added web crafting and its various upgrades. Sticky! Also tweaked some powers here and there. --- .../powers/resonant/aberrant/bloodhound.dm | 4 +- .../resonant/aberrant/shapechange_wolf.dm | 2 - .../powers/resonant/aberrant/vent_crawl.dm | 1 + .../aberrant/web_crafter/_web_craft_datum.dm | 61 ++++++++ .../aberrant/web_crafter/snare_webs.dm | 73 +++++++++ .../aberrant/web_crafter/tripwire_webs.dm | 107 +++++++++++++ .../aberrant/web_crafter/web_crafter.dm | 140 ++++++++++++++++++ .../web_crafter/web_crafter_entries.dm | 29 ++++ .../modular_powers/icons/items/restraints.dmi | Bin 0 -> 5023 bytes tgstation.dme | 5 + 10 files changed, 418 insertions(+), 4 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_craft_datum.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/snare_webs.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/tripwire_webs.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter_entries.dm create mode 100644 modular_doppler/modular_powers/icons/items/restraints.dmi 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 efb578b470e6fa..97becf35153306 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm @@ -46,10 +46,10 @@ // Checks if the target can be affected by bloodhound tracking. Basically magic resistance + scrying immunity. /datum/action/cooldown/power/aberrant/bloodhound/proc/can_affect_bloodhound(mob/living/target) - if(target.can_block_magic(MAGIC_RESISTANCE)) - return FALSE if(target.can_block_resonance()) return FALSE + if(target.can_block_magic(MAGIC_RESISTANCE)) + return FALSE if(HAS_TRAIT(target, TRAIT_ANTIRESONANCE_SCRYING)) return FALSE return TRUE 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 076c3aa29d4d55..5c20387bd0b012 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 @@ -20,8 +20,6 @@ var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action() if(shape_action) shape_action.animal_form = previous_form - shape_action.melee_damage_lower = initial(shape_action.melee_damage_lower) - shape_action.melee_damage_upper = initial(shape_action.melee_damage_upper) previous_form = null return ..() 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 efafcebcbb2266..4266e0ef6d37de 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 @@ -5,6 +5,7 @@ \nIf you are undersized, you can crawl in vents while wearing your normal equipment. Does not work on oversized mobs." value = 5 power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES + required_powers = list(/datum/power/aberrant_root/beastial) /datum/power/aberrant/vent_crawl/add(client/client_source) . = ..() diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_craft_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_craft_datum.dm new file mode 100644 index 00000000000000..b85048af3c52d1 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_craft_datum.dm @@ -0,0 +1,61 @@ +// Used to store what web crafter can make and pass it back to the power. Partially so other powers can add onto it without too much hasle. +/datum/web_craft_entry + /// Type spawned by this entry + var/obj/spawn_type + /// Hunger cost to craft + var/hunger_cost = 0 + /// Display name for the radial + var/display_name + /// Description shown in tooltip + var/desc + /// Icon data for radial choice + var/icon + var/icon_state + /// Whether this should be placed on the turf instead of in hands + var/is_structure = FALSE + +/datum/web_craft_entry/New() + . = ..() + if(ispath(spawn_type, /obj/structure)) + is_structure = TRUE + if(ispath(spawn_type)) + if(!display_name) + display_name = spawn_type.name + if(!desc) + desc = spawn_type.desc + if(!icon) + icon = spawn_type.icon + if(!icon_state) + icon_state = spawn_type.icon_state + +/datum/web_craft_entry/proc/get_radial_choice() + if(!display_name || !icon || !icon_state) + return null + var/datum/radial_menu_choice/choice = new() + choice.name = display_name + choice.image = image(icon = icon, icon_state = icon_state) + return choice + +/datum/web_craft_entry/proc/can_place(mob/living/user, turf/target_turf) + return TRUE + +// In the event we need to pass data to the object, e.g tripwire webs or do other fancy stuff on spawn +/datum/web_craft_entry/proc/spawn_entry(mob/living/user, turf/target_turf) + if(is_structure) + return spawn_structure(user, target_turf) + return spawn_item(user) + +/datum/web_craft_entry/proc/spawn_item(mob/living/user) + return new spawn_type(user) + +/datum/web_craft_entry/proc/spawn_structure(mob/living/user, turf/target_turf) + return new spawn_type(target_turf) + +/datum/web_craft_entry/stickyweb/can_place(mob/living/user, turf/target_turf) + if(HAS_TRAIT(target_turf, TRAIT_SPINNING_WEB_TURF)) + user.balloon_alert(user, "already being webbed!") + return FALSE + if(locate(/obj/structure/spider/stickyweb) in target_turf) + user.balloon_alert(user, "already webbed!") + return FALSE + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/snare_webs.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/snare_webs.dm new file mode 100644 index 00000000000000..16edfb7fb1cbb9 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/snare_webs.dm @@ -0,0 +1,73 @@ +// Creates snaaaares +/datum/power/aberrant/snare_webs + name = "Snare Webs" + desc = " Allows you to craft web restraints and web bolas using web crafter. Web restraints are functionally similar to zipties. Web Bolas can be thrown just like regular bolas." + value = 3 + + required_powers = list(/datum/action/cooldown/power/aberrant/web_crafter) + +/datum/power/aberrant/snare_webs/post_add(client/client_source) + . = ..() + var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action() + if(!action) + return + action.web_craft_entries |= /datum/web_craft_entry/web_restraints + action.web_craft_entries |= /datum/web_craft_entry/web_bola + +/datum/power/aberrant/snare_webs/remove() + var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action() + if(!action) + return + action.web_craft_entries -= /datum/web_craft_entry/web_restraints + action.web_craft_entries -= /datum/web_craft_entry/web_bola + +/datum/power/aberrant/snare_webs/proc/get_web_crafter_action() + if(!power_holder) + return null + for(var/datum/action/cooldown/power/aberrant/web_crafter/action in power_holder.actions) + return action + return null + +// Reflavored +/obj/item/restraints/handcuffs/cable/zipties/web + name = "web ties" + desc = "Sticky strings meant for binding pesky hands. Be careful not to get yourself stuck!" + breakouttime = 60 SECONDS // sticky = better + trashtype = null + /// Tracks if this was actually used as cuffs so we can delete on uncuff only. + var/was_cuffed = FALSE + +// If you're not a web weaver yourself, you might get yourself stuck using it instead. Or if you're clumsy, it will DEFINETLY happy. +/obj/item/restraints/handcuffs/cable/zipties/web/attempt_to_cuff(mob/living/carbon/victim, mob/living/user) + if(iscarbon(user) && !HAS_TRAIT(user, TRAIT_WEB_SURFER) && (HAS_TRAIT(user, TRAIT_CLUMSY) || prob(50))) + to_chat(user, span_warning("Your hands get stuck in the webs!")) + apply_cuffs(user, user) + return + return ..() + +/obj/item/restraints/handcuffs/cable/zipties/web/equipped(mob/living/user, slot) + . = ..() + if(slot == ITEM_SLOT_HANDCUFFED) + was_cuffed = TRUE + RegisterSignal(src, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(on_uncuffed)) + +// why do we not have an uncuff proc on cuffs hello?!?!?! +/obj/item/restraints/handcuffs/cable/zipties/web/proc/on_uncuffed(datum/source, force, atom/newloc, no_move, invdrop, silent) + SIGNAL_HANDLER + if(was_cuffed) + qdel(src) + +// Just normal bolas but extra webby and the same caveat as handcuffs. +/obj/item/restraints/legcuffs/bola/web + name = "web bola" + desc = "A bola made out of a sticky material. Throwing this will definetly get at least one involved party stuck." + breakouttime = 6 SECONDS // sticky = better + icon = 'modular_doppler/modular_powers/icons/items/restraints.dmi' + +// Just like webcuffs, chance of ensnaring yourself instead +/obj/item/restraints/legcuffs/bola/web/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, gentle = FALSE, quickstart = TRUE, throw_type_path = /datum/thrownthing) + if(iscarbon(thrower) && !HAS_TRAIT(thrower, TRAIT_WEB_SURFER) && (HAS_TRAIT(thrower, TRAIT_CLUMSY) || prob(50))) + to_chat(thrower, span_warning("The bola sticks to your hands, whiffing the throw and entangling yourself instead!")) + ensnare(thrower) + return + return ..() diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/tripwire_webs.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/tripwire_webs.dm new file mode 100644 index 00000000000000..88b17760f0e643 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/tripwire_webs.dm @@ -0,0 +1,107 @@ +/datum/power/aberrant/tripwire_webs + name = "Tripwire Webs" + desc = "Allows you to place near- invisible tripwires using web crafter.\ + \n Any creature that isn't able to safely pass webs will trigger the tripwire when they pass through it, destroying it and warning you of which wire was triggered.\ + \n Creatures immune to resonant scrying can trigger the webs without notifying you. Extreme distances and non-movement destruction will also not notify you." + value = 3 + + required_powers = list(/datum/action/cooldown/power/aberrant/web_crafter) + +/datum/power/aberrant/tripwire_webs/post_add(client/client_source) + . = ..() + var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action() + if(!action) + return + action.web_craft_entries |= /datum/web_craft_entry/tripwire_web + +/datum/power/aberrant/tripwire_webs/remove() + var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action() + if(!action) + return + action.web_craft_entries -= /datum/web_craft_entry/tripwire_web + +/datum/power/aberrant/tripwire_webs/proc/get_web_crafter_action() + if(!power_holder) + return null + for(var/datum/action/cooldown/power/aberrant/web_crafter/action in power_holder.actions) + return action + return null + +/obj/structure/spider/tripwire_web + name = "tripwire web" + desc = "Nearly invisible silk stretched tight." + icon = 'icons/effects/navigation.dmi' // see pick_icon_state + icon_state = "2-5" // default shown for the webcrafting + anchored = TRUE + density = FALSE + alpha = 15 + max_integrity = 1 + layer = ABOVE_OPEN_TURF_LAYER + plane = FLOOR_PLANE + /// Who placed the tripwire, if any + var/datum/weakref/maker_ref + +/obj/structure/spider/tripwire_web/Initialize(mapload, mob/living/maker) + . = ..() + if(maker) + maker_ref = WEAKREF(maker) + pick_icon_state() + +/** So we don't actually have the old web sprites; a lot of web sprites are DENSE and noticeable. So we take the navigation lines and place one randomly on the tile. Boom, tripwire. + * We filter out the ones that start with a 0 because they're dead-ends +**/ +/obj/structure/spider/tripwire_web/proc/pick_icon_state() + var/static/list/valid_states + if(!valid_states) + valid_states = list() + for(var/state in icon_states(icon)) + if(copytext(state, 1, 2) == "0") + continue + valid_states += state + if(length(valid_states)) + icon_state = pick(valid_states) + +// Basically the most reliable proc to use for passing through a space. +/obj/structure/spider/tripwire_web/CanAllowThrough(atom/movable/mover, border_dir) + . = ..() + if(!isliving(mover)) + return . + if(HAS_TRAIT(mover, TRAIT_WEB_SURFER)) + return . + triggered(mover) + return TRUE + +/obj/structure/spider/tripwire_web/proc/triggered(mob/living/triggerer) + var/mob/living/maker = maker_ref?.resolve() + if(!should_notify_maker(maker, triggerer)) + qdel(src) + return + if(maker) + var/area/area_loc = get_area(src) + var/area_name = area_loc ? area_loc.name : "unknown area" + to_chat(maker, span_warning("Your tripwire in [area_name] was triggered!")) + qdel(src) + +// We do not notify the maker under certain circumstances. +/obj/structure/spider/tripwire_web/proc/should_notify_maker(mob/living/maker, mob/living/triggerer) + // DO WE EXIST? + if(!maker) + return FALSE + // They're not on the same z level (maps with multiple Zs are fine) + var/turf/maker_turf = get_turf(maker) + var/turf/web_turf = get_turf(src) + if(!maker_turf || !web_turf || !is_valid_z_level(maker_turf, web_turf)) + return FALSE + // It was destroyed without a triggerer + if(!triggerer) + return FALSE + // The triggerer is immune to resonance + if(triggerer.can_block_resonance()) + return FALSE + // The triggerer is immune to magic + if(triggerer.can_block_magic(MAGIC_RESISTANCE)) + return FALSE + // The triggerer is immune to scrying + if(HAS_TRAIT(triggerer, TRAIT_ANTIRESONANCE_SCRYING)) + return FALSE + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm new file mode 100644 index 00000000000000..055e10b2e59cf1 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm @@ -0,0 +1,140 @@ +// Web crafting! Create various doodads associated with web crafting. +/datum/power/aberrant/web_crafter + name = "Web Crafter" + desc = "Threads of spidery silk crafted at your leisure. You gain the Web Crafting ability. You can use it to make passive webs in an area (which do not slow you down); or you can use it to make cloth.\ + \n Creating anything using web crafter makes you hungry, and you cannot use it if you are starving." + value = 3 + + required_powers = list(/datum/power/aberrant_root/beastial) + action_path = /datum/action/cooldown/power/aberrant/web_crafter + +// Lets us walk on webs +/datum/power/aberrant/web_crafter/add(client/client_source) + if(power_holder) + ADD_TRAIT(power_holder, TRAIT_WEB_SURFER, REF(src)) + +/datum/power/aberrant/web_crafter/remove() + if(power_holder) + REMOVE_TRAIT(power_holder, TRAIT_WEB_SURFER, REF(src)) + +/datum/action/cooldown/power/aberrant/web_crafter + name = "Web Crafter" + desc = "Spend some of your satiation to craft web-like objects!" + button_icon = 'icons/effects/web.dmi' + button_icon_state = "webpassage" + + cooldown_time = 10 + + /// Entries shown in the radial menu. Other powers can append to this. + /// Accepts /datum/web_craft_entry instances or typepaths of that datum. + var/list/web_craft_entries = list( + /datum/web_craft_entry/cloth, + /datum/web_craft_entry/stickyweb + ) + +/datum/action/cooldown/power/aberrant/web_crafter/use_action(mob/living/user, atom/target) + var/list/entries = get_web_craft_entries() + if(!length(entries)) + user.balloon_alert(user, "no web crafts!") + return FALSE + + var/list/key_to_entry = list() + var/list/radial_options = build_radial_options(entries, key_to_entry) + if(!length(radial_options)) + user.balloon_alert(user, "no web crafts!") + return FALSE + + var/picked_key = show_radial_menu(user, user, radial_options) + + if(!picked_key) + return FALSE + + var/datum/web_craft_entry/entry = key_to_entry[picked_key] + if(!entry) + return FALSE + + if(!can_craft_entry(user, entry)) + return FALSE + + if(!create_obj(user, entry)) + return FALSE + + if(!HAS_TRAIT(user, TRAIT_NOHUNGER)) + user.adjust_nutrition(-entry.hunger_cost) + return TRUE + +/datum/action/cooldown/power/aberrant/web_crafter/can_use(mob/living/user, atom/target) + . = ..() + if(!.) + return FALSE + // No using when you're hungry. + if(!HAS_TRAIT(user, TRAIT_NOHUNGER) && user.nutrition <= NUTRITION_LEVEL_STARVING) + owner.balloon_alert(user, "too hungry!") + return FALSE + return TRUE + +// Populates the list of web entries +/datum/action/cooldown/power/aberrant/web_crafter/proc/get_web_craft_entries() + // Normalize any typepaths to instances. + for(var/i in 1 to length(web_craft_entries)) + var/entry = web_craft_entries[i] + if(ispath(entry, /datum/web_craft_entry)) + web_craft_entries[i] = new entry + return web_craft_entries + +// Creates and shows the options in the radial menu. +/datum/action/cooldown/power/aberrant/web_crafter/proc/build_radial_options(list/entries, list/key_to_entry) + var/list/options = list() + for(var/datum/web_craft_entry/entry as anything in entries) + if(!istype(entry)) + continue + var/datum/radial_menu_choice/choice = entry.get_radial_choice() + if(!choice) + continue + var/key = entry.display_name + if(!key) + key = "[entry.type]" + var/original_key = key + var/dupe_index = 2 + while(options[key]) + key = "[original_key] ([dupe_index])" + dupe_index++ + options[key] = choice + key_to_entry[key] = entry + return options + +// Check before crafting. +/datum/action/cooldown/power/aberrant/web_crafter/proc/can_craft_entry(mob/living/user, datum/web_craft_entry/entry) + // Are we hungy? + if(!HAS_TRAIT(user, TRAIT_NOHUNGER) && user.nutrition <= NUTRITION_LEVEL_STARVING) + user.balloon_alert(user, "too hungry!") + return FALSE + // Are we silenced. Yes, shooting strings from your body is resonant; you go ahead and explain how spiderman does it with your fancy psuedo-science.. + if(HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED)) + user.balloon_alert(user, "silenced!") + return FALSE + // We don't have the entry? + if(!entry) + return FALSE + // Special requirements for structure placement. + if(entry.is_structure) + if(!isturf(user.loc)) + user.balloon_alert(user, "invalid location!") + return FALSE + var/turf/target_turf = get_turf(user) + if(!entry.can_place(user, target_turf)) + return FALSE + return TRUE + +// Atually creates the item. +/datum/action/cooldown/power/aberrant/web_crafter/proc/create_obj(mob/living/user, datum/web_craft_entry/entry) + if(entry.is_structure) + var/turf/target_turf = get_turf(user) + if(!target_turf) + return FALSE + entry.spawn_entry(user, target_turf) + return TRUE + + var/obj/item/new_item = entry.spawn_entry(user, null) + user.put_in_hands(new_item) + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter_entries.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter_entries.dm new file mode 100644 index 00000000000000..93344edfaa611c --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter_entries.dm @@ -0,0 +1,29 @@ +// Base two web crafting items that come with web_crafter +/datum/web_craft_entry/cloth + desc = "Cloth made from your silk! Practically indistinguishable, but you might make people awkward if they start wearing clothes made from it." + spawn_type = /obj/item/stack/sheet/cloth + hunger_cost = 7 + +/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 + icon = 'icons/effects/web.dmi' + icon_state = "webpassage" + +// Snare Crafter +/datum/web_craft_entry/web_bola + spawn_type = /obj/item/restraints/legcuffs/bola/web + hunger_cost = 10 + +/datum/web_craft_entry/web_restraints + spawn_type = /obj/item/restraints/handcuffs/cable/zipties/web + hunger_cost = 10 + +// Tripwire Webs +/datum/web_craft_entry/tripwire_web + spawn_type = /obj/structure/spider/tripwire_web + hunger_cost = 5 + +/datum/web_craft_entry/tripwire_web/spawn_structure(mob/living/user, turf/target_turf) + return new /obj/structure/spider/tripwire_web(target_turf, user) diff --git a/modular_doppler/modular_powers/icons/items/restraints.dmi b/modular_doppler/modular_powers/icons/items/restraints.dmi new file mode 100644 index 0000000000000000000000000000000000000000..ad7599ebb74d36ea8ded8a923805807ff43479ac GIT binary patch literal 5023 zcmX|Fc|4R~)E`Wiib2`S5F(6Sp~jFUYa~fC_AN`6h%gKxvPH60_Oiv;*DNz3Vf>6O zTQUkEB4aEwjhT1!e%|+w=ehSj_j~U>=X=h*=iDdJ^5zZBlfowf005`4k-in9#2!EF ztc=VciSc)F!+FUJ78yTS=JOHJ2H&FU3W)m zOm*>xJl^cmARsU$wM|3SN_9fjL@e(|Mqh@t*zHS~VA9InMv2L)QjHaY?~;>M(K5^Y-%=9njFTo~ZD$iO{{}eb2*Nv&T$NQkAnklh5wR z4ohPNJh^9(HF$x*0RZs!8SCp>hvxmx@2Ig?Kl2$+2J)5|XIhHsKL?hW2@p?v>Ghtm z+*O(ki%~0H5n`{jDiPy}vy$ZX;^zHC9*Hug_x9ZraO^MGX^0x1T3Bt^m&X$iRYcS9 zg|H~p~Z0!QtVZ>$PE*^%Vk!eSNS7jfoCA$1_Ops@ZCSqumjfqtYZSpdS#y~iZ zB#g%2BBccm6=KLg-&7Dw?H(6s0ef2b`ttBrdGQgKWOK_*#c(hUtl zB9m6aPQTjedwOCrbEbFO<05he!MAzP;qT{&KEE8fpaGZ)GqpJzykOvHw9aMeLnnV; z`C66;LLir0Ez9C5Fbowz{+zP7hvbMkaW1xI+%J4eJvmL1eSqxCKy5#$K zS5i{wo|azP#2kBt+`!X_Um=`SkZjZ@^rB*17W>z?ZgwIz-Fe$h*@a0hw2uo%KL;kq zKfZo$x`p->H#@VgvW%=ga5h!8Y>5cV+J4&g*FZ?~ToPH^d|FoavaE(|_+jgOe~~9U zX(h)o+YZHPA0H>mUbhm7C2Y;N2eo{WC5gcVAV=jf4JAF$ND7I#g3GCbgs;J+9-)7# zu~#gf5)!$K$MohAr6`4|-T=)wPt!Rr$HdfDL%Ck$@(YjPrqIYq5X?1sZcLQJPCIWzWm*U&&h`Pq4 zoW?KFYyfmMkk9gr54qBF19P?s%Nyzhkj2=sF$L6S*CZ6JI_p3-*yam-y_`yrfTy; z6DY5#LhsC{H0ZVti0E2bN9r`k|*^mK00zSkGy8dkWgq~5S%`M*!05z}XC1Q%&N>TNcx7n`HAEH6m6 zLI?fPQQ;5PS|kre)3OQ+%6M%!qYHSNTp~Wnyqs1pxVo=p{?27_VL8^k>)ndUwOoj?L+@ks~AvP`ZUqX%IOPde|1St>r5?BLxK^5lhzwTPGQ8zNlI% z=->K&QkC=?fw?TuQ^DRIBM?iCs_H{j$5n7Q5S!=P%fvCIv@lGC?RO8P#k$Qq1Yg~Y zfp1NX-hfc22K98gFhoFaNSpF3^G3dp6cIuKFVwdB)<^~~{pEiHjgDX!3n9ODbr=m? ztB>1bk$>S|%s47OiHWcXKi)EY`{ilfBv6m2_2{qFx&wk2Z+4{EYur6AXC^T)`=F_v zcm9Ixly*3Z!o5L6 z;G}>4Ln{a=!@BxDP^XAe|NG%)5BKyf;>4L_49KgLZos0`D$rjNn@ioa5_289?V{E~ zocL^4`*C2UH5&x~6&1`iDhopnc#_xks&W31!ZrfoZQ)ytXL z3{@bG2)aVX($DmaJU$~wrT*4-5gF_nWAWNbdi4?DHF-8j(*#AalRt*urq6yKt)rIW zZHi8$E6RWes3hy|N82kZ;S521)-h{&PCxIrJ`>PwWy29RVtNT)Q0l)mOz8%Q3xv+F zAR=hBVC2Oq>1&%Y)9GP;%MEQd4du6v74uvpd@FGBy>brBYy*69FcWc1k$~r=4A;hr!pXA-8Vp-Nk2#Ct zgdE)$(rF7aQgyKN(9c#-wOAUE$RWT{r(aVnxSrbL@LVSM4+B-3>~z+}VbHZMhP_-z zr=qe2j8tJAZhce|@3AmxrL$y!kLS<(pq+|8>HjwY{x!>h#P;pWa^R=Oo>4}7$?4?P ztKwIp*54^`>^E~1yfAZ?A9XALfBRRNgkBauJaba{mF=Repu$t7Qvp7X&CS$AE_-2s z#YtcuyR+F$;Ku%hct7> z`Ztr3LceXh;iOwGtULJB0|+esHRT2icHma3Mg<(J)EZvl&5E7qT=71iX+z;2jj1nP z5oZr=hNm=SgH0hvEKQ_g7NEo-BYm(RVX@p9yIR}|SB;U?6I;K~cbE%#XJaa42q8ZH zfl(Q)reqh}&Z;OFQUhrpKfk)NmRm3)@$Sq8}ZU6N`Jc(RVss z0a8nIrA=QJJ`XMAQt;B%R5yQAAQYIPpxXU8{8S57P zvK8S{1|auURBhR5@y3?JW9sUQ@`RHRa@Q|RKQ*Do^z`gL|Fk7)ST|4(7J8q(>8JXl zuKT~3_P9Lf?lQZgu1yNsWc(#{fkglIm{6JRXoC_B;e7m45kqazn9U{zxjTpevJ`iF zMqQ)NP)3K0{s824ajm5GyX3BQM;jw^3vM~sQZ?3!7KL6|Jt*8beha~HsK9T^Jm%jw=pHx8w?blJTyeqn3gC5KX4^D1it#Du>D!ox$?=LcyROb zK?%K?V-*CW22^?hL(}Y?6 zz_wxNTA<&kZF1~;C@!tIjOHqrdqrYq!+i2_+~AI~J#t>DXV88~uZT^uH2J<%oQg@; znSYTK7R{wJlICmg49o@FR$n!ec=pPo;xlvcN^ft@z@Yuf6z|U9jj6pjRaRidcVRoZ z-Or?pFgEiemNs!7+iaC4Ed7VDVR8!)3Q9>EE1IvZYY9S|sL9B4^8t$X?gje(*2OH)7_+Ji|}@h zxb0auDZSt<*5{yR_aY|K>WJlDB9B#PbERY_7abSJWP^+s;yhUt+iZUI%RW$*`xM`G zS(rQ37?&lT@>z_IaeM9x!2mgraH`B!JrzRXzFl|JG=;6-WO(Y!IEFZ?XnSmHX#>(H z_K@kdx`{ORmBf3QCu{X^m@mW@7mJX+YHq0lz&#G#2R=R7r!vnKI6)j^d5=Cf6}_Im zw|H6ECO4eB?-5~h-@O#K6N$hTHhn}hMbB>`eD)^bw0GJyoK7m0=F^ot(l4#bn9Slm zj$_zyr3}ZSVx-vpGtu#bIzZ^xoR9aGU|_0U)x)}M$2STIIx4Uun+l)pWzp+AFZtTp z$dHN!mPv08Hxxyf>FyN7c1!%Mvo{Y-q5MQ^cG|%lrFQPP+k3VAAdqUo=v1L$B!z0S zsM(a-7Ty$2j+7X;B=6rzL*}4oM^luWv&6x_)0r;<6;lS&Qv2oGM`Ec{l-hBY0o&Xz z>ZX(!4B8Oy)f?c^&Q`P%&mWT%i^R&Q3#+bbDH0LC9KQjqb{lg_@(T-k<+OnjhW9Kl zW}4rz1*UUKd^X&j@%e*kW5oUk5KJ@ePJ)i6*CmsRS23>Z-Cd#_mtRrjc?{FYUH>ZeCT4)3j zW@Fe^!L*@-rWc2G%%}7?kX_j&#|s99^^;~dzs5!smC`{|wDvoweLL^vatmD#ZP%yl z?N~oYAR5)8Y&Czz5nhVbz>_3W*O<~|_GSY_57DZnupW5I%SI`C6uh(ZhAGLC` z_7v#Iy$1!Q&V;cdV|ekCR%bV=slPodqxNizCaIgqrV$J9hnO89Laa(y6?JMMN0^A% zzzoIbW>Pm1wkze1fhUl|TdWeFH#`~k!OT5Z(*ILTT=C#=z*&GBB@)T&#n_pAGs!D* zfj-rs;tDB-WvaH=T|IsL*a7gEJOfD~i-MQ0gbtFB`*vgq0|8-Vg;m=eBarA|RCpSn zqe~>afiD?N+SbzpAI7pa)imb>1b5Ppz`_jV_V7$K*cV_z$#w2lZ@^3_y~? zwe_uJ`gnGG4Jv>RIB3qCh&{Rh2#9(=p{iwt<14|>6O>d`yuALvkRDV+zOFu)am z)dY3uurfVP_gvLG>zf3-e%y=|rhtD(Jo^!_w!oN7#<2uV9~((n^V3TSMxlJj5W)2b z_Er&>YJrmKl>@`tRlT-Nrs~~Y1a3P^6rUiY5`E0YMc4_@LTmcMNP>(o^Z9@J*E3Y9 zGqhOMa}&YdBK9I*f|LIV9;mH9#l2=k{tg`?^fEZIWpH$%gH6K(6W4adY z@9gOTQWARY1$bF}NtiixD_*3FdPA&(?N5fzRMHqO>THtB6E$Mv!NA%IjMt!EN0>b8 zvjl}5^<`I!>Zrny=HUYtuzD#Zms5TW60~K+>{;p~_+*u6@k3^hIX|$`&d$#BTiVGP z&``cr^X>#vo-SGNBWrP-x;dwW1hlL;->^czv+hHXrM=F{p1oj}f9vX{FYltbTmcC3 zI(p0N2RMf{ea`zJuMp*8dQ09i_4LO}HKt}E5IIb6{Iap;EHs*N#0)+ouS>VMi7@K> z>0`tRiw6%M>yI-M_|MgYLm%&Q}`{s!^um{ zb9<$5lX3S#6C0=dk;fz7Kz001g=cCo5_cGLN4<^|#B(HgL6BjI`(=iCr~gxn9vn4e f$vyoaovDDP`4@ihi}61T05CSVsb6#5Ddv9wQJ4V! literal 0 HcmV?d00001 diff --git a/tgstation.dme b/tgstation.dme index b5df12578c075e..4d7a6c8f401a5b 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7473,6 +7473,11 @@ #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\vent_crawl.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\snare_webs.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\tripwire_webs.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\web_crafter.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\web_crafter_entries.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_dantian.dm" From 1c93d331a8a61254f260673c4f2124932537c175 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 28 Feb 2026 12:04:12 +0100 Subject: [PATCH 106/212] Forgot to add a cost to bloodhound --- .../modular_powers/code/powers/resonant/aberrant/bloodhound.dm | 3 +++ 1 file changed, 3 insertions(+) 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 97becf35153306..980e38df61b61a 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm @@ -16,6 +16,8 @@ click_to_activate = TRUE target_range = 1 + // How much hunger does tracking someone take? + var/hunger_cost = 20 // How long you can keep a mob's scent. var/scent_duration = 2 MINUTES @@ -42,6 +44,7 @@ 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. From 653f51ede696056212371822267cbfb8fe412e8e Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 28 Feb 2026 21:58:53 +0100 Subject: [PATCH 107/212] Adds cocoon. Store things, store people! --- .../code/powers/resonant/aberrant/cocoon.dm | 306 ++++++++++++++++++ tgstation.dme | 1 + 2 files changed, 307 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm new file mode 100644 index 00000000000000..4bf5cd8bd1b155 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm @@ -0,0 +1,306 @@ +// Put things into a bundle of webs. Mostly on-demand storage and sorting; or dealing with people you don't want to escape. +/datum/power/aberrant/cocoon + name = "Cocoon" + desc = "Allows you to cocoon objects and people after a delay. You can destroy cocoons by interacting with them.\ + \n Targeting a space without a creature bundles all items on that space up in a container; this has the size and storage capacity of about two backpacks, and can only be opened by destroying it.\ + \n Targeting a prone creature that you have aggressively grabbed bundles them up. The creature is buckled inside the cocoon and can't interact with the world or escape without struggling. \ + Creature cocoons can be dragged around with less slow down commpared to normal." + value = 3 + + required_powers = list(/datum/power/aberrant/web_crafter) + action_path = /datum/action/cooldown/power/aberrant/cocoon + +/datum/action/cooldown/power/aberrant/cocoon + name = "Cocoon" + desc = "Wraps up a person or object for convenient storage. Object cocoons can be carried around and allow you to carry greater amount of items with relative ease. People cocoons can be used to keep people from escaping." + button_icon = 'icons/effects/web.dmi' + button_icon_state = "cocoon_large1" + cooldown_time = 2 + + target_range = 1 + target_self = FALSE // why would you + click_to_activate = TRUE + use_time = 5 SECONDS + +/datum/action/cooldown/power/aberrant/cocoon/InterceptClickOn(mob/living/clicker, params, atom/target) + ..() + // Always consume the click to avoid normal click interactions. + return TRUE + +/datum/action/cooldown/power/aberrant/cocoon/use_action(mob/living/user, atom/target) + var/atom/resolved_target = resolve_cocoon_target(target) + // Living targets get wrapped. + if(isliving(resolved_target)) + if(!can_cocoon_mob(user, resolved_target)) + return FALSE + return cocoon_mob(user, resolved_target) + // Because misclicking will also start a progress bar, it can kinda suck to realize you're cocooning the wrong thing. A soft auto-aim where if therés a mob on the turf, it always has presedence. + var/turf/target_turf = get_turf(target) + if(target_turf) + for(var/mob/living/occupant in target_turf) + return cocoon_mob(user, occupant) + // Cocoon objects in the space if we don't have other targets. + return cocoon_object(user, target) + +// Both cast time and visual effects are resolved into that. +/datum/action/cooldown/power/aberrant/cocoon/do_use_time(mob/living/user, atom/target) + // Woooow I worked hard on this and you just var-edit it away BAKA. + if(use_time <= 0) + return TRUE + var/atom/use_target = resolve_cocoon_target(target) + if(!use_target) + return FALSE + var/turf/target_turf = get_turf(use_target) + if(!target_turf) + return do_after(user, use_time, target = use_target, timed_action_flags = use_time_flags) + playsound(user, 'sound/items/handling/surgery/organ1.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + // Applies a visual effect similar to chiseling away at stone + var/obj/effect/temp_visual/cocoon_progress/progress_visual = new /obj/effect/temp_visual/cocoon_progress(target_turf) + progress_visual.apply_cocoon_appearance(use_target) + progress_visual.pixel_x = use_target.pixel_x + progress_visual.pixel_y = use_target.pixel_y + // If we having a living target, we assing it here. + var/mob/living/target_mob + if(isliving(use_target)) + target_mob = use_target + // Align the sprite with the animation + target_mob.set_lying_angle(LYING_ANGLE_EAST) + progress_visual.setDir(EAST) + else + progress_visual.setDir(use_target.dir) + progress_visual.set_completion(0) + + // spin! + if(target_mob) + target_mob.spin(spintime = use_time, speed = 2) + + // Do_after loop and a progress bar for the user. + var/datum/progressbar/total_progress_bar = new(user, use_time, use_target) + var/use_time_period = max(1, round(use_time / ICON_SIZE_Y)) + var/remaining_time = use_time + var/interrupted = FALSE + if(target_mob) + target_mob.remove_filter("cocoon_hide") // removes existing filter if its there. + while(remaining_time > 0 && !interrupted) + if(target_mob && !can_cocoon_mob(user, use_target)) + interrupted = TRUE + break + // We update the progress bar as well as the visual effects for the cocoon. + if(do_after(user, use_time_period, target = use_target, timed_action_flags = use_time_flags, progress = FALSE)) + remaining_time -= use_time_period + total_progress_bar.update(use_time - remaining_time) + var/progress = (use_time - remaining_time) / use_time + progress_visual.set_completion(progress) // this line's responsible for the cocoon effect + // this filter keeps pace with the cocoon and hides the mob so it doesn't have 'bits' poking out. + if(target_mob) + var/mask_offset = min(ICON_SIZE_Y, round(progress * ICON_SIZE_Y)) + target_mob.add_filter("cocoon_hide", 1, alpha_mask_filter(icon = icon('icons/effects/alphacolors.dmi', "white"), y = mask_offset)) + else + interrupted = TRUE + total_progress_bar.end_progress() + + if(!QDELETED(progress_visual)) + qdel(progress_visual) + if(target_mob) + target_mob.remove_filter("cocoon_hide") + return !interrupted + +// Because misclicking will also start a progress bar, it can kinda suck to realize you're cocooning the wrong thing. A soft auto-aim where if there's a mob on the turf, it always has presedence. +/datum/action/cooldown/power/aberrant/cocoon/proc/resolve_cocoon_target(atom/target) + if(isliving(target)) + return target + var/turf/target_turf = get_turf(target) + if(target_turf) + for(var/mob/living/occupant in target_turf) + return occupant + return target + +/datum/action/cooldown/power/aberrant/cocoon/proc/cocoon_mob(mob/living/user, mob/living/target) + if(!target || QDELETED(target)) + return FALSE + var/turf/target_turf = get_turf(target) + if(!target_turf) + return FALSE + + var/obj/structure/closet/body_bag/cocoon/new_cocoon = new /obj/structure/closet/body_bag/cocoon(target_turf) + if(!new_cocoon) + return FALSE + if(!new_cocoon.insert(target)) + qdel(new_cocoon) + return FALSE + return TRUE + +/datum/action/cooldown/power/aberrant/cocoon/proc/can_cocoon_mob(mob/living/user, mob/living/target) + if(!user || !target) + user.balloon_alert(user, "No target!") + return FALSE + if(QDELETED(user) || QDELETED(target)) + user.balloon_alert(user, "No target!") + return FALSE + if(user.pulling != target || user.grab_state < GRAB_AGGRESSIVE) + user.balloon_alert(user, "You must aggressively grab the target!") + return FALSE + if(target.body_position != LYING_DOWN) + user.balloon_alert(user, "Target must be prone!") + return FALSE + return TRUE + +// We get the space we're on and bundle up all the items on the space; as much as possible. +/datum/action/cooldown/power/aberrant/cocoon/proc/cocoon_object(mob/living/user, atom/target) + var/turf/target_turf = get_turf(target) + if(!target_turf) + return FALSE + + var/obj/item/storage/cocoon_item/new_cocoon = new /obj/item/storage/cocoon_item(target_turf) + if(!new_cocoon?.atom_storage) + qdel(new_cocoon) + return FALSE + + // Stuffs everything inside of the container + var/inserted_any = FALSE + var/previous_lock_state = new_cocoon.atom_storage.locked + new_cocoon.atom_storage.set_locked(STORAGE_NOT_LOCKED) + for(var/obj/item/thing in target_turf) + if(thing == new_cocoon || thing.anchored) + continue + if(new_cocoon.atom_storage.attempt_insert(thing, null, messages = FALSE)) + inserted_any = TRUE + new_cocoon.atom_storage.set_locked(previous_lock_state) + + // can't make empty ones + if(!inserted_any) + user.balloon_alert(user, "Nothing to wrap!") + qdel(new_cocoon) + return FALSE + return TRUE + +// Cocoon for items +/obj/item/storage/cocoon_item + name = "cocoon" + desc = "A tight bundle of webbing packed with stored goods. You will have to tear it open to get anything out." + icon = 'icons/effects/web.dmi' + icon_state = "cocoon1" + w_class = WEIGHT_CLASS_BULKY + var/unwrap_delay = 4 SECONDS + +/obj/item/storage/cocoon_item/Initialize(mapload) + . = ..() + if(atom_storage) + atom_storage.max_slots = 30 + atom_storage.max_total_storage = 35 + atom_storage.attack_hand_interact = FALSE + atom_storage.click_alt_open = FALSE + atom_storage.display_contents = FALSE + atom_storage.insert_on_attack = FALSE + atom_storage.set_locked(STORAGE_FULLY_LOCKED) + +/obj/item/storage/cocoon_item/attack_self(mob/user, modifiers) + return attempt_unwrap(user) + +/obj/item/storage/cocoon_item/proc/attempt_unwrap(mob/living/user) + if(!user) + return FALSE + to_chat(user, span_notice("You start tearing open [src]...")) + if(!do_after(user, unwrap_delay, target = src)) + return FALSE + if(QDELETED(src)) + return FALSE + var/turf/drop_turf = get_turf(src) + if(atom_storage && drop_turf) + atom_storage.remove_all(drop_turf) + visible_message(span_notice("[src] is torn open, spilling its contents!")) + qdel(src) + return TRUE + +// Cocoon for people +/obj/structure/closet/body_bag/cocoon + name = "cocoon" + desc = "A person-sized cocoon; rows upon rows of silk keeping something quite secure." + icon = 'icons/effects/web.dmi' + icon_state = "cocoon_large1" + max_integrity = 40 + material_drop = null + material_drop_amount = 0 + obj_flags = CAN_BE_HIT + breakout_time = 2 MINUTES + mob_storage_capacity = 1 + drag_slowdown = 0.5 + var/open_time = 5 SECONDS + +/obj/structure/closet/body_bag/cocoon/can_open(mob/living/user, force = FALSE) + return FALSE + +/obj/structure/closet/body_bag/cocoon/can_close(mob/living/user) + return FALSE + +/obj/structure/closet/body_bag/cocoon/toggle(mob/living/user) + return FALSE + +/obj/structure/closet/body_bag/cocoon/attack_hand(mob/living/user, list/modifiers) + tear_open(user) + +/obj/structure/closet/body_bag/cocoon/attack_hand_secondary(mob/user, list/modifiers) + return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN + +/obj/structure/closet/body_bag/cocoon/attempt_fold(mob/living/carbon/human/the_folder) + return FALSE + +/obj/structure/closet/body_bag/cocoon/container_resist_act(mob/living/user, loc_required = TRUE) + var/breakout_time = 2 MINUTES + user.changeNext_move(CLICK_CD_BREAKOUT) + user.last_special = world.time + CLICK_CD_BREAKOUT + to_chat(user, span_notice("You struggle against the webs... (This will take about [DisplayTimeText(breakout_time)].)")) + visible_message(span_notice("You see something struggling and writhing in \the [src]!")) + if(do_after(user, breakout_time, target = src)) + if(user.stat != CONSCIOUS || user.loc != src) + return + qdel(src) + +/obj/structure/closet/body_bag/cocoon/proc/tear_open(mob/living/user) + if(open_time > 0) + to_chat(user, span_notice("You start tearing open [src]...")) + if(!do_after(user, open_time, target = src)) + return + if(QDELETED(src)) + return + var/turf/drop_turf = get_turf(src) + if(drop_turf) + for(var/atom/movable/thing as anything in contents) + thing.forceMove(drop_turf) + visible_message(span_notice("[src] is torn open, spilling its contents!")) + qdel(src) + +// Cocoon progress visual for use_time. +/obj/effect/temp_visual/cocoon_progress + icon = 'icons/effects/web.dmi' + icon_state = "cocoon_large1" + randomdir = FALSE + layer = ABOVE_MOB_LAYER + appearance_flags = KEEP_TOGETHER | KEEP_APART + duration = 1 MINUTES + var/completion = 0 + +// Takes the icon and state from the associated cocoon +/obj/effect/temp_visual/cocoon_progress/proc/apply_cocoon_appearance(atom/target) + if(isliving(target)) + var/obj/structure/closet/body_bag/cocoon/reference = /obj/structure/closet/body_bag/cocoon + icon = initial(reference.icon) + icon_state = initial(reference.icon_state) + else + var/obj/item/storage/cocoon_item/reference = /obj/item/storage/cocoon_item + icon = initial(reference.icon) + icon_state = initial(reference.icon_state) + +/obj/effect/temp_visual/cocoon_progress/proc/set_completion(value) + completion = clamp(value, 0, 1) + var/static/icon/white = icon('icons/effects/alphacolors.dmi', "white") + switch(completion) + if(0) + alpha = 0 + remove_filter("partial_uncover") + filters = null + else + alpha = 255 + var/mask_offset = min(ICON_SIZE_X, round(completion * ICON_SIZE_X)) + remove_filter("partial_uncover") + add_filter("partial_uncover", 1, alpha_mask_filter(icon = white, x = mask_offset, flags = MASK_INVERSE)) diff --git a/tgstation.dme b/tgstation.dme index 4d7a6c8f401a5b..ebf72e63edcf5c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7468,6 +7468,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root_beastial.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bioluminescence.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bloodhound.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\cocoon.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\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" From c1bfd00ec4effb0294362a58019c41e0af9769a0 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 1 Mar 2026 10:37:52 +0100 Subject: [PATCH 108/212] Fixed some bugs with web crafting. Added snare webs. --- .../code/powers/resonant/aberrant/cocoon.dm | 80 +++++++----- .../aberrant/web_crafter/_web_craft_datum.dm | 2 + .../aberrant/web_crafter/binding_webs.dm | 73 +++++++++++ .../aberrant/web_crafter/snare_webs.dm | 122 ++++++++++++------ .../aberrant/web_crafter/web_crafter.dm | 2 +- .../web_crafter/web_crafter_entries.dm | 11 +- tgstation.dme | 1 + 7 files changed, 216 insertions(+), 75 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/binding_webs.dm 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 4bf5cd8bd1b155..7380fa5468dcfa 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm @@ -4,7 +4,8 @@ desc = "Allows you to cocoon objects and people after a delay. You can destroy cocoons by interacting with them.\ \n Targeting a space without a creature bundles all items on that space up in a container; this has the size and storage capacity of about two backpacks, and can only be opened by destroying it.\ \n Targeting a prone creature that you have aggressively grabbed bundles them up. The creature is buckled inside the cocoon and can't interact with the world or escape without struggling. \ - Creature cocoons can be dragged around with less slow down commpared to normal." + Creature cocoons can be dragged around with less slow down commpared to normal.\ + \n Costs hunger to use, and cannot be used while starving." value = 3 required_powers = list(/datum/power/aberrant/web_crafter) @@ -21,53 +22,70 @@ target_self = FALSE // why would you click_to_activate = TRUE use_time = 5 SECONDS + // Used to determine the cost + var/last_cocoon_was_mob = FALSE /datum/action/cooldown/power/aberrant/cocoon/InterceptClickOn(mob/living/clicker, params, atom/target) ..() // Always consume the click to avoid normal click interactions. return TRUE +// Block use while starving. +/datum/action/cooldown/power/aberrant/cocoon/can_use(mob/living/user, atom/target) + . = ..() + if(!.) + return FALSE + if(user.nutrition <= NUTRITION_LEVEL_STARVING) + owner.balloon_alert(user, "too hungry!") + return FALSE + return TRUE + /datum/action/cooldown/power/aberrant/cocoon/use_action(mob/living/user, atom/target) - var/atom/resolved_target = resolve_cocoon_target(target) // Living targets get wrapped. - if(isliving(resolved_target)) - if(!can_cocoon_mob(user, resolved_target)) + if(isliving(target)) + if(!can_cocoon_mob(user, target)) return FALSE - return cocoon_mob(user, resolved_target) - // Because misclicking will also start a progress bar, it can kinda suck to realize you're cocooning the wrong thing. A soft auto-aim where if therés a mob on the turf, it always has presedence. - var/turf/target_turf = get_turf(target) - if(target_turf) - for(var/mob/living/occupant in target_turf) - return cocoon_mob(user, occupant) + if(cocoon_mob(user, target)) + last_cocoon_was_mob = TRUE + return TRUE + return FALSE // Cocoon objects in the space if we don't have other targets. - return cocoon_object(user, target) + if(cocoon_object(user, target)) + last_cocoon_was_mob = FALSE + return TRUE + return FALSE -// Both cast time and visual effects are resolved into that. +/datum/action/cooldown/power/aberrant/cocoon/on_action_success(mob/living/user, atom/target) + if(!user) + return + user.adjust_nutrition(last_cocoon_was_mob ? -40 : -15) + return + +// Both cast time and visual effects are resolved in this. /datum/action/cooldown/power/aberrant/cocoon/do_use_time(mob/living/user, atom/target) // Woooow I worked hard on this and you just var-edit it away BAKA. if(use_time <= 0) return TRUE - var/atom/use_target = resolve_cocoon_target(target) - if(!use_target) + if(!target) return FALSE - var/turf/target_turf = get_turf(use_target) + var/turf/target_turf = get_turf(target) if(!target_turf) - return do_after(user, use_time, target = use_target, timed_action_flags = use_time_flags) + return do_after(user, use_time, target = target, timed_action_flags = use_time_flags) playsound(user, 'sound/items/handling/surgery/organ1.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) // Applies a visual effect similar to chiseling away at stone var/obj/effect/temp_visual/cocoon_progress/progress_visual = new /obj/effect/temp_visual/cocoon_progress(target_turf) - progress_visual.apply_cocoon_appearance(use_target) - progress_visual.pixel_x = use_target.pixel_x - progress_visual.pixel_y = use_target.pixel_y + progress_visual.apply_cocoon_appearance(target) + progress_visual.pixel_x = target.pixel_x + progress_visual.pixel_y = target.pixel_y // If we having a living target, we assing it here. var/mob/living/target_mob - if(isliving(use_target)) - target_mob = use_target + if(isliving(target)) + target_mob = target // Align the sprite with the animation target_mob.set_lying_angle(LYING_ANGLE_EAST) progress_visual.setDir(EAST) else - progress_visual.setDir(use_target.dir) + progress_visual.setDir(target.dir) progress_visual.set_completion(0) // spin! @@ -75,18 +93,18 @@ target_mob.spin(spintime = use_time, speed = 2) // Do_after loop and a progress bar for the user. - var/datum/progressbar/total_progress_bar = new(user, use_time, use_target) + var/datum/progressbar/total_progress_bar = new(user, use_time, target) var/use_time_period = max(1, round(use_time / ICON_SIZE_Y)) var/remaining_time = use_time var/interrupted = FALSE if(target_mob) target_mob.remove_filter("cocoon_hide") // removes existing filter if its there. while(remaining_time > 0 && !interrupted) - if(target_mob && !can_cocoon_mob(user, use_target)) + if(target_mob && !can_cocoon_mob(user, target)) interrupted = TRUE break // We update the progress bar as well as the visual effects for the cocoon. - if(do_after(user, use_time_period, target = use_target, timed_action_flags = use_time_flags, progress = FALSE)) + if(do_after(user, use_time_period, target = target, timed_action_flags = use_time_flags, progress = FALSE)) remaining_time -= use_time_period total_progress_bar.update(use_time - remaining_time) var/progress = (use_time - remaining_time) / use_time @@ -105,16 +123,6 @@ target_mob.remove_filter("cocoon_hide") return !interrupted -// Because misclicking will also start a progress bar, it can kinda suck to realize you're cocooning the wrong thing. A soft auto-aim where if there's a mob on the turf, it always has presedence. -/datum/action/cooldown/power/aberrant/cocoon/proc/resolve_cocoon_target(atom/target) - if(isliving(target)) - return target - var/turf/target_turf = get_turf(target) - if(target_turf) - for(var/mob/living/occupant in target_turf) - return occupant - return target - /datum/action/cooldown/power/aberrant/cocoon/proc/cocoon_mob(mob/living/user, mob/living/target) if(!target || QDELETED(target)) return FALSE @@ -134,6 +142,8 @@ 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/web_crafter/_web_craft_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_craft_datum.dm index b85048af3c52d1..7c19171acf2c18 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 @@ -34,6 +34,8 @@ var/datum/radial_menu_choice/choice = new() choice.name = display_name choice.image = image(icon = icon, icon_state = icon_state) + if(desc) + choice.info = "[desc]" return choice /datum/web_craft_entry/proc/can_place(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 new file mode 100644 index 00000000000000..97880bfb712563 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/binding_webs.dm @@ -0,0 +1,73 @@ +// Creates snaaaares +/datum/power/aberrant/binding_webs + name = "Binding Webs" + desc = " Allows you to craft web restraints and web bolas using web crafter. Web restraints are functionally similar to zipties. Web Bolas can be thrown just like regular bolas." + value = 3 + + required_powers = list(/datum/action/cooldown/power/aberrant/web_crafter) + +/datum/power/aberrant/binding_webs/post_add(client/client_source) + . = ..() + var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action() + if(!action) + return + action.web_craft_entries |= /datum/web_craft_entry/web_restraints + action.web_craft_entries |= /datum/web_craft_entry/web_bola + +/datum/power/aberrant/binding_webs/remove() + var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action() + if(!action) + return + action.web_craft_entries -= /datum/web_craft_entry/web_restraints + action.web_craft_entries -= /datum/web_craft_entry/web_bola + +/datum/power/aberrant/binding_webs/proc/get_web_crafter_action() + if(!power_holder) + return null + for(var/datum/action/cooldown/power/aberrant/web_crafter/action in power_holder.actions) + return action + return null + +// Reflavored +/obj/item/restraints/handcuffs/cable/zipties/web + name = "web ties" + desc = "Sticky strings meant for binding pesky hands. Be careful not to get yourself stuck!" + breakouttime = 60 SECONDS // sticky = better + trashtype = null + /// Tracks if this was actually used as cuffs so we can delete on uncuff only. + var/was_cuffed = FALSE + +// If you're not a web weaver yourself, you might get yourself stuck using it instead. Or if you're clumsy, it will DEFINETLY happy. +/obj/item/restraints/handcuffs/cable/zipties/web/attempt_to_cuff(mob/living/carbon/victim, mob/living/user) + if(iscarbon(user) && !HAS_TRAIT(user, TRAIT_WEB_SURFER) && (HAS_TRAIT(user, TRAIT_CLUMSY) || prob(50))) + to_chat(user, span_warning("Your hands get stuck in the webs!")) + apply_cuffs(user, user) + return + return ..() + +/obj/item/restraints/handcuffs/cable/zipties/web/equipped(mob/living/user, slot) + . = ..() + if(slot == ITEM_SLOT_HANDCUFFED) + was_cuffed = TRUE + RegisterSignal(src, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(on_uncuffed)) + +// why do we not have an uncuff proc on cuffs hello?!?!?! +/obj/item/restraints/handcuffs/cable/zipties/web/proc/on_uncuffed(datum/source, force, atom/newloc, no_move, invdrop, silent) + SIGNAL_HANDLER + if(was_cuffed) + qdel(src) + +// Just normal bolas but extra webby and the same caveat as handcuffs. +/obj/item/restraints/legcuffs/bola/web + name = "web bola" + desc = "A bola made out of a sticky material. Throwing this will definetly get at least one involved party stuck." + breakouttime = 6 SECONDS // sticky = better + icon = 'modular_doppler/modular_powers/icons/items/restraints.dmi' + +// Just like webcuffs, chance of ensnaring yourself instead +/obj/item/restraints/legcuffs/bola/web/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, gentle = FALSE, quickstart = TRUE, throw_type_path = /datum/thrownthing) + if(iscarbon(thrower) && !HAS_TRAIT(thrower, TRAIT_WEB_SURFER) && (HAS_TRAIT(thrower, TRAIT_CLUMSY) || prob(50))) + to_chat(thrower, span_warning("The bola sticks to your hands, whiffing the throw and entangling yourself instead!")) + ensnare(thrower) + return + return ..() 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 16edfb7fb1cbb9..e845da1788c8d4 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 @@ -1,25 +1,25 @@ -// Creates snaaaares +// Snares on the ground! Its like a beartrap but it doesnt hurt and destroys itself after. /datum/power/aberrant/snare_webs name = "Snare Webs" - desc = " Allows you to craft web restraints and web bolas using web crafter. Web restraints are functionally similar to zipties. Web Bolas can be thrown just like regular bolas." + desc = " Allows you to craft snares. These are placed on the ground and are hard to see; but can be disarmed.\ + \n Mobs without the ability to walk through webs will be legcuffed if they walk through it.\ + \n Simple mobs instead receive a slowing status effect for 10 seconds." value = 3 required_powers = list(/datum/action/cooldown/power/aberrant/web_crafter) /datum/power/aberrant/snare_webs/post_add(client/client_source) - . = ..() + ..() var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action() if(!action) return - action.web_craft_entries |= /datum/web_craft_entry/web_restraints - action.web_craft_entries |= /datum/web_craft_entry/web_bola + action.web_craft_entries |= /datum/web_craft_entry/web_snare /datum/power/aberrant/snare_webs/remove() var/datum/action/cooldown/power/aberrant/web_crafter/action = get_web_crafter_action() if(!action) return - action.web_craft_entries -= /datum/web_craft_entry/web_restraints - action.web_craft_entries -= /datum/web_craft_entry/web_bola + action.web_craft_entries -= /datum/web_craft_entry/web_snare /datum/power/aberrant/snare_webs/proc/get_web_crafter_action() if(!power_holder) @@ -28,46 +28,92 @@ return action return null -// Reflavored -/obj/item/restraints/handcuffs/cable/zipties/web - name = "web ties" - desc = "Sticky strings meant for binding pesky hands. Be careful not to get yourself stuck!" - breakouttime = 60 SECONDS // sticky = better - trashtype = null - /// Tracks if this was actually used as cuffs so we can delete on uncuff only. +// Web snare (applied to legs) +/obj/item/restraints/legcuffs/beartrap/web_snare + name = "web snare" + desc = "Sticky silk woven into a snare." + icon = 'icons/effects/web.dmi' + icon_state = "sticky_overlay" + armed = TRUE + trap_damage = 0 + breakouttime = 15 SECONDS + item_flags = DROPDEL + /// Tracks if this was actually used as legcuffs so we can delete on uncuff only. var/was_cuffed = FALSE -// If you're not a web weaver yourself, you might get yourself stuck using it instead. Or if you're clumsy, it will DEFINETLY happy. -/obj/item/restraints/handcuffs/cable/zipties/web/attempt_to_cuff(mob/living/carbon/victim, mob/living/user) - if(iscarbon(user) && !HAS_TRAIT(user, TRAIT_WEB_SURFER) && (HAS_TRAIT(user, TRAIT_CLUMSY) || prob(50))) - to_chat(user, span_warning("Your hands get stuck in the webs!")) - apply_cuffs(user, user) +/obj/item/restraints/legcuffs/beartrap/web_snare/update_icon_state() + . = ..() + icon_state = "sticky_overlay" + return . + +/obj/item/restraints/legcuffs/beartrap/web_snare/attack_self(mob/user) + return + +/obj/item/restraints/legcuffs/beartrap/web_snare/spring_trap(atom/movable/target, ignore_movetypes = FALSE, hit_prone = FALSE) + if(isliving(target) && HAS_TRAIT(target, TRAIT_WEB_SURFER)) return - return ..() + return ..(target, ignore_movetypes, hit_prone) -/obj/item/restraints/handcuffs/cable/zipties/web/equipped(mob/living/user, slot) - . = ..() - if(slot == ITEM_SLOT_HANDCUFFED) +/obj/item/restraints/legcuffs/beartrap/web_snare/equipped(mob/living/user, slot) + ..() + if(slot == ITEM_SLOT_LEGCUFFED) was_cuffed = TRUE RegisterSignal(src, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(on_uncuffed)) -// why do we not have an uncuff proc on cuffs hello?!?!?! -/obj/item/restraints/handcuffs/cable/zipties/web/proc/on_uncuffed(datum/source, force, atom/newloc, no_move, invdrop, silent) +/obj/item/restraints/legcuffs/beartrap/web_snare/proc/on_uncuffed(datum/source, force, atom/newloc, no_move, invdrop, silent) SIGNAL_HANDLER if(was_cuffed) qdel(src) -// Just normal bolas but extra webby and the same caveat as handcuffs. -/obj/item/restraints/legcuffs/bola/web - name = "web bola" - desc = "A bola made out of a sticky material. Throwing this will definetly get at least one involved party stuck." - breakouttime = 6 SECONDS // sticky = better - icon = 'modular_doppler/modular_powers/icons/items/restraints.dmi' - -// Just like webcuffs, chance of ensnaring yourself instead -/obj/item/restraints/legcuffs/bola/web/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, gentle = FALSE, quickstart = TRUE, throw_type_path = /datum/thrownthing) - if(iscarbon(thrower) && !HAS_TRAIT(thrower, TRAIT_WEB_SURFER) && (HAS_TRAIT(thrower, TRAIT_CLUMSY) || prob(50))) - to_chat(thrower, span_warning("The bola sticks to your hands, whiffing the throw and entangling yourself instead!")) - ensnare(thrower) +// Web snare structure (trigger on ground) +/obj/structure/spider/web_snare + name = "web snare" + desc = "A barely visible snare woven from silk." + icon = 'icons/effects/web.dmi' + icon_state = "sticky_overlay" + anchored = TRUE + density = FALSE + alpha = 15 + max_integrity = 10 + +/obj/structure/spider/web_snare/CanAllowThrough(atom/movable/mover, border_dir) + . = ..() + if(!isliving(mover)) + return . + var/mob/living/target = mover + if(HAS_TRAIT(target, TRAIT_WEB_SURFER)) + return . + if(target.mob_size >= MOB_SIZE_HUGE) // the bigger they are the harder they don't fall. + qdel(src) // us humans dont care about tiny webs either. + return . + trigger_snare(target) + return TRUE + +/obj/structure/spider/web_snare/proc/trigger_snare(mob/living/target) + if(!iscarbon(target)) + trigger_snare_noncarbon(target) + return + trigger_snare_carbon(target) + +// Applies the snare legtrap to the target. +/obj/structure/spider/web_snare/proc/trigger_snare_carbon(mob/living/target) + var/mob/living/carbon/carbon_target = target + if(carbon_target.legcuffed || carbon_target.num_legs < 2) // no legs to cuff + qdel(src) return - return ..() + var/obj/item/restraints/legcuffs/beartrap/web_snare/snare = new /obj/item/restraints/legcuffs/beartrap/web_snare + carbon_target.equip_to_slot(snare, ITEM_SLOT_LEGCUFFED) + playsound(src, 'sound/effects/snap.ogg', 50, TRUE) + target.visible_message(span_danger("\The [src] ensnares [target]!"), span_userdanger("\The [src] ensnares you!")) + qdel(src) + +// Non-carbons get a passive slowdown instead for 10sec. +/obj/structure/spider/web_snare/proc/trigger_snare_noncarbon(mob/living/target) + target.add_movespeed_modifier(/datum/movespeed_modifier/web_snare, update = TRUE) + playsound(src, 'sound/effects/snap.ogg', 50, TRUE) + target.visible_message(span_danger("\The [src] ensnares [target]!"), span_userdanger("\The [src] ensnares you!")) + addtimer(CALLBACK(target, TYPE_PROC_REF(/mob, remove_movespeed_modifier), /datum/movespeed_modifier/web_snare), 10 SECONDS) + qdel(src) + +/datum/movespeed_modifier/web_snare + multiplicative_slowdown = 1 diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm index 055e10b2e59cf1..83a8f7ded14558 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 @@ -44,7 +44,7 @@ user.balloon_alert(user, "no web crafts!") return FALSE - var/picked_key = show_radial_menu(user, user, radial_options) + var/picked_key = show_radial_menu(user, user, radial_options, tooltips = TRUE) if(!picked_key) return FALSE 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 93344edfaa611c..6b665af56f5521 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 @@ -11,17 +11,26 @@ icon = 'icons/effects/web.dmi' icon_state = "webpassage" -// Snare Crafter +// Binding Webs /datum/web_craft_entry/web_bola + desc = "Sticky bola. Others can't use it without risking snaring themselves." spawn_type = /obj/item/restraints/legcuffs/bola/web hunger_cost = 10 /datum/web_craft_entry/web_restraints + desc = "Sticky zipties. Destroyed after use; others can't use it without risking binding themselves." spawn_type = /obj/item/restraints/handcuffs/cable/zipties/web hunger_cost = 10 +// Snare Webs +/datum/web_craft_entry/web_snare + desc = "Creates a barely visible web snare that traps the legs of any mob that walk through it." + spawn_type = /obj/structure/spider/web_snare + hunger_cost = 10 + // 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 diff --git a/tgstation.dme b/tgstation.dme index ebf72e63edcf5c..384559df86d8fe 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7475,6 +7475,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\shapechange_wolf.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\vent_crawl.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\binding_webs.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\snare_webs.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\tripwire_webs.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\web_crafter.dm" From 0e60d7a3757035e36bb0c4f588afc129c4495b0a Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 1 Mar 2026 11:38:44 +0100 Subject: [PATCH 109/212] Beastial done. Added darkvision cause it was criminal of me not adding anything there. --- .../powers/resonant/aberrant/darkvision.dm | 23 +++++++++++++++++++ .../aberrant/web_crafter/_web_craft_datum.dm | 9 +++++++- .../aberrant/web_crafter/binding_webs.dm | 13 +++++++++++ .../aberrant/web_crafter/web_crafter.dm | 7 +++++- .../web_crafter/web_crafter_entries.dm | 4 ++++ tgstation.dme | 1 + 6 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm new file mode 100644 index 00000000000000..a9f0d367c64fb0 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm @@ -0,0 +1,23 @@ +// Lets you see in the dark. Duh. +/datum/power/aberrant/darkvision + name = "Darkvision" + desc = "Your eyes see perfectly in the dark; but your vision gains a blue-ish hue to it." + mob_trait = TRAIT_TRUE_NIGHT_VISION + + value = 3 + required_powers = list(/datum/power/aberrant_root/beastial) + var/eye_color_cutoffs_applied = FALSE + +/datum/power/aberrant/darkvision/add() + var/obj/item/organ/eyes/eyes = power_holder.get_organ_slot(ORGAN_SLOT_EYES) + if(eyes && isnull(eyes.color_cutoffs)) // we apply a vision tint but only if our current eyes dont apply it + eyes.color_cutoffs = list(25, 15, 35) + eye_color_cutoffs_applied = TRUE + power_holder.update_sight() + +/datum/power/aberrant/darkvision/remove() + var/obj/item/organ/eyes/eyes = power_holder.get_organ_slot(ORGAN_SLOT_EYES) + if(eyes && eye_color_cutoffs_applied) + eyes.color_cutoffs = null + eye_color_cutoffs_applied = FALSE + power_holder.update_sight() diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_craft_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_craft_datum.dm index 7c19171acf2c18..33742719b5e9c1 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 @@ -4,6 +4,8 @@ var/obj/spawn_type /// Hunger cost to craft var/hunger_cost = 0 + /// Time to craft (do_after). 0 for instant. + var/craft_time = 0 /// Display name for the radial var/display_name /// Description shown in tooltip @@ -34,8 +36,13 @@ var/datum/radial_menu_choice/choice = new() choice.name = display_name choice.image = image(icon = icon, icon_state = icon_state) + var/list/info_bits = list() if(desc) - choice.info = "[desc]" + info_bits += desc + info_bits += "Cost: [hunger_cost] hunger" + if(craft_time > 0) + info_bits += "Time: [craft_time/10]s" + choice.info = jointext(info_bits, "
") return choice /datum/web_craft_entry/proc/can_place(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 97880bfb712563..146dd1fc74db9c 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 @@ -63,6 +63,8 @@ desc = "A bola made out of a sticky material. Throwing this will definetly get at least one involved party stuck." breakouttime = 6 SECONDS // sticky = better icon = 'modular_doppler/modular_powers/icons/items/restraints.dmi' + /// Tracks if this was actually used as legcuffs so we can delete on uncuff only. + var/was_cuffed = FALSE // Just like webcuffs, chance of ensnaring yourself instead /obj/item/restraints/legcuffs/bola/web/throw_at(atom/target, range, speed, mob/thrower, spin=1, diagonals_first = 0, datum/callback/callback, gentle = FALSE, quickstart = TRUE, throw_type_path = /datum/thrownthing) @@ -71,3 +73,14 @@ ensnare(thrower) return return ..() + +/obj/item/restraints/legcuffs/bola/web/equipped(mob/living/user, slot) + . = ..() + if(slot == ITEM_SLOT_LEGCUFFED) + was_cuffed = TRUE + RegisterSignal(src, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(on_uncuffed)) + +/obj/item/restraints/legcuffs/bola/web/proc/on_uncuffed(datum/source, force, atom/newloc, no_move, invdrop, silent) + SIGNAL_HANDLER + if(was_cuffed) + qdel(src) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm index 83a8f7ded14558..13fc56d4772c9e 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 @@ -23,7 +23,7 @@ button_icon = 'icons/effects/web.dmi' button_icon_state = "webpassage" - cooldown_time = 10 + cooldown_time = 5 /// Entries shown in the radial menu. Other powers can append to this. /// Accepts /datum/web_craft_entry instances or typepaths of that datum. @@ -56,6 +56,11 @@ if(!can_craft_entry(user, entry)) return FALSE + // Small craft time so its a tad more defensive. + if(entry.craft_time > 0) + if(!do_after(user, entry.craft_time, target = user)) + return FALSE + if(!create_obj(user, entry)) return FALSE 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 6b665af56f5521..d77c7ccab12073 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 @@ -3,11 +3,13 @@ desc = "Cloth made from your silk! Practically indistinguishable, but you might make people awkward if they start wearing clothes made from it." spawn_type = /obj/item/stack/sheet/cloth hunger_cost = 7 + craft_time = 1 SECONDS /datum/web_craft_entry/stickyweb desc = "A sticky web; sticky for everyone but you. Your colleagues may not appreciate it." spawn_type = /obj/structure/spider/stickyweb hunger_cost = 5 + craft_time = 1 SECONDS icon = 'icons/effects/web.dmi' icon_state = "webpassage" @@ -27,12 +29,14 @@ desc = "Creates a barely visible web snare that traps the legs of any mob that walk through it." spawn_type = /obj/structure/spider/web_snare hunger_cost = 10 + craft_time = 2 SECONDS // Tripwire Webs /datum/web_craft_entry/tripwire_web desc = "Creates a barely visible tripwire snare that silently tells you if a mob walk throughs it." spawn_type = /obj/structure/spider/tripwire_web hunger_cost = 5 + craft_time = 1 SECONDS /datum/web_craft_entry/tripwire_web/spawn_structure(mob/living/user, turf/target_turf) return new /obj/structure/spider/tripwire_web(target_turf, user) diff --git a/tgstation.dme b/tgstation.dme index 384559df86d8fe..6f5810421fdde8 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7469,6 +7469,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bioluminescence.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bloodhound.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\cocoon.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\darkvision.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\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" From db8709d0d88ee8c336bb1cadc2a8206ad0b887fd Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 1 Mar 2026 13:14:46 +0100 Subject: [PATCH 110/212] Added Monstrous Body. Changed power prefs middleware to always return lists and added support for "any" in required powers. --- modular_doppler/modular_powers/code/_power.dm | 2 + .../aberrant/_aberrant_root_beastial.dm | 2 +- .../aberrant/_aberrant_root_monstrous.dm | 62 +++++++++++++++++++ .../powers/resonant/aberrant/darkvision.dm | 7 ++- .../powers/resonant/aberrant/shapechange.dm | 6 +- .../powers/resonant/aberrant/vent_crawl.dm | 6 +- ...ter_entries.dm => _web_crafter_entries.dm} | 0 .../code/powers_prefs_middleware.dm | 41 +++++++++--- tgstation.dme | 3 +- 9 files changed, 111 insertions(+), 18 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_monstrous.dm rename modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/{web_crafter_entries.dm => _web_crafter_entries.dm} (100%) diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index fa39a81a2c87fa..4a5801188c6aa7 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -41,6 +41,8 @@ var/list/required_powers /// Allow subtypes to count for requirements. var/required_allow_subtypes + /// Any one of the required powers satisfies the requirement list. + var/required_allow_any // The path, if applicable, to the action. var/datum/action/cooldown/power/action_path 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 de9d2c5e7c6a39..f35f630b78cb78 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 @@ -1,5 +1,5 @@ /datum/power/aberrant_root/beastial - name = "Beastkindred" + name = "Beastial Body" desc = "You have the traits of an animal; and with it, the apetite of one. In addition to your species normal preferences, you now like the following food based on your choice of Herbivore or Carnivore (including making it non-toxic)\ \nHerbivore: Vegetables, Fruit & Nuts. \ \nCarnivore: Raw, Gore, Meat, Bugs & Seafood." diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_monstrous.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_monstrous.dm new file mode 100644 index 00000000000000..80c7de51ecc38b --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_monstrous.dm @@ -0,0 +1,62 @@ +// MORE BLOOD +/datum/power/aberrant_root/monstrous + name = "Monstrous Body" + desc = "If it bleeds, you can kill it. Just with you, blood doesn't really matter. You have 125% the normal blood capacity of your species, and regenerate blood that much faster as well.\ + \n The thresholds for being low on blood are unchanged, meaning you are extra resistent to bloodloss." + value = 3 + + // Target blood level while this power is active. + var/target_blood_volume + // Tracks if we applied our regen multiplier so we can undo safely. + var/regen_multiplier_applied + // How much extra blood capacity we have. + var/extra_blood_mult = 1.25 + // How much faster our blood regenerates. + var/extra_blood_regen_mult = 1.25 + +/datum/power/aberrant_root/monstrous/add() + var/mob/living/carbon/human/human_holder = power_holder + if(!istype(human_holder) || HAS_TRAIT(human_holder, TRAIT_NOBLOOD)) + return + + target_blood_volume = BLOOD_VOLUME_NORMAL * extra_blood_mult + human_holder.blood_volume = min(target_blood_volume, BLOOD_VOLUME_MAXIMUM) + + human_holder.physiology.blood_regen_mod *= extra_blood_regen_mult + regen_multiplier_applied = TRUE + + RegisterSignal(human_holder, COMSIG_HUMAN_ON_HANDLE_BLOOD, PROC_REF(handle_extra_blood_regen)) + + +/datum/power/aberrant_root/monstrous/remove() + var/mob/living/carbon/human/human_holder = power_holder + if(!istype(human_holder)) + return + + UnregisterSignal(human_holder, COMSIG_HUMAN_ON_HANDLE_BLOOD) + + if(regen_multiplier_applied) + human_holder.physiology.blood_regen_mod /= extra_blood_regen_mult + regen_multiplier_applied = FALSE + + if(human_holder.blood_volume > BLOOD_VOLUME_NORMAL) + human_holder.blood_volume = BLOOD_VOLUME_NORMAL + + target_blood_volume = 0 + +// So its hardcoded that blood caps out at BLOOD_VOLUME_NORMAL so we have to handle blood regen in our own way here. +/datum/power/aberrant_root/monstrous/proc/handle_extra_blood_regen(datum/source, seconds_per_tick, times_fired) + SIGNAL_HANDLER + + if(!target_blood_volume) + return + + var/mob/living/carbon/human/human_holder = power_holder + if(!istype(human_holder) || HAS_TRAIT(human_holder, TRAIT_NOBLOOD)) + return + + if(human_holder.blood_volume < BLOOD_VOLUME_NORMAL || human_holder.blood_volume >= target_blood_volume) + return + + var/blood_regen_amount = BLOOD_REGEN_FACTOR * human_holder.physiology.blood_regen_mod * seconds_per_tick + human_holder.blood_volume = min(human_holder.blood_volume + blood_regen_amount, target_blood_volume) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm index a9f0d367c64fb0..6675bd05792744 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm @@ -1,11 +1,14 @@ // Lets you see in the dark. Duh. /datum/power/aberrant/darkvision name = "Darkvision" - desc = "Your eyes see perfectly in the dark; but your vision gains a blue-ish hue to it." + desc = "Your eyes see perfectly in the dark; but your vision gains a blue-ish hue to it.\ + \n(Available to both Beastial and Monstrous Root)" mob_trait = TRAIT_TRUE_NIGHT_VISION value = 3 - required_powers = list(/datum/power/aberrant_root/beastial) + required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous) + required_allow_any = TRUE + var/eye_color_cutoffs_applied = FALSE /datum/power/aberrant/darkvision/add() 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 c7a46e5e937d39..dc4e8f34c836a7 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm @@ -7,10 +7,12 @@ desc = "You can adjust your body to turn into a specific type of animal (chosen in the power).\ \n Activating the ability transforms you into the chosen animal. It does not have your name or any other identifying traits, but the number is always the same when you use it (and the security record for this power elaborates on what creature and numbers). \ \n Using the ability makes you hungry, and cannot be used while you're starving.\ - \n If the creature dies or the effect ends, you are reverted to your normal form (prone on the ground), and all damage taken is transfered to your original form (halved if reverting back manually)." + \n If the creature dies or the effect ends, you are reverted to your normal form (prone on the ground), and all damage taken is transfered to your original form (halved if reverting back manually).\ + \n(Available to both Beastial and Monstrous Root)" value = 5 - required_powers = list(/datum/power/aberrant_root/beastial) + required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous) + required_allow_any = TRUE action_path = /datum/action/cooldown/power/aberrant/shapechange /datum/action/cooldown/power/aberrant/shapechange 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 4266e0ef6d37de..e3a45f429de81e 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,10 +2,12 @@ /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. Does not work on oversized mobs.\ + \n(Available to both Beastial and Monstrous Root)" value = 5 power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES - required_powers = list(/datum/power/aberrant_root/beastial) + required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous) + required_allow_any = TRUE /datum/power/aberrant/vent_crawl/add(client/client_source) . = ..() 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 similarity index 100% rename from modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter_entries.dm rename to modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/_web_crafter_entries.dm diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index 29c09d73c4f508..c49cee9534dee8 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -54,7 +54,7 @@ if(get_requiring_power(power_type)) locked_in = TRUE else - if(get_incompatible_power(power_type) || get_required_power(power_type) || would_exceed_path_limit(power_type)) + if(get_incompatible_power(power_type) || length(get_required_power(power_type)) || would_exceed_path_limit(power_type)) locked_in = TRUE var/state @@ -204,9 +204,15 @@ return FALSE // Make sure we have the required powers. - var/datum/power/required_power_type = get_required_power(power_type) - if(required_power_type) - to_chat(user, span_boldwarning("[power_name] is missing [required_power_type.name]!")) + var/list/missing_required_powers = get_required_power(power_type) + if(length(missing_required_powers)) + var/list/required_names = list() + for(var/datum/power/required_option as anything in missing_required_powers) + required_names += required_option.name + if(power_type.required_allow_any) + to_chat(user, span_boldwarning("[power_name] requires any of: [english_list(required_names)]!")) + else + to_chat(user, span_boldwarning("[power_name] requires: [english_list(required_names)]!")) return FALSE // Make sure we don't select an incompatible power. @@ -363,23 +369,30 @@ return TRUE /** - * Checks whether we are missing at least one required power for a given power type, - * and returns the first one encountered if so. + * Checks whether we are missing required powers for a given power type. + * Returns a list of missing requirements (empty if satisfied). + * If required_allow_any is TRUE, the list contains all valid options when none are satisfied. */ /datum/preference_middleware/powers/proc/get_required_power(datum/power/power_type) var/list/required_powers = GLOB.powers_requirements_list[power_type] if(!length(required_powers)) - return + return list() + + var/allow_any = power_type.required_allow_any + var/allow_subtypes = power_type.required_allow_subtypes + var/list/missing_required = list() for(var/datum/power/required_power_type as anything in required_powers) var/required_power_name = required_power_type.name - // Exact requirement satisfied (current behaviour) + // Exact requirement satisfied if(required_power_name in preferences.all_powers) + if(allow_any) + return list() continue // Optional: allow subtypes, decided by the power we're trying to learn - if(power_type.required_allow_subtypes) + if(allow_subtypes) var/required_typepath = ispath(required_power_type) ? required_power_type : required_power_type.type var/found_subtype = FALSE @@ -393,9 +406,17 @@ break if(found_subtype) + if(allow_any) + return list() continue - return required_power_type + if(!allow_any) + missing_required += required_power_type + + if(allow_any) + return required_powers + + return missing_required /** diff --git a/tgstation.dme b/tgstation.dme index 6f5810421fdde8..25f8863e70b0ce 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7466,6 +7466,7 @@ #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\bioluminescence.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bloodhound.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\cocoon.dm" @@ -7476,11 +7477,11 @@ #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\shapechange_wolf.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\vent_crawl.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\_web_craft_datum.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\_web_crafter_entries.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\binding_webs.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\snare_webs.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\tripwire_webs.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\web_crafter.dm" -#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\web_crafter_entries.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_dantian.dm" From 1974a58e0d56489f7058c6f2d66e3d71b1b80429 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 1 Mar 2026 13:21:19 +0100 Subject: [PATCH 111/212] Removed available to beastial and monstrous root from descriptions --- .../code/powers/resonant/aberrant/bioluminescence.dm | 3 ++- .../modular_powers/code/powers/resonant/aberrant/darkvision.dm | 3 +-- .../code/powers/resonant/aberrant/shapechange.dm | 3 +-- .../modular_powers/code/powers/resonant/aberrant/vent_crawl.dm | 3 +-- 4 files changed, 5 insertions(+), 7 deletions(-) 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 5e88ecab31fd3d..7ef3ef3387240a 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm @@ -4,7 +4,8 @@ desc = "You can glow! You passively emit the chosen light color; which can be toggled on or off at will. Very slightly increases passive hunger when enabling or disabling the light." value = 1 - required_powers = list(/datum/power/aberrant_root/beastial) + required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous) + required_allow_any = TRUE action_path = /datum/action/cooldown/power/aberrant/bioluminescence /datum/action/cooldown/power/aberrant/bioluminescence 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 6675bd05792744..eb1e24a15428c1 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm @@ -1,8 +1,7 @@ // Lets you see in the dark. Duh. /datum/power/aberrant/darkvision name = "Darkvision" - desc = "Your eyes see perfectly in the dark; but your vision gains a blue-ish hue to it.\ - \n(Available to both Beastial and Monstrous Root)" + desc = "Your eyes see perfectly in the dark; but your vision gains a blue-ish hue to it." mob_trait = TRAIT_TRUE_NIGHT_VISION value = 3 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 dc4e8f34c836a7..cbb35a6e0c3b70 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm @@ -7,8 +7,7 @@ desc = "You can adjust your body to turn into a specific type of animal (chosen in the power).\ \n Activating the ability transforms you into the chosen animal. It does not have your name or any other identifying traits, but the number is always the same when you use it (and the security record for this power elaborates on what creature and numbers). \ \n Using the ability makes you hungry, and cannot be used while you're starving.\ - \n If the creature dies or the effect ends, you are reverted to your normal form (prone on the ground), and all damage taken is transfered to your original form (halved if reverting back manually).\ - \n(Available to both Beastial and Monstrous Root)" + \n If the creature dies or the effect ends, you are reverted to your normal form (prone on the ground), and all damage taken is transfered to your original form (halved if reverting back manually)." value = 5 required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous) 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 e3a45f429de81e..70e16d0c75f5ec 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,8 +2,7 @@ /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.\ - \n(Available to both Beastial and Monstrous Root)" + \nIf you are undersized, you can crawl in vents while wearing your normal equipment. Does not work on oversized mobs." value = 5 power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous) From 9ef49e9912d5ef4abc73451267b2d068d40bbae0 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 3 Mar 2026 08:10:56 +0100 Subject: [PATCH 112/212] Adds armblade. Cahnges some dispel behavior with the root theologsit powers. --- .../code/powers/resonant/aberrant/armblade.dm | 88 +++++++++++++++++++ .../aberrant/web_crafter/snare_webs.dm | 6 +- .../theologist/_theologist_root_revered.dm | 2 +- .../theologist/_theologist_root_shared.dm | 2 +- .../theologist/_theologist_root_twisted.dm | 8 +- .../powers/sorcerous/theologist/purify.dm | 4 +- tgstation.dme | 1 + 7 files changed, 98 insertions(+), 13 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm new file mode 100644 index 00000000000000..75e937b446994a --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm @@ -0,0 +1,88 @@ +// Its like a weaker version of the changeling armblade. +/datum/power/aberrant/armblade + name = "Armblade" + desc = "Allows you to transform your arm into a deadly blade. The weapon itself has high damage, pierces armor and can destroy tables that block your way.\ + \n Requires an empty hand to use." + value = 4 + required_powers = list(/datum/power/aberrant_root/monstrous) + action_path = /datum/action/cooldown/power/aberrant/armblade + +/datum/action/cooldown/power/aberrant/armblade + name = "Armblade" + desc = "Reform your arm into a deadly blade. Using the power again retracts it." + button_icon = 'icons/mob/actions/actions_changeling.dmi' + button_icon_state = "armblade" + active = FALSE + + cooldown_time = 50 + +/datum/action/cooldown/power/aberrant/armblade/Grant(mob/granted_to) + . = ..() + RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + +/datum/action/cooldown/power/aberrant/armblade/Remove(mob/removed_from) + . = ..() + UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL) + +/datum/action/cooldown/power/aberrant/armblade/use_action(mob/living/user, atom/target) + if(active) + for(var/obj/item/melee/arm_blade/aberrant/blade in user.held_items) + user.temporarilyRemoveItemFromInventory(blade, TRUE) + playsound(user, 'sound/effects/blob/blobattack.ogg', 30, TRUE) + user.visible_message( + span_warning("With a sickening crunch, [user] reforms [user.p_their()] blade into an arm!"), + span_notice("You assimilate the blade back into your body.")) + user.update_held_items() + active = FALSE + return TRUE + + if(user.get_active_held_item()) + user.balloon_alert(user, "hand occupied!") + return FALSE + + var/obj/item/melee/arm_blade/aberrant/new_blade = new(user, FALSE) + if(!user.put_in_active_hand(new_blade)) + qdel(new_blade) + return FALSE + + playsound(user, 'sound/effects/blob/blobattack.ogg', 30, TRUE) + active = TRUE + return TRUE + +/datum/action/cooldown/power/aberrant/armblade/proc/on_dispel(mob/owner, atom/dispeller) + SIGNAL_HANDLER + if(!active) + return NONE + + for(var/obj/item/melee/arm_blade/aberrant/blade in owner.held_items) + owner.temporarilyRemoveItemFromInventory(blade, TRUE) + owner.visible_message( + span_warning("With a sickening crunch, [owner] reforms [owner.p_their()] blade into an arm!"), + span_boldwarning("Your arm twists back to normal against your own volition!")) + owner.update_held_items() + break + + active = FALSE + StartCooldownSelf(150) + return DISPEL_RESULT_DISPELLED + +// Weaker version +/obj/item/melee/arm_blade/aberrant + force = 20 + armour_penetration = 25 + +// No door forcing. +/obj/item/melee/arm_blade/aberrant/afterattack(atom/target, mob/user, list/modifiers, list/attack_modifiers) + if(istype(target, /obj/structure/table)) + var/obj/smash = target + smash.deconstruct(FALSE) + + else if(istype(target, /obj/machinery/computer)) + target.attack_alien(user) + +// Override the init as to rephrase the spawn message, preventing changeling nouns of 'our' +/obj/item/melee/arm_blade/aberrant/Initialize(mapload, silent, synthetic) + . = ..(mapload, TRUE, synthetic) // suppress parent message + if(ismob(loc)) + loc.visible_message(span_warning("A grotesque blade forms around [loc.name]\'s arm!"), span_notice("Your arm twists and mutates, transforming it into a deadly blade.")) + return . diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/snare_webs.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/snare_webs.dm index e845da1788c8d4..85fa37f237a843 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 @@ -1,9 +1,9 @@ // Snares on the ground! Its like a beartrap but it doesnt hurt and destroys itself after. /datum/power/aberrant/snare_webs name = "Snare Webs" - desc = " Allows you to craft snares. These are placed on the ground and are hard to see; but can be disarmed.\ + desc = "Allows you to craft snares. These are placed on the ground and are hard to see; but can be disarmed.\ \n Mobs without the ability to walk through webs will be legcuffed if they walk through it.\ - \n Simple mobs instead receive a slowing status effect for 10 seconds." + \n Simple mobs instead receive a slowing status effect for 8 seconds." value = 3 required_powers = list(/datum/action/cooldown/power/aberrant/web_crafter) @@ -36,7 +36,7 @@ icon_state = "sticky_overlay" armed = TRUE trap_damage = 0 - breakouttime = 15 SECONDS + breakouttime = 8 SECONDS item_flags = DROPDEL /// Tracks if this was actually used as legcuffs so we can delete on uncuff only. var/was_cuffed = FALSE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index d175deda89a6bb..4547134dc1ec1e 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -102,7 +102,7 @@ to_chat(owner, span_userdanger("Your [burden_power.name] deactives prematurely!")) if(!owner == burden_power.owner) to_chat(burden_power.owner, span_warning("Your [burden_power.name] has been dispelled!")) - burden_power.StartCooldownSelf(150) // Just so you don't immediately reapply it. + burden_power.StartCooldownSelf() expire() return DISPEL_RESULT_DISPELLED diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index 01cd0a4d5fa83f..7c5b834902fc4a 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -127,7 +127,7 @@ to_chat(owner, span_userdanger("Your burdens are no longer shared!")) to_chat(current_target, span_userdanger("Your burdens are no longer shared!")) clear_link() - StartCooldownSelf(150) // Just so you don't immediately reapply it. + StartCooldownSelf() // Just so you don't immediately reapply it. return DISPEL_RESULT_DISPELLED /** diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index c658623291b9ba..fe5e6ecad2b896 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -6,10 +6,6 @@ action_path = /datum/action/cooldown/power/theologist/theologist_root/twisted value = 5 - mob_trait = TRAIT_ARCHETYPE_SORCEROUS - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THEOLOGIST - priority = POWER_PRIORITY_ROOT /datum/action/cooldown/power/theologist/theologist_root/twisted name = "A Burden Twisted" @@ -17,7 +13,7 @@ Gives Piety proportional to the amount of damage twisted." button_icon = 'icons/mob/actions/actions_cult.dmi' button_icon_state = "hand" - cooldown_time = 300 + cooldown_time = 200 target_range = 1 target_type = /mob/living click_to_activate = TRUE @@ -194,5 +190,5 @@ return NONE keep_going = FALSE owner.visible_message(span_warning("The resonant link between [owner.get_visible_name()] and [current_target.get_visible_name()] is broken!!"), span_notice("Your [name] is dispelled!")) - StartCooldownSelf(300) + StartCooldownSelf() return DISPEL_RESULT_DISPELLED diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm index 8331da7003dbec..d3b862c22f8c0e 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm @@ -9,7 +9,7 @@ /datum/power/theologist/purify name = "Purify" desc = "Cleanses impurity from objects and creatures in melee range. The chosen target is immediately dispelled and purified of all poisons. \ - If the target is an object with a holy equivelant, it turns it into that (e.g water into holy water). Has varying purity costs, but usually defaults to 5." + If the target is an object with a holy equivelant, it turns it into that (e.g water into holy water). Has varying piety costs, but usually defaults to 5." action_path = /datum/action/cooldown/power/theologist/purify value = 5 @@ -18,7 +18,7 @@ /datum/action/cooldown/power/theologist/purify name = "Purify" desc = "Cleanses impurity from objects and creatures in melee range. The chosen target is immediately dispelled and purified of all poisons. \ - If the target is an object with a holy equivelant, it turns it into that (e.g water into holy water). Has varying purity costs, but usually defaults to 5." + If the target is an object with a holy equivelant, it turns it into that (e.g water into holy water). Has varying piety costs, but usually defaults to 5." button_icon = 'icons/obj/mining_zones/artefacts.dmi' button_icon_state = "purified_soulstone" cooldown_time = 60 diff --git a/tgstation.dme b/tgstation.dme index 25f8863e70b0ce..d31c0f27c6f872 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7467,6 +7467,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root_anomalous.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root_beastial.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\_aberrant_root_monstrous.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\armblade.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bioluminescence.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bloodhound.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\cocoon.dm" From 828496f3d76daa5f5dd8b91d7dfbc8ad60939fba Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 5 Mar 2026 20:57:24 +0100 Subject: [PATCH 113/212] Adds manipulate which mimmicks the long distance manipulation genetic TK has. --- code/__DEFINES/~doppler_defines/powers.dm | 6 ++ code/modules/mob/living/living.dm | 7 +- code/modules/mob/mob.dm | 4 + code/modules/tgui/states.dm | 4 + .../mortal/augmented/simple_augments.dm | 10 +-- .../code/powers/resonant/psyker/manipulate.dm | 82 +++++++++++++++++++ tgstation.dme | 1 + 7 files changed, 103 insertions(+), 11 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index bd56c68bc48621..c293d56256ff08 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -59,6 +59,12 @@ // Bitflags for how dispel should behave #define DISPEL_CASCADE_CARRIED (1<<0) +// Trait defining that someone can interact remotely with objects. Used by Manipulate and is overall used to bypass range checks on can_interact +#define TRAIT_REMOTE_INTERACT "remote_interact" + +// Trait that allows a mob to keep UIs open beyond their normal range. +#define TRAIT_NO_UI_DISTANCE "no_ui_distance" + /** * SORCEROUS * All defines related to the sorcerous archetype. diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index ad318acd95b2d6..dcb91e47fd5fb7 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1388,9 +1388,10 @@ var/datum/dna/mob_DNA = has_dna() if(!mob_DNA || !mob_DNA.check_mutation(/datum/mutation/telekinesis) || !tkMaxRangeCheck(src, target)) - if(!(action_bitflags & SILENT_ADJACENCY)) - to_chat(src, span_warning("You are too far away!")) - return FALSE + if(!HAS_TRAIT(src, TRAIT_REMOTE_INTERACT)) // DOPPLER EDIT - Adds a trait for Powers to bypass range checks. + if(!(action_bitflags & SILENT_ADJACENCY)) + to_chat(src, span_warning("You are too far away!")) + return FALSE if((action_bitflags & NEED_VENTCRAWL) && !HAS_TRAIT(src, TRAIT_VENTCRAWLER_NUDE) && !HAS_TRAIT(src, TRAIT_VENTCRAWLER_ALWAYS)) to_chat(src, span_warning("You wouldn't fit!")) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 4512b1ef3bcbc8..f5e1647d98ea2c 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -1104,6 +1104,10 @@ return TRUE if (Adjacent(A)) return TRUE + // DOPPLER ADDITION BEGIN - Adds a trait for Powers to bypass range checks. + if(HAS_TRAIT(src, TRAIT_NO_UI_DISTANCE) && (A in view(src))) + return TRUE + // DOPPLER ADDITION END - Adds a trait for Powers to bypass range checks. var/datum/dna/mob_dna = has_dna() if(mob_dna?.check_mutation(/datum/mutation/telekinesis) && tkMaxRangeCheck(src, A)) return TRUE diff --git a/code/modules/tgui/states.dm b/code/modules/tgui/states.dm index a8fe0a46b4f600..b2e24e911d240e 100644 --- a/code/modules/tgui/states.dm +++ b/code/modules/tgui/states.dm @@ -125,4 +125,8 @@ /mob/living/carbon/human/shared_living_ui_distance(atom/movable/src_object, viewcheck = TRUE, allow_tk = TRUE) if(allow_tk && dna.check_mutation(/datum/mutation/telekinesis) && tkMaxRangeCheck(src, src_object)) return UI_INTERACTIVE + // DOPPLER ADDITION BEGIN - Support for powers to interact with objects from range with LoS + if(HAS_TRAIT(src, TRAIT_NO_UI_DISTANCE) && (src_object in view(src))) + return UI_INTERACTIVE + // DOPPLER ADDITION END - Support for powers to interact with objects from range with LoS return ..() diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm index 859e55b1aff0b6..723e35c7a312e2 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm @@ -47,13 +47,14 @@ ARMS value = 4 augment = /obj/item/organ/cyberimp/arm/toolkit/mining_drill +/* I'm not including this one baseline because its just too fkn stron for unarmed stacking. /datum/power/augmented/strong_arm name = "Strong Arm Implant" desc = "When implanted, this cybernetic implant will enhance the muscles of the arm to deliver more power-per-action. Install one in each arm \ to pry open doors with your bare hands!" value = 10 // door forcing + unarmed stacking with cultivator make this a potential balance hazard. - augment = /obj/item/organ/cyberimp/arm/strongarm + augment = /obj/item/organ/cyberimp/arm/strongarm*/ /* CHEST @@ -67,13 +68,6 @@ The game sometimes calls this spine. value = 3 augment = /obj/item/organ/cyberimp/chest/spine -/datum/power/augmented/spinal_implant/atlas - name = "Atlas Gravitonic Spinal Implant" - desc = "The upgraded version of the Herculean Gravitronic Spinal Implant. Allows for easier lifting, as well as remaining grounded even in low gravity." - - value = 7 - augment = /obj/item/organ/cyberimp/chest/spine/atlas - /* EYE HUDS. Keep in mind these are HUDS. Not actual eye replacements. diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm new file mode 100644 index 00000000000000..8e9089e5d582dc --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm @@ -0,0 +1,82 @@ +/* + I bestow upon thee my attempt to emulate telekines remoter interactions. Allows you to interact with objects from a limited distance. + This required three nonmodular edits: + - code\modules\mob\living\living.dm line 1384 to bypass the range gate. + - code\modules\tgui\states.dm line 128 to bypass the UI closing + - code\modules\mob\mob.dm line 110 to bypass the interaction gate. + I condensed it into TRAIT_NO_UI_DISTANCE && TRAIT_REMOTE_INTERACT so that if someone else wants to do something similar, they can. +*/ + +/datum/power/psyker_power/manipulate + name = "Manipulate" + desc = "Allows you to interact with machinery and various other structures within line of sight as if it were next to you." + + value = 2 + action_path = /datum/action/cooldown/power/psyker/manipulate + mob_trait = TRAIT_NO_UI_DISTANCE + required_powers = list(/datum/power/psyker_power/telekinesis) //given this lets you grab items from certain things from a distance this is basically a fluff requirement to explain why you can grab objects from a distance. + +/datum/action/cooldown/power/psyker/manipulate + name = "Manipulate" + desc = "Allows you to interact with machinery and various other structures within line of sight as if it were next to you." + button_icon = 'icons/mob/actions/actions_mime.dmi' + button_icon_state = "invisible_box" + + target_type = /obj + click_to_activate = TRUE + target_range = 12 + + // Saves if its a right click so that all click interactions are routed through use_action. + var/right_click + + // Saved glow effects on UI elements + var/list/ui_filters = list() + +// We're manipulating click-on to A distnguish between obj machinery and obj structure and B to distinguish between left and right hand clicks. +/datum/action/cooldown/power/psyker/manipulate/InterceptClickOn(mob/living/clicker, params, atom/target) + if(!istype(target, /obj/machinery) && !istype(target, /obj/structure)) + return FALSE + + var/list/mods = params2list(params) + // Right click functionality. + if(LAZYACCESS(mods, RIGHT_CLICK)) + right_click = TRUE + ..() + +// We use TRAIT_REMOTE_INTERACT (temporarily) as to bypass /mob/living/can_perform_action +/datum/action/cooldown/power/psyker/manipulate/use_action(mob/living/user, atom/target) + ADD_TRAIT(user, TRAIT_REMOTE_INTERACT, src) // this is specifically for allowing us to bypass the interaction gate. + new /obj/effect/temp_visual/telekinesis(get_turf(target)) + if(right_click) // rmb + target.attack_hand_secondary(user) + else // lmb + target.attack_hand(user) + + // interact with UI if present. + if(target.interaction_flags_atom & INTERACT_ATOM_UI_INTERACT) + target.ui_interact(user) + + // We save the ui so we can add a filter + var/datum/tgui/ui = SStgui.get_open_ui(user, target) + if(ui) + var/filter_id = "manipulate_glow" + target.add_filter(filter_id, 1, list(type = "outline", color = "#ff66cc", size = 2)) + animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1) + animate(alpha = 40, time = 2.5 SECONDS) + ui_filters[ui] = list(target, filter_id) + + RegisterSignal(ui, COMSIG_QDELETING, PROC_REF(on_ui_closed)) + + REMOVE_TRAIT(user, TRAIT_REMOTE_INTERACT, src) + right_click = FALSE + modify_stress(PSYKER_STRESS_TRIVIAL) + return TRUE + +/datum/action/cooldown/power/psyker/manipulate/proc/on_ui_closed(datum/tgui/ui) + SIGNAL_HANDLER + var/list/entry = ui_filters[ui] + if(entry) + var/atom/target = entry[1] + var/filter_id = entry[2] + target?.remove_filter(filter_id) + ui_filters -= ui diff --git a/tgstation.dme b/tgstation.dme index d31c0f27c6f872..11e4fd5ce3776d 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7502,6 +7502,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\levitate.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\manipulate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\scrying.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\_psyker_event.dm" From 5c26d9de77c36689be5fe17906d92e45abecdc2d Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 5 Mar 2026 21:22:22 +0100 Subject: [PATCH 114/212] Adds a whitelist and blacklist to manipulate. --- .../code/powers/resonant/psyker/manipulate.dm | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) 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 8e9089e5d582dc..75dfb4bf72c121 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm @@ -31,10 +31,20 @@ // Saved glow effects on UI elements var/list/ui_filters = list() + // Whitelist of types allowed to be manipulated. + var/static/list/target_whitelist = typecacheof(list( + /obj/machinery, + /obj/structure, + /obj/item/radio/intercom, + )) + // UI blacklist for targets that should never open a UI via Manipulate. + var/static/list/ui_blacklist = typecacheof(list( + /obj/machinery/door/airlock, + )) // We're manipulating click-on to A distnguish between obj machinery and obj structure and B to distinguish between left and right hand clicks. /datum/action/cooldown/power/psyker/manipulate/InterceptClickOn(mob/living/clicker, params, atom/target) - if(!istype(target, /obj/machinery) && !istype(target, /obj/structure)) + if(!is_type_in_typecache(target, target_whitelist)) return FALSE var/list/mods = params2list(params) @@ -45,24 +55,26 @@ // We use TRAIT_REMOTE_INTERACT (temporarily) as to bypass /mob/living/can_perform_action /datum/action/cooldown/power/psyker/manipulate/use_action(mob/living/user, atom/target) - ADD_TRAIT(user, TRAIT_REMOTE_INTERACT, src) // this is specifically for allowing us to bypass the interaction gate. + ADD_TRAIT(user, TRAIT_REMOTE_INTERACT, src) // this is specifically for allowing us to bypass the range interaction gate. new /obj/effect/temp_visual/telekinesis(get_turf(target)) if(right_click) // rmb target.attack_hand_secondary(user) else // lmb target.attack_hand(user) - // interact with UI if present. - if(target.interaction_flags_atom & INTERACT_ATOM_UI_INTERACT) + // interact with UI if present and not blacklisted. + if((target.interaction_flags_atom & INTERACT_ATOM_UI_INTERACT) && !is_type_in_typecache(target, ui_blacklist)) target.ui_interact(user) - // We save the ui so we can add a filter + // We save the ui so we can add a filter to show it is being interacted with. var/datum/tgui/ui = SStgui.get_open_ui(user, target) if(ui) var/filter_id = "manipulate_glow" target.add_filter(filter_id, 1, list(type = "outline", color = "#ff66cc", size = 2)) - animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1) - animate(alpha = 40, time = 2.5 SECONDS) + var/filter = target.get_filter(filter_id) + if(filter) + animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1) + animate(alpha = 40, time = 2.5 SECONDS) ui_filters[ui] = list(target, filter_id) RegisterSignal(ui, COMSIG_QDELETING, PROC_REF(on_ui_closed)) From d9be992a0f74837c602d1ef7433a80833995399c Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 6 Mar 2026 20:35:52 +0100 Subject: [PATCH 115/212] Adds omnilingual. Lets you speak a whole loada languages. Also rebasing after this. --- .../code/powers/mortal/expert/omnilingual.dm | 25 +++++++++++++++++++ tgstation.dme | 1 + 2 files changed, 26 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm new file mode 100644 index 00000000000000..8bb7df006fb76b --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm @@ -0,0 +1,25 @@ +// Lets you speak a lot of things; but not as many lots as Curator. +/datum/power/expert/omnilingual + name = "Omnilingual" + desc = "You speak an absurd amount of languages; you start off understanding every language at full proficiency. Does not apply to languages not available at character selection." + + value = 4 + // Saved list of languages that were given by this power to remove when the power is removed. + var/list/given_languages_list = list() + +// Iterate through every language in the roundstart languages. If they have it, skip, otherwise, give it to them and add it to given_langauges_list +/datum/power/expert/omnilingual/add() + for(var/datum/language/language_type as anything in GLOB.uncommon_roundstart_languages) + if(power_holder.has_language(language_type, ALL)) + continue + power_holder.grant_language(language_type, ALL, src) + given_languages_list += language_type + +// Removes all languages that were given through omnilingual. +/datum/power/expert/omnilingual/remove() + if(!power_holder) + return + + for(var/datum/language/language_type as anything in given_languages_list) + power_holder.remove_language(language_type, ALL, src) + given_languages_list.Cut() diff --git a/tgstation.dme b/tgstation.dme index 11e4fd5ce3776d..1d2925b30e0fd9 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7440,6 +7440,7 @@ #include "modular_doppler\modular_powers\code\powers\mortal\expert\heavy_lifter.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\master_surgeon.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\obfuscate_voice.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\expert\omnilingual.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\punt.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\rich.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\strider.dm" From c3d003131b211c4a8aefaa5a7d83edc76ed2919c Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 6 Mar 2026 21:18:34 +0100 Subject: [PATCH 116/212] Gets omnilingual up to parity with current doppler language systems. Tweaks some errors from the rebase. --- modular_doppler/modular_powers/code/_power.dm | 2 +- .../powers/mortal/expert/obfuscate_voice.dm | 8 +--- .../code/powers/mortal/expert/omnilingual.dm | 41 ++++++++++++++++--- .../psyker/psyker_events/_psyker_event.dm | 2 +- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index 4a5801188c6aa7..f82965060da531 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -225,7 +225,7 @@ GLOBAL_LIST_INIT_TYPED(all_power_constant_data, /datum/power_constant_data, gene /// A singleton datum representing constant data and procs used by powers. /datum/power_constant_data - var/abstract_type = /datum/power_constant_data + abstract_type = /datum/power_constant_data /// The typepath of the power we will be associated with in the global list. var/datum/power/associated_typepath diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm index 19f229b477d3ae..e2d3982e0f80a6 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm @@ -42,15 +42,11 @@ Hides your voice as unknown while active. Act out the machivalean you always wan source_action = passed_action /datum/status_effect/power/obfuscate_voice/on_apply() - var/mob/living/carbon/human/H = owner - if(istype(H)) - H.SetSpecialVoice("Unknown") + ADD_TRAIT(owner, TRAIT_UNKNOWN_VOICE, TRAIT_STATUS_EFFECT(id)) return TRUE /datum/status_effect/power/obfuscate_voice/on_remove() - var/mob/living/carbon/human/H = owner - if(istype(H)) - H.UnsetSpecialVoice() + REMOVE_TRAIT(owner, TRAIT_UNKNOWN_VOICE, TRAIT_STATUS_EFFECT(id)) return /atom/movable/screen/alert/status_effect/obfuscate_voice 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 8bb7df006fb76b..6849253d1a47c2 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm @@ -1,19 +1,48 @@ // Lets you speak a lot of things; but not as many lots as Curator. /datum/power/expert/omnilingual name = "Omnilingual" - desc = "You speak an absurd amount of languages; you start off understanding every language at full proficiency. Does not apply to languages not available at character selection." + desc = "You speak an absurd amount of languages; you are able to understand and speak every language at full proficiency. Does not apply to languages not available to your character at character selection." value = 4 // Saved list of languages that were given by this power to remove when the power is removed. var/list/given_languages_list = list() -// Iterate through every language in the roundstart languages. If they have it, skip, otherwise, give it to them and add it to given_langauges_list +// Iterate through the language prefs list. If they have it, skip, otherwise, give it to them and add it to given_languages_list. /datum/power/expert/omnilingual/add() - for(var/datum/language/language_type as anything in GLOB.uncommon_roundstart_languages) - if(power_holder.has_language(language_type, ALL)) + if(!power_holder) + return + + var/datum/species/species = null + if(istype(power_holder, /mob/living/carbon/human)) + var/mob/living/carbon/human/human_holder = power_holder + species = human_holder.dna?.species + + var/datum/language_holder/lang_holder = null + if(species) + lang_holder = new species.species_language_holder() + + given_languages_list = list() + // Doppler languages specifically filter all languages, so we mimmick those filters. + for (var/language_name in GLOB.all_languages_by_priority) + var/datum/language/language = GLOB.language_datum_instances[language_name] + + // If we already have the language, skip + if(power_holder.has_language(language.type, ALL)) continue - power_holder.grant_language(language_type, ALL, src) - given_languages_list += language_type + + // Skips secret languages. + if(language.secret && !(species && (language.type in species.language_prefs_whitelist))) + continue + + // Trims languages not available to your species. + if(species && species.always_customizable && lang_holder && !(language.type in lang_holder.spoken_languages)) + continue + + power_holder.grant_language(language.type, ALL, src) + given_languages_list += language.type + + if(lang_holder) + qdel(lang_holder) // Removes all languages that were given through omnilingual. /datum/power/expert/omnilingual/remove() diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm index 0895406e0da8da..786043e8f9d4af 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/_psyker_event.dm @@ -4,7 +4,7 @@ /datum/psyker_event // Remember to set abstracts to this. - var/abstract_type = /datum/psyker_event + abstract_type = /datum/psyker_event // check defines for weights. var/weight = PSYKER_EVENT_RARITY_COMMON // For events that continue for a while, this skips the qdel step. MAKE SURE YOU QDEL IT YOURSELF LATER INSIDE THE CODE. From 78b98a7dda0f38e8f430116cfdb7af7ca0fdcbe5 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Mar 2026 09:04:27 +0100 Subject: [PATCH 117/212] Radiosynthesis; heal from rads! --- .../resonant/aberrant/radiosynthesis.dm | 38 +++++++++++++++++++ tgstation.dme | 1 + 2 files changed, 39 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm new file mode 100644 index 00000000000000..68d64ae4ffe176 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm @@ -0,0 +1,38 @@ +/* + Sunbathe under the Supermatter for healing. Doctors hate this trick! Heals every damage type except oxyloss. +*/ +/datum/power/aberrant/radiosyntehsis + name = "Radiosynthesis" + desc = "Rather than the molecular degredation you experience from radioactivity, your body instead uses it as an energy source to rapidly heal your body. Radioactivity heals you instead of damaging you. Because this healing is anomalous, it heals synthetic and biological bodyparts." + value = 3 + mob_trait = TRAIT_HALT_RADIATION_EFFECTS // we don't give radimmune cause we want to ENCOURAGE people to get irradiated. + power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES + + required_powers = list(/datum/power/aberrant_root/anomalous) + + // how much we heal per second + var/healing = 1 + +/datum/power/aberrant/radiosyntehsis/process(seconds_per_tick) + // Only heal if we're irradiated + if(!HAS_TRAIT(power_holder, TRAIT_IRRADIATED)) + return + + var/heal_amt = healing * seconds_per_tick + if(heal_amt <= 0) + return + + // Get body parts, heal damage on them if there's any. + var/mob/living/carbon/mob = power_holder + var/list/parts = mob.get_damaged_bodyparts(1, 1) + if(parts.len) + for(var/obj/item/bodypart/bodypart in parts) + if(bodypart.heal_damage(heal_amt/parts.len, heal_amt/parts.len, required_bodytype = NONE)) // Because anomalous is weird and funky we allow it to heal synthetic parts. This is deliberate. Be a radation powered robot. Beep boop. + mob.update_damage_overlays() + return + + // Heal toxins if we didn't heal any other damage, but never remove the last point (keeps irradiation). + var/tox_loss = power_holder.getToxLoss() + if(tox_loss > 1 && heal_amt > tox_loss) // We don't want to heal all of a person's radiation, just as to preserve their radioactiv + var/tox_heal = min(heal_amt, tox_loss - 1) + power_holder.adjustToxLoss(-tox_heal) diff --git a/tgstation.dme b/tgstation.dme index 1d2925b30e0fd9..a67a9cb7de5f7f 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7473,6 +7473,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bloodhound.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\cocoon.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\darkvision.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\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" From 669c9272aded093c8aae16a6e121da16438022b5 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Mar 2026 09:19:59 +0100 Subject: [PATCH 118/212] Tweaked ventcrawl to be affected by silences. Adjusted the text on resonant immune to specify the pedanticness of passive and active effects. --- .../code/powers/resonant/aberrant/resonant_immune.dm | 5 ++--- .../code/powers/resonant/aberrant/vent_crawl.dm | 11 +++++------ 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm index 7d325ca6cbbf10..c5dbbe8374a87b 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm @@ -3,12 +3,11 @@ */ /datum/power/aberrant/counter_resonance name = "Counter-Resonance Anomaly" - desc = "You have a counteractive effect on resonance-based phenomena. You are immune to resonance-based effects (but not the highly advanced magics wielded by some antagonistic forces), and you cannot use any resonance-based powers." + desc = "You have a counteractive effect on resonance-based phenomena. You are immune to resonance-based effects (but not the highly advanced magics wielded by some antagonistic forces), and you cannot use any resonance-based powers.\ + \n (Silencing only affects active powers; passive powers, such as Radiosyntehsis, are uanffected.)" value = 9 required_powers = list(/datum/power/aberrant_root/anomalous) - archetype = POWER_ARCHETYPE_RESONANT - path = POWER_PATH_ABERRANT /datum/power/aberrant/counter_resonance/add() ADD_TRAIT(power_holder, TRAIT_ANTIRESONANCE, src) 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 70e16d0c75f5ec..5eeeee1374c277 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 @@ -12,8 +12,6 @@ . = ..() if(!power_holder) return - if(!can_use_ventcrawl(power_holder, provide_feedback = FALSE)) - return ADD_TRAIT(power_holder, TRAIT_VENTCRAWLER_ALWAYS, src) RegisterSignal(power_holder, COMSIG_MOB_ALTCLICKON, PROC_REF(on_altclick)) @@ -68,11 +66,12 @@ to_chat(source, span_warning("You need to remove your backpack, helmet, and suit to ventcrawl!")) return COMSIG_MOB_CANCEL_CLICKON -// Are you TOO FUKKEN BIG? -/datum/power/aberrant/vent_crawl/proc/can_use_ventcrawl(mob/living/source, provide_feedback = TRUE) +// Are you TOO FUKKEN BIG? or are you SILENCED? +/datum/power/aberrant/vent_crawl/proc/can_use_ventcrawl(mob/living/source) + if(HAS_TRAIT(source, TRAIT_RESONANCE_SILENCED)) + source.balloon_alert(source, "Silenced!") if(HAS_TRAIT(source, TRAIT_OVERSIZED)) - if(provide_feedback) - to_chat(source, span_warning("You're too large to fit into the ventilation ducts!")) + source.balloon_alert(source, "You're too big to fit!") return FALSE return TRUE From a73858722867bc7a9bf9affbccc0d0eea3f3d90c Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Mar 2026 09:52:53 +0100 Subject: [PATCH 119/212] Adds healing factor. Fixes a typo. --- .../resonant/aberrant/healing_factor.dm | 29 +++++++++++++++++++ .../resonant/aberrant/resonant_immune.dm | 2 +- tgstation.dme | 1 + 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/healing_factor.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/healing_factor.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/healing_factor.dm new file mode 100644 index 00000000000000..455a247208b4cc --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/healing_factor.dm @@ -0,0 +1,29 @@ +/* + You passively heal. Wow. +*/ +/datum/power/aberrant/healing_factor + name = "Healing Factor" + desc = "Your physical injuries heal without assistance. You heal 0.2 damage per second, randomly split between brute and burn damage while not in critical condition. Wounds such as bleeding still require medical treatment." + value = 4 + power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES + + required_powers = list(/datum/power/aberrant_root/monstrous) + + // how much we heal per second + var/healing = 0.2 + +/datum/power/aberrant/healing_factor/process(seconds_per_tick) + // Does not work if you're in crit + if(power_holder.stat >= SOFT_CRIT) + return + + var/heal_amt = healing * seconds_per_tick + if(heal_amt <= 0) + return + + // Heal the first damaged organic limb we find. + var/mob/living/carbon/mob = power_holder + for(var/obj/item/bodypart/bodypart in mob.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC)) + if(bodypart.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC)) + mob.update_damage_overlays() + break diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm index c5dbbe8374a87b..973df7614eeb74 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm @@ -4,7 +4,7 @@ /datum/power/aberrant/counter_resonance name = "Counter-Resonance Anomaly" desc = "You have a counteractive effect on resonance-based phenomena. You are immune to resonance-based effects (but not the highly advanced magics wielded by some antagonistic forces), and you cannot use any resonance-based powers.\ - \n (Silencing only affects active powers; passive powers, such as Radiosyntehsis, are uanffected.)" + \n (Silencing only affects active powers; passive powers, such as Radiosyntehsis, are unaffected.)" value = 9 required_powers = list(/datum/power/aberrant_root/anomalous) diff --git a/tgstation.dme b/tgstation.dme index a67a9cb7de5f7f..74dad9b32eac8a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7473,6 +7473,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\bloodhound.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\cocoon.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\darkvision.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\healing_factor.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\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" From 8cbacc5b9542f2563bec50ebc1e2f7a6134ed41a Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Mar 2026 10:35:02 +0100 Subject: [PATCH 120/212] Added miasmic conversion. Fixes revered gaining piety on self-heall. --- .../resonant/aberrant/miasmic_conversion.dm | 44 +++++++++++++++++++ .../theologist/_theologist_root_revered.dm | 10 ++--- tgstation.dme | 1 + 3 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/miasmic_conversion.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/miasmic_conversion.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/miasmic_conversion.dm new file mode 100644 index 00000000000000..d3de921a37e4ed --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/miasmic_conversion.dm @@ -0,0 +1,44 @@ +/* + You passively convert your brute and burn damage into toxins damage at a 70% rate. +*/ +/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 70% ratio. You also passively heal 0.1 toxins damage per second." + value = 4 + power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES + + required_powers = list(/datum/power/aberrant_root/monstrous) + + // how much we passively heal tox + var/passive_tox_healing = 0.1 + // how much we heal per second + var/healing = 1 + // the ratio at which we convert. + var/conversion_rate = 0.70 + +/datum/power/aberrant/miasmic_conversion/process(seconds_per_tick) + // Does not work if you're in crit + if(power_holder.stat >= SOFT_CRIT) + return + + var/heal_amt = healing * seconds_per_tick + if(heal_amt <= 0) + return + + // Always heal a small amount of toxins. + power_holder.adjustToxLoss(-passive_tox_healing * seconds_per_tick) + + // Gets all limbs and picks a random one. + var/mob/living/carbon/mob = power_holder + var/list/parts = mob.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC) + if(!parts.len) + return + var/obj/item/bodypart/bodypart = pick(parts) + + // Applies healing, then reapplies as damage. + var/damage_before = bodypart.get_damage() + if(bodypart.heal_damage(heal_amt, heal_amt, required_bodytype = BODYTYPE_ORGANIC)) + mob.update_damage_overlays() + var/healed = damage_before - bodypart.get_damage() + if(healed > 0) // Reapply the damage as tox. + power_holder.adjustToxLoss(healed * conversion_rate) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index 4547134dc1ec1e..ab966ad6680ca0 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -12,8 +12,8 @@ name = "A Burden Revered" desc = "Nullifies pain and slowly heals the targeted creature over a prolonged period of time. This may be yourself. \ Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again." - button_icon = 'icons/mob/actions/actions_spells.dmi' - button_icon_state = "transformslime" + button_icon = 'icons/obj/weapons/guns/magic.dmi' + button_icon_state = "revivewand" // I need something better cooldown_time = 50 target_range = 1 target_type = /mob/living @@ -27,7 +27,6 @@ var/healing_self = FALSE /datum/action/cooldown/power/theologist/theologist_root/revered/use_action(mob/living/user, mob/living/target) - to_chat(owner, span_boldnotice("Placeholder")) if(active_effect) qdel(active_effect) active_effect = target.apply_status_effect(/datum/status_effect/power/burden_revered, src) @@ -42,8 +41,8 @@ /datum/action/cooldown/power/theologist/theologist_root/revered/proc/effect_expired(mob/living/target, amount) if(target.ckey) // Don't get piety from healing nobodies. - adjust_piety(amount) if(amount >= 1 && !healing_self) + adjust_piety(amount) to_chat(owner, span_notice("Your previous Burden Revered has expired! You gained [amount] piety!")) owner.playsound_local(owner, 'sound/effects/pray.ogg', 50, FALSE) else @@ -172,4 +171,5 @@ /atom/movable/screen/alert/status_effect/burden_revered name = "A Burden Revered" desc = "You passively heal damage, and are immune to pain for it's duration." - icon_state = "designated_target" // Placeholder + icon = 'icons/obj/weapons/guns/magic.dmi' + icon_state = "revivewand" diff --git a/tgstation.dme b/tgstation.dme index 74dad9b32eac8a..c8b17ae3e29e05 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7474,6 +7474,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\cocoon.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\darkvision.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\healing_factor.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\aberrant\miasmic_conversion.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\radiosynthesis.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\resonant_immune.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\shapechange.dm" From b2e8f4bd787de51b2c9707e0db8bc8f5ec9b40a8 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Mar 2026 15:23:56 +0100 Subject: [PATCH 121/212] First parts of premium augment mechanics. Has repair surgery, mental shielding as an example. Needs polish. --- code/__DEFINES/~doppler_defines/powers.dm | 23 +++ .../items/devices/scanners/health_analyzer.dm | 14 +- .../mortal/augmented/_premium_action.dm | 68 ++++++++ .../mortal/augmented/_premium_augment.dm | 115 ++++++++++++++ .../mortal/augmented/mental_shielding.dm | 78 +++++++++ .../augmented/surgery/_premium_surgery.dm | 126 +++++++++++++++ .../surgery/_premium_surgery_steps.dm | 149 ++++++++++++++++++ tgstation.dme | 5 + 8 files changed, 574 insertions(+), 4 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_action.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index c293d56256ff08..463acf96fa8f36 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -285,5 +285,28 @@ * All defines related to the augmented powers. */ +// Max quality on augments +#define AUGMENTED_PREMIUM_QUALITY_MAX 100 + +// The quality you start with at roundstart +#define AUGMENTED_PREMIUM_QUALITY_START 75 + +// How often augments will normally lose quality, and how much. +#define AUGMENTED_DECAY_INTERVAL 5 MINUTES +#define AUGMENTED_DECAY_AMOUNT 1 + +// Thresholds for Premium Quality tiers. As long as it is above the number = all is good +#define AUGMENTED_PREMIUM_QUALITY_OPTIMAL (AUGMENTED_PREMIUM_QUALITY_MAX * 0.75) +#define AUGMENTED_PREMIUM_QUALITY_HIGH (AUGMENTED_PREMIUM_QUALITY_MAX * 0.50) +#define AUGMENTED_PREMIUM_QUALITY_MEDIUM (AUGMENTED_PREMIUM_QUALITY_MAX * 0.25) +#define AUGMENTED_PREMIUM_QUALITY_LOW (AUGMENTED_PREMIUM_QUALITY_MAX * 0) + +// The amount of performance from each. We expect high to be the norm, so that is our 1, instead of optimal. +#define AUGMENTED_PREMIUM_EFFICIENCY_OPTIMAL 1.2 +#define AUGMENTED_PREMIUM_EFFICIENCY_HIGH 1 +#define AUGMENTED_PREMIUM_EFFICIENCY_MEDIUM 0.85 +#define AUGMENTED_PREMIUM_EFFICIENCY_LOW 0.6 +#define AUGMENTED_PREMIUM_EFFICIENCY_BROKEN 0 + // Used for the prefs to shorthand tell there's nothing in the right or left arm augment slot. #define AUGMENTED_NO_AUGMENT "None" diff --git a/code/game/objects/items/devices/scanners/health_analyzer.dm b/code/game/objects/items/devices/scanners/health_analyzer.dm index 7d66b7ce965223..4a018f093cf58f 100644 --- a/code/game/objects/items/devices/scanners/health_analyzer.dm +++ b/code/game/objects/items/devices/scanners/health_analyzer.dm @@ -190,7 +190,7 @@ render_list += "Fatigue level: [target.getStaminaLoss()]%.
" else render_list += "Subject appears to be suffering from fatigue.
" - + // Check for brain - both organic (carbon) and synthetic (cyborg MMI) var/has_brain = FALSE if(target.get_organ_slot(ORGAN_SLOT_BRAIN)) @@ -199,7 +199,7 @@ var/mob/living/silicon/robot/cyborg_target = target if(cyborg_target.mmi?.brain) has_brain = TRUE - + if(!has_brain) // kept exclusively for soul purposes render_list += "Subject lacks a brain.
" @@ -339,7 +339,14 @@ var/list/cyberimps for(var/obj/item/organ/target_organ as anything in humantarget.organs) if(IS_ROBOTIC_ORGAN(target_organ) && !(target_organ.organ_flags & ORGAN_HIDDEN)) - LAZYADD(cyberimps, target_organ.examine_title(user)) + // DOPPLER ADDITION START - Shows quality for special augments from Powers + var/line = target_organ.examine_title(user) + if(istype(target_organ, /obj/item/organ/cyberimp)) + var/obj/item/organ/cyberimp/cy = target_organ + if(cy.premium) + line = "[line] (quality: [round(cy.premium.quality)]%)" + LAZYADD(cyberimps, line) + // DOPPLER ADDITION END if(target_organ.organ_flags & ORGAN_MUTANT) mutant = TRUE if(LAZYLEN(cyberimps)) @@ -347,7 +354,6 @@ render_list += "
" render_list += "Detected cybernetic modifications:
" render_list += "[english_list(cyberimps, and_text = ", and ")]
" - render_list += "
" //Genetic stability diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_action.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_action.dm new file mode 100644 index 00000000000000..499990fe66ff76 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_action.dm @@ -0,0 +1,68 @@ +// Custom actions for premium augments, meant to show the progress bar with quality wear. +/datum/action/item_action/organ_action/premium + name = "Premium Augment" + check_flags = AB_CHECK_CONSCIOUS + + var/datum/premium_augment/premium + var/mutable_appearance/quality_overlay + +/datum/action/item_action/organ_action/premium/New(Target) + ..() + var/obj/item/organ/cyberimp/organ_target = target + premium = organ_target?.premium + premium?.register_quality_action(src) + update_quality_overlay() + +/datum/action/item_action/organ_action/premium/Destroy() + premium?.unregister_quality_action(src) + return ..() + +/datum/action/item_action/organ_action/premium/Grant(mob/grant_to) + . = ..() + if(!premium) + var/obj/item/organ/cyberimp/organ_target = target + premium = organ_target?.premium + addtimer(CALLBACK(src, PROC_REF(update_quality_overlay)), 1) // Adresses a bug that the percentage is not visible at round start. + +/datum/action/item_action/organ_action/premium/IsAvailable(feedback = FALSE) + . = ..() + if(!premium) + var/obj/item/organ/cyberimp/organ_target = target + premium = organ_target?.premium + return . + +/datum/action/item_action/organ_action/premium/proc/update_quality_overlay() + var/atom/movable/ui_element = get_atom_moveable() + if(!ui_element || !premium) + return + ui_element.cut_overlay(quality_overlay) + quality_overlay = new/mutable_appearance + quality_overlay.maptext_width = 32 + quality_overlay.maptext_height = 16 + quality_overlay.maptext_x = 4 + quality_overlay.maptext_y = 0 + var/percent = clamp(round(premium.quality), 0, 100) + quality_overlay.maptext = MAPTEXT("[percent]%") + ui_element.add_overlay(quality_overlay) + build_all_button_icons(UPDATE_BUTTON_STATUS) + +/datum/action/item_action/organ_action/premium/proc/get_atom_moveable() + for(var/datum/hud/hud_instance as anything in viewers) + var/atom/movable/screen/movable/action_button/action_button_instance = viewers[hud_instance] + if(istype(action_button_instance, /atom/movable/screen/movable/action_button)) + return action_button_instance + +/datum/action/item_action/organ_action/premium/use + name = "Toggle Premium Augment" + +/datum/action/item_action/organ_action/premium/use/New(Target) + ..() + var/obj/item/organ/organ_target = target + name = "Toggle [organ_target.name]" + +/datum/action/item_action/organ_action/premium/use/do_effect(trigger_flags) + var/obj/item/organ/cyberimp/organ_target = target + if(!organ_target) + return FALSE + organ_target.use_action() + return TRUE diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm new file mode 100644 index 00000000000000..d8d261f5cc4f95 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm @@ -0,0 +1,115 @@ +// Package this along with cyberimps +/obj/item/organ/cyberimp + // The datum for premium augments that handle all the quality fuckery. + var/datum/premium_augment/premium + +/// Default premium action hook. Override per implant. +/obj/item/organ/cyberimp/proc/use_action() + return FALSE + +/datum/premium_augment + /// Host implant that owns this premium augment logic. + var/obj/item/organ/cyberimp/host + /// Current quality percentage (0..100) + var/quality = AUGMENTED_PREMIUM_QUALITY_START + /// Passive decay configuration. + var/decay_interval = AUGMENTED_DECAY_INTERVAL + var/decay_amount = AUGMENTED_DECAY_AMOUNT + var/last_decay_time = 0 + /// Actions that render the quality bar. + var/list/premium_actions + +/datum/premium_augment/New(obj/item/organ/cyberimp/_host) + host = _host + last_decay_time = world.time + START_PROCESSING(SSfastprocess, src) + +/datum/premium_augment/Destroy() + STOP_PROCESSING(SSfastprocess, src) + premium_actions = null + host = null + return ..() + +/// Whether the premium augment can function at all. +/datum/premium_augment/proc/can_function() + return quality > 0 + +/// Returns a tier label for UI or logic. +/datum/premium_augment/proc/quality_tier() + if(quality > AUGMENTED_PREMIUM_QUALITY_OPTIMAL) + return "optimal" + if(quality > AUGMENTED_PREMIUM_QUALITY_HIGH) + return "standard" + if(quality > AUGMENTED_PREMIUM_QUALITY_MEDIUM) + return "compromised" + if(quality > AUGMENTED_PREMIUM_QUALITY_LOW) + return "failing" + return "broken" + +/// Performance multiplier based purely on quality tiers. +/datum/premium_augment/proc/perf_mult() + return get_efficiency() + +/// Returns the efficiency value based on quality tiers. +/datum/premium_augment/proc/get_efficiency() + if(quality > AUGMENTED_PREMIUM_QUALITY_OPTIMAL) + return AUGMENTED_PREMIUM_EFFICIENCY_OPTIMAL + if(quality > AUGMENTED_PREMIUM_QUALITY_HIGH) + return AUGMENTED_PREMIUM_EFFICIENCY_HIGH + if(quality > AUGMENTED_PREMIUM_QUALITY_MEDIUM) + return AUGMENTED_PREMIUM_EFFICIENCY_MEDIUM + if(quality > AUGMENTED_PREMIUM_QUALITY_LOW) + return AUGMENTED_PREMIUM_EFFICIENCY_LOW + return AUGMENTED_PREMIUM_EFFICIENCY_BROKEN + +/// Adjust quality by amount, clamped to [0..AUGMENTED_PREMIUM_QUALITY_MAX] (or override). +/datum/premium_augment/proc/adjust_quality(amount, override_cap) + if(!isnum(amount)) + return + var/cap_to = isnum(override_cap) ? override_cap : AUGMENTED_PREMIUM_QUALITY_MAX + quality = clamp(quality + amount, 0, cap_to) + update_quality_actions() + +/// Passive decay processing. +/datum/premium_augment/process(seconds_per_tick) + if(decay_amount <= 0 || decay_interval <= 0) + return + if(world.time - last_decay_time < decay_interval) + return + adjust_quality(-decay_amount) + last_decay_time = world.time + +/// Register an action that should display the quality bar. +/datum/premium_augment/proc/register_quality_action(datum/action/item_action/organ_action/premium/action) + if(!action) + return + LAZYADD(premium_actions, action) + action.update_quality_overlay() + +/// Unregister a quality bar action. +/datum/premium_augment/proc/unregister_quality_action(datum/action/item_action/organ_action/premium/action) + if(!premium_actions || !action) + return + premium_actions -= action + +/// Update all registered action quality bars. +/datum/premium_augment/proc/update_quality_actions() + if(!LAZYLEN(premium_actions)) + return + for(var/datum/action/item_action/organ_action/premium/action as anything in premium_actions) + if(QDELETED(action)) + premium_actions -= action + continue + action.update_quality_overlay() + +/// Premium maintenance: restores quality up to 75%. +/datum/premium_augment/proc/apply_premium_maintenance(amount) + if(amount <= 0) + return + adjust_quality(amount, AUGMENTED_PREMIUM_QUALITY_START) + +/// Refurbish: restores quality up to 100%. +/datum/premium_augment/proc/refurbish(amount) + if(amount <= 0) + return + adjust_quality(amount, AUGMENTED_PREMIUM_QUALITY_MAX) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm new file mode 100644 index 00000000000000..9e9d2ae35c1d31 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm @@ -0,0 +1,78 @@ +/* + Blocks mental magic and scrying. Rather than adding the antimagic component we handle it here because we need to handle charge_cost in our own way (convert it to quality). +*/ +/datum/power/augmented/mental_shielding + name = "Premium TNFL Mental Shielding Implant" + desc = " Based on the nullifying effects that tinfoil has on certain magical phenomena, this dermal implant creates a protective coating around your brain.\ + \n Creates a barrier that blocks resonant based scrying, as well as mental abilities used on you (including magic stronger than Resonant).\ + \n Blocking mental abilities consumes quality, increasing consumption rate the lower the quality is." + + value = 6 + augment = /obj/item/organ/cyberimp/brain/mental_shielding + +/obj/item/organ/cyberimp/brain/mental_shielding + name = "TNFL Mental Shielding Implant" + desc = "Based on the nullifying effects that tinfoil has on certain magical phenomena, this dermal implant creates a protective coating around your brain. \ + Creates a barrier that blocks resonant based scrying, as well as mental abilities used on you (including magic stronger than Resonant). \ + Blocking mental abilities consumes quality, increasing consumption rate the lower the quality is." + icon_state = "brain_implant_connector" + slot = ORGAN_SLOT_BRAIN_CNS + actions_types = list(/datum/action/item_action/organ_action/premium/use) + + var/enabled = TRUE + + // the factor with which we multiply the final cost of anti-mental + var/mental_mult = 5 + +/obj/item/organ/cyberimp/brain/mental_shielding/Initialize(mapload) + . = ..() + if(!premium) + premium = new /datum/premium_augment(src) + +// Registers antimagic signals +/obj/item/organ/cyberimp/brain/mental_shielding/on_mob_insert(mob/living/carbon/receiver, special, movement_flags) + . = ..() + RegisterSignal(receiver, COMSIG_MOB_RECEIVE_MAGIC, PROC_REF(on_receive_magic), override = TRUE) + if(enabled) + ADD_TRAIT(receiver, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT) + +// Unregisters antimagic signals +/obj/item/organ/cyberimp/brain/mental_shielding/on_mob_remove(mob/living/carbon/owner, special, movement_flags) + . = ..() + UnregisterSignal(owner, COMSIG_MOB_RECEIVE_MAGIC) + REMOVE_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT) + +// Listener to check if it can block. Basically we just check if the quality is not 0. +// Direct hook for antimagic signals, avoids component deletion behavior. +/obj/item/organ/cyberimp/brain/mental_shielding/proc/on_receive_magic(mob/living/carbon/source, casted_magic_flags, charge_cost, list/antimagic_sources) + SIGNAL_HANDLER + if(!enabled || !premium?.can_function()) + return NONE + if(!(casted_magic_flags & MAGIC_RESISTANCE_MIND)) + return NONE + antimagic_sources += src + var/adjusted_cost = process_quality_cost(max(1, charge_cost)) + premium.adjust_quality(-adjusted_cost) + return COMPONENT_MAGIC_BLOCKED + +/// Convert an antimagic charge cost into a quality cost. +/obj/item/organ/cyberimp/brain/mental_shielding/proc/process_quality_cost(raw_cost) + if(raw_cost <= 0 || !premium) + return 0 + var/efficiency = premium?.get_efficiency() || 0 + if(efficiency <= 0) + return 0 + var/mult = (AUGMENTED_PREMIUM_EFFICIENCY_HIGH / efficiency) * mental_mult + return max(1, round(raw_cost * mult)) + +/obj/item/organ/cyberimp/brain/mental_shielding/use_action() + if(!owner) + return FALSE + enabled = !enabled + if(enabled) + ADD_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT) + to_chat(owner, span_notice("Your [name] hums as it activates.")) + else + REMOVE_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT) + to_chat(owner, span_notice("Your [name] powers down.")) + return enabled diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery.dm new file mode 100644 index 00000000000000..1f67d3a95ab665 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery.dm @@ -0,0 +1,126 @@ +/// Surgery to service premium augments and restore their maintenance quality. + +/datum/surgery/premium_augment_maintenance + name = "Premium augment maintenance" + desc = "Perform maintenance on premium augments, restoring them up to their standard operating quality." + surgery_flags = SURGERY_REQUIRE_RESTING | SURGERY_REQUIRE_LIMB | SURGERY_REQUIRES_REAL_LIMB + /// Selected premium augment to service for this surgery. + var/obj/item/organ/cyberimp/selected_premium + /// Zone used when selecting the premium augment. + var/selected_premium_zone + possible_locs = list( + BODY_ZONE_HEAD, + BODY_ZONE_CHEST, + BODY_ZONE_L_ARM, + BODY_ZONE_R_ARM, + BODY_ZONE_L_LEG, + BODY_ZONE_R_LEG, + BODY_ZONE_PRECISE_EYES, + BODY_ZONE_PRECISE_MOUTH, + BODY_ZONE_PRECISE_GROIN, + BODY_ZONE_PRECISE_L_HAND, + BODY_ZONE_PRECISE_R_HAND, + BODY_ZONE_PRECISE_L_FOOT, + BODY_ZONE_PRECISE_R_FOOT, + ) + steps = list( + /datum/surgery_step/incise, + /datum/surgery_step/retract_skin, + /datum/surgery_step/clamp_bleeders, + /datum/surgery_step/premium_augment_access, + /datum/surgery_step/premium_augment_maintenance, + /datum/surgery_step/close, + ) + +/datum/surgery/premium_augment_maintenance/can_start(mob/user, mob/living/carbon/target) + . = ..() + if(!.) + return . + var/list/premium_augments = get_premium_augments_for_zone(target, user.zone_selected) + return LAZYLEN(premium_augments) + +/datum/surgery/premium_augment_maintenance/proc/get_premium_augments_for_zone(mob/living/carbon/target, target_zone) + if(!target) + return null + var/list/organs = target.get_organs_for_zone(target_zone) + var/list/premium_augments = list() + for(var/obj/item/organ/organ as anything in organs) + var/obj/item/organ/cyberimp/implant = organ + if(istype(implant) && implant.premium) + premium_augments += implant + return premium_augments + +/datum/surgery/premium_augment_maintenance/proc/get_selected_premium(mob/user, mob/living/carbon/target, target_zone, obj/item/tool) + if(!target) + return null + + if(selected_premium && selected_premium.owner == target && selected_premium.premium && selected_premium.zone == target_zone) + return selected_premium + + selected_premium = null + selected_premium_zone = null + + var/list/premium_augments = get_premium_augments_for_zone(target, target_zone) + if(!LAZYLEN(premium_augments)) + return null + + if(LAZYLEN(premium_augments) == 1) + selected_premium = premium_augments[1] + selected_premium_zone = target_zone + return selected_premium + + var/list/options = list() + for(var/obj/item/organ/cyberimp/implant as anything in premium_augments) + var/label = implant.name + if(options[label]) + label = "[label] ([implant.type])" + options[label] = implant + + var/chosen = tgui_input_list(user, "Service which premium augment?", "Surgery", sort_list(options)) + if(isnull(chosen)) + return null + + if(!(user && target && user.Adjacent(target))) + return null + + var/obj/item/held_tool = user.get_active_held_item() + if(held_tool) + held_tool = held_tool.get_proxy_attacker_for(target, user) + if(held_tool != tool) + return null + + selected_premium = options[chosen] + if(!selected_premium || selected_premium.owner != target || !selected_premium.premium) + selected_premium = null + return null + + selected_premium_zone = target_zone + return selected_premium + +/datum/surgery/premium_augment_maintenance/mechanic + name = "Premium augment maintenance" + requires_bodypart_type = BODYTYPE_ROBOTIC + surgery_flags = SURGERY_SELF_OPERABLE | SURGERY_REQUIRE_LIMB | SURGERY_CHECK_TOOL_BEHAVIOUR + possible_locs = list( + BODY_ZONE_HEAD, + BODY_ZONE_CHEST, + BODY_ZONE_L_ARM, + BODY_ZONE_R_ARM, + BODY_ZONE_L_LEG, + BODY_ZONE_R_LEG, + BODY_ZONE_PRECISE_EYES, + BODY_ZONE_PRECISE_MOUTH, + BODY_ZONE_PRECISE_GROIN, + BODY_ZONE_PRECISE_L_HAND, + BODY_ZONE_PRECISE_R_HAND, + BODY_ZONE_PRECISE_L_FOOT, + BODY_ZONE_PRECISE_R_FOOT, + ) + steps = list( + /datum/surgery_step/mechanic_open, + /datum/surgery_step/open_hatch, + /datum/surgery_step/prepare_electronics, + /datum/surgery_step/premium_augment_access, + /datum/surgery_step/premium_augment_maintenance, + /datum/surgery_step/mechanic_close, + ) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm new file mode 100644 index 00000000000000..309be80b9f211e --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm @@ -0,0 +1,149 @@ +// Custom steps for premium augment maintenance surgery. + +// Surgery step: open access panel before servicing. +/datum/surgery_step/premium_augment_access + name = "open maintenance panel (screwdriver)" + implements = list( + TOOL_SCREWDRIVER = 100, + TOOL_SCALPEL = 75, + /obj/item/knife = 50, + /obj/item = 10) // 10% success with any sharp item. + time = 2.6 SECONDS + preop_sound = 'sound/items/tools/screwdriver.ogg' + success_sound = 'sound/items/tools/screwdriver2.ogg' + surgery_effects_mood = TRUE + +/datum/surgery_step/premium_augment_access/proc/get_premium_augments_for_zone(mob/living/carbon/target, target_zone) + if(!target) + return null + var/list/organs = target.get_organs_for_zone(target_zone) + var/list/premium_augments = list() + for(var/obj/item/organ/organ as anything in organs) + var/obj/item/organ/cyberimp/implant = organ + if(istype(implant) && implant.premium) + premium_augments += implant + return premium_augments + +/datum/surgery_step/premium_augment_access/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery) + var/obj/item/organ/cyberimp/target_implant + var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery + if(istype(premium_surgery)) + target_implant = premium_surgery.get_selected_premium(user, target, target_zone, tool) + else + var/list/premium_augments = get_premium_augments_for_zone(target, target_zone) + if(LAZYLEN(premium_augments) == 1) + target_implant = premium_augments[1] + + if(!target_implant) + if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH) + target_zone = check_zone(target_zone) + to_chat(user, span_warning("You can't find any premium augments to access in [target]'s [target.parse_zone_with_bodypart(target_zone)].")) + return SURGERY_STEP_FAIL + + if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH) + target_zone = check_zone(target_zone) + display_results( + user, + target, + span_notice("You begin opening the access panel to [target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)]..."), + span_notice("[user] begins opening an access panel in [target]'s [target.parse_zone_with_bodypart(target_zone)]."), + span_notice("[user] begins opening something inside [target]'s [target.parse_zone_with_bodypart(target_zone)]."), + ) + display_pain(target, "You feel a sharp, uncomfortable pressure in your [target.parse_zone_with_bodypart(target_zone)]!") + +/datum/surgery_step/premium_augment_access/success(mob/living/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery, default_display_results = FALSE) + var/obj/item/organ/cyberimp/target_implant + var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery + if(istype(premium_surgery)) + target_implant = premium_surgery.selected_premium + if(!target_implant || target_implant.owner != target || !target_implant.premium || target_implant.zone != target_zone) + target_implant = null + if(!target_implant) + to_chat(user, span_warning("[target] has no premium augments there to access!")) + return ..() + + if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH) + target_zone = check_zone(target_zone) + display_results( + user, + target, + span_notice("You open access to [target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)]."), + span_notice("[user] opens access to premium augment hardware in [target]'s [target.parse_zone_with_bodypart(target_zone)]."), + span_notice("[user] opens access to something inside [target]'s [target.parse_zone_with_bodypart(target_zone)]."), + ) + return ..() + +// Surgery step: perform the actual maintenance. +/datum/surgery_step/premium_augment_maintenance + name = "service premium augment (multitool)" + implements = list( + TOOL_MULTITOOL = 100, + TOOL_WIRECUTTER = 65, + ) + time = 4 SECONDS + preop_sound = 'sound/items/tools/ratchet.ogg' + success_sound = 'sound/machines/airlock/doorclick.ogg' + surgery_effects_mood = TRUE + +/datum/surgery_step/premium_augment_maintenance/proc/get_premium_augments_for_zone(mob/living/carbon/target, target_zone) + if(!target) + return null + var/list/organs = target.get_organs_for_zone(target_zone) + var/list/premium_augments = list() + for(var/obj/item/organ/organ as anything in organs) + var/obj/item/organ/cyberimp/implant = organ + if(istype(implant) && implant.premium) + premium_augments += implant + return premium_augments + +/datum/surgery_step/premium_augment_maintenance/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery) + var/obj/item/organ/cyberimp/target_implant + var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery + if(istype(premium_surgery)) + target_implant = premium_surgery.get_selected_premium(user, target, target_zone, tool) + else + var/list/premium_augments = get_premium_augments_for_zone(target, target_zone) + if(LAZYLEN(premium_augments) == 1) + target_implant = premium_augments[1] + + if(!target_implant) + if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH) + target_zone = check_zone(target_zone) + to_chat(user, span_warning("You can't find any premium augments to service in [target]'s [target.parse_zone_with_bodypart(target_zone)].")) + return SURGERY_STEP_FAIL + + if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH) + target_zone = check_zone(target_zone) + display_results( + user, + target, + span_notice("You begin servicing [target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)]..."), + span_notice("[user] begins servicing the premium augment hardware in [target]'s [target.parse_zone_with_bodypart(target_zone)]."), + span_notice("[user] begins servicing something inside [target]'s [target.parse_zone_with_bodypart(target_zone)]."), + ) + display_pain(target, "You feel a sharp, uncomfortable pressure in your [target.parse_zone_with_bodypart(target_zone)]!") + +/datum/surgery_step/premium_augment_maintenance/success(mob/living/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery, default_display_results = FALSE) + var/obj/item/organ/cyberimp/target_implant + var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery + if(istype(premium_surgery)) + target_implant = premium_surgery.selected_premium + if(!target_implant || target_implant.owner != target || !target_implant.premium || target_implant.zone != target_zone) + target_implant = null + if(!target_implant) + to_chat(user, span_warning("[target] has no premium augments there to service!")) + return ..() + + target_implant.premium.apply_premium_maintenance(AUGMENTED_PREMIUM_QUALITY_START) + + if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH) + target_zone = check_zone(target_zone) + display_results( + user, + target, + span_notice("You successfully service [target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)]."), + span_notice("[user] successfully services [target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)]."), + span_notice("[user] successfully services something inside [target]'s [target.parse_zone_with_bodypart(target_zone)]."), + ) + log_combat(user, target, "serviced premium augments in", addition="COMBAT MODE: [uppertext(user.combat_mode)]") + return ..() diff --git a/tgstation.dme b/tgstation.dme index c8b17ae3e29e05..ff7e29ce51fee9 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7431,7 +7431,12 @@ #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_action.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_augment.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\mental_shielding.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\surgery\_premium_surgery.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\surgery\_premium_surgery_steps.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_action.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\_expert_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\expert\creature_tamer.dm" From 34e21504dd149c807b72f1ce340ace6ed9936ac4 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Mar 2026 15:49:18 +0100 Subject: [PATCH 122/212] Add refurbishing. Early commit to send to Ephe. --- code/__DEFINES/~doppler_defines/powers.dm | 6 + .../internal/cyberimp/augments_internal.dm | 12 ++ .../mortal/augmented/_premium_augment.dm | 117 ++++++++++++++++-- .../mortal/augmented/mental_shielding.dm | 3 + 4 files changed, 129 insertions(+), 9 deletions(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 463acf96fa8f36..f724ef2c1fab2c 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -308,5 +308,11 @@ #define AUGMENTED_PREMIUM_EFFICIENCY_LOW 0.6 #define AUGMENTED_PREMIUM_EFFICIENCY_BROKEN 0 +// Refurbish steps +#define AUGMENTED_REFURBISH_OPEN "open" +#define AUGMENTED_REFURBISH_PARTS "parts" +#define AUGMENTED_REFURBISH_CALIBRATE "calibrate" +#define AUGMENTED_REFURBISH_CLOSE "close" + // Used for the prefs to shorthand tell there's nothing in the right or left arm augment slot. #define AUGMENTED_NO_AUGMENT "None" diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm index 71ab09f5b22b72..6d8fc0caa1be41 100644 --- a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm +++ b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm @@ -14,6 +14,7 @@ /// Bodypart overlay we're going to apply to whoever we're implanted into var/datum/bodypart_overlay/augment/bodypart_aug = null + var/datum/premium_augment/premium // DOPPLER EDIT ADDITION - Allows Premium augments to hook into cyberimp and still be semi-modular /obj/item/organ/cyberimp/Initialize(mapload) . = ..() if (aug_overlay) @@ -23,6 +24,17 @@ QDEL_NULL(bodypart_aug) return ..() +// DOPPLER ADDITION START - Handles powers related additions +/// Default premium action hook. Override per implant. +/obj/item/organ/cyberimp/proc/use_action() + return FALSE +// Handles refurbishing +/obj/item/organ/cyberimp/attackby(obj/item/tool, mob/user, params) + if(premium && premium.handle_refurbish_interaction(user, tool, src)) + return + return ..() +// DOPPLER ADDITION END + /obj/item/organ/cyberimp/proc/get_overlay_state() return aug_overlay diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm index d8d261f5cc4f95..181854cfc010fb 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm @@ -1,12 +1,5 @@ -// Package this along with cyberimps -/obj/item/organ/cyberimp - // The datum for premium augments that handle all the quality fuckery. - var/datum/premium_augment/premium - -/// Default premium action hook. Override per implant. -/obj/item/organ/cyberimp/proc/use_action() - return FALSE - +// +// Responsible for handling most of the premium augment interactions away from the base cyberimplant. /datum/premium_augment /// Host implant that owns this premium augment logic. var/obj/item/organ/cyberimp/host @@ -18,6 +11,21 @@ var/last_decay_time = 0 /// Actions that render the quality bar. var/list/premium_actions + /// Refurbish flow state. + var/refurb_stage = 1 + /// Sequence of refurb steps. Override per-augment for customization. + var/list/refurb_sequence = list( + AUGMENTED_REFURBISH_OPEN, + AUGMENTED_REFURBISH_PARTS, + AUGMENTED_REFURBISH_CALIBRATE, + AUGMENTED_REFURBISH_CLOSE, + ) + /// Parts required during refurb. Override per-augment for customization. + var/list/refurb_parts = list( + /obj/item/stack/sheet/iron = 2, + /obj/item/stack/cable_coil = 1, + ) + var/list/refurb_parts_remaining /datum/premium_augment/New(obj/item/organ/cyberimp/_host) host = _host @@ -113,3 +121,94 @@ if(amount <= 0) return adjust_quality(amount, AUGMENTED_PREMIUM_QUALITY_MAX) + +/// Handle refurbish interactions while the implant is out of the body. +/datum/premium_augment/proc/handle_refurbish_interaction(mob/user, obj/item/tool, obj/item/organ/cyberimp/implant) + if(!user || !tool || !implant) + return FALSE + if(implant.owner) + to_chat(user, span_warning("You need to remove [implant] before refurbishing it.")) + return TRUE + var/step = get_refurb_step() + if(!step) + return FALSE + + switch(step) + if(AUGMENTED_REFURBISH_OPEN) + if(tool.tool_behaviour != TOOL_SCREWDRIVER) + to_chat(user, span_warning("You need a screwdriver to open [implant]'s casing.")) + return TRUE + to_chat(user, span_notice("You open [implant]'s casing.")) + advance_refurb_step() + return TRUE + + if(AUGMENTED_REFURBISH_PARTS) + ensure_refurb_parts() + if(!istype(tool, /obj/item/stack)) + to_chat(user, span_warning("You need spare parts to refurbish [implant].")) + return TRUE + var/obj/item/stack/stack = tool + var/typepath = stack.type + var/needed = refurb_parts_remaining[typepath] + if(!needed) + to_chat(user, span_warning("[stack] doesn't fit [implant]'s parts.")) + return TRUE + var/available = stack.amount + if(available <= 0) + to_chat(user, span_warning("[stack] doesn't have anything left to use.")) + return TRUE + var/use_amount = min(needed, available) + if(!stack.use(use_amount)) + to_chat(user, span_warning("You need more [stack] to continue.")) + return TRUE + needed -= use_amount + if(needed <= 0) + refurb_parts_remaining -= typepath + else + refurb_parts_remaining[typepath] = needed + to_chat(user, span_notice("You replace worn parts inside [implant].")) + if(!LAZYLEN(refurb_parts_remaining)) + advance_refurb_step() + return TRUE + + if(AUGMENTED_REFURBISH_CALIBRATE) + if(tool.tool_behaviour != TOOL_MULTITOOL) + to_chat(user, span_warning("You need a multitool to calibrate [implant].")) + return TRUE + to_chat(user, span_notice("You calibrate [implant]'s diagnostics.")) + advance_refurb_step() + return TRUE + + if(AUGMENTED_REFURBISH_CLOSE) + if(tool.tool_behaviour != TOOL_SCREWDRIVER) + to_chat(user, span_warning("You need a screwdriver to close [implant]'s casing.")) + return TRUE + refurbish(AUGMENTED_PREMIUM_QUALITY_MAX) + reset_refurb() + to_chat(user, span_notice("You finish refurbishing [implant]. It looks factory-new.")) + return TRUE + + return FALSE + +/datum/premium_augment/proc/get_refurb_step() + if(!LAZYLEN(refurb_sequence)) + return null + refurb_stage = clamp(refurb_stage, 1, refurb_sequence.len) + return refurb_sequence[refurb_stage] + +/datum/premium_augment/proc/advance_refurb_step() + refurb_stage++ + refurb_parts_remaining = null + if(refurb_stage > refurb_sequence.len) + refurb_stage = refurb_sequence.len + +/datum/premium_augment/proc/reset_refurb() + refurb_stage = 1 + refurb_parts_remaining = null + +/datum/premium_augment/proc/ensure_refurb_parts() + if(refurb_parts_remaining) + return + refurb_parts_remaining = list() + for(var/typepath in refurb_parts) + refurb_parts_remaining[typepath] = refurb_parts[typepath] diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm index 9e9d2ae35c1d31..504e7d5a7a0807 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 @@ -28,6 +28,9 @@ . = ..() if(!premium) premium = new /datum/premium_augment(src) + premium.refurb_parts = list( + /obj/item/stack/sheet/iron = 3, + /obj/item/stack/cable_coil = 2) // Registers antimagic signals /obj/item/organ/cyberimp/brain/mental_shielding/on_mob_insert(mob/living/carbon/receiver, special, movement_flags) From 6ade1dcb7cf8f32014dd6348570e544d4ba5e7e1 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Mar 2026 16:48:23 +0100 Subject: [PATCH 123/212] Finalized Premium Augments. Still needs audio/feedback polish. --- .../items/devices/scanners/health_analyzer.dm | 4 +- .../internal/cyberimp/augments_internal.dm | 24 ++++-- .../mortal/augmented/_premium_action.dm | 20 ++--- .../mortal/augmented/_premium_augment.dm | 81 ++++++++++++++----- .../mortal/augmented/mental_shielding.dm | 15 ++-- .../surgery/_premium_surgery_steps.dm | 2 +- 6 files changed, 95 insertions(+), 51 deletions(-) diff --git a/code/game/objects/items/devices/scanners/health_analyzer.dm b/code/game/objects/items/devices/scanners/health_analyzer.dm index 4a018f093cf58f..eb285c0312dc01 100644 --- a/code/game/objects/items/devices/scanners/health_analyzer.dm +++ b/code/game/objects/items/devices/scanners/health_analyzer.dm @@ -343,8 +343,8 @@ var/line = target_organ.examine_title(user) if(istype(target_organ, /obj/item/organ/cyberimp)) var/obj/item/organ/cyberimp/cy = target_organ - if(cy.premium) - line = "[line] (quality: [round(cy.premium.quality)]%)" + if(cy.premium_component) + line = "[line] (quality: [round(cy.premium_component.quality)]%)" LAZYADD(cyberimps, line) // DOPPLER ADDITION END if(target_organ.organ_flags & ORGAN_MUTANT) diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm index 6d8fc0caa1be41..37eba9260ee4e5 100644 --- a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm +++ b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm @@ -14,27 +14,35 @@ /// Bodypart overlay we're going to apply to whoever we're implanted into var/datum/bodypart_overlay/augment/bodypart_aug = null - var/datum/premium_augment/premium // DOPPLER EDIT ADDITION - Allows Premium augments to hook into cyberimp and still be semi-modular + var/premium = FALSE // DOPPLER EDIT ADDITION - Allows Premium augments to hook into cyberimp and still be semi-modular + var/datum/component/premium_augment/premium_component // DOPPLER EDIT ADDITION - Component for quality mechanics. + +// DOPPLER ADDITION START - Handles powers related additions /obj/item/organ/cyberimp/Initialize(mapload) . = ..() if (aug_overlay) bodypart_aug = new(src) - -/obj/item/organ/cyberimp/Destroy() - QDEL_NULL(bodypart_aug) - return ..() - -// DOPPLER ADDITION START - Handles powers related additions + if(premium && !premium_component) + premium_component = AddComponent(/datum/component/premium_augment) /// Default premium action hook. Override per implant. /obj/item/organ/cyberimp/proc/use_action() return FALSE // Handles refurbishing /obj/item/organ/cyberimp/attackby(obj/item/tool, mob/user, params) - if(premium && premium.handle_refurbish_interaction(user, tool, src)) + if(premium_component && premium_component.handle_refurbish_interaction(user, tool, src)) return return ..() +// Extra details for examining premium parts. +/obj/item/organ/cyberimp/examine(mob/user) + . = ..() + if(premium_component) + . += premium_component.get_refurb_examine_lines(src) // DOPPLER ADDITION END +/obj/item/organ/cyberimp/Destroy() + QDEL_NULL(bodypart_aug) + return ..() + /obj/item/organ/cyberimp/proc/get_overlay_state() return aug_overlay 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 499990fe66ff76..ddeeb5687e1d0e 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 @@ -3,37 +3,37 @@ name = "Premium Augment" check_flags = AB_CHECK_CONSCIOUS - var/datum/premium_augment/premium + var/datum/component/premium_augment/premium_component var/mutable_appearance/quality_overlay /datum/action/item_action/organ_action/premium/New(Target) ..() var/obj/item/organ/cyberimp/organ_target = target - premium = organ_target?.premium - premium?.register_quality_action(src) + premium_component = organ_target?.premium_component + premium_component?.register_quality_action(src) update_quality_overlay() /datum/action/item_action/organ_action/premium/Destroy() - premium?.unregister_quality_action(src) + premium_component?.unregister_quality_action(src) return ..() /datum/action/item_action/organ_action/premium/Grant(mob/grant_to) . = ..() - if(!premium) + if(!premium_component) var/obj/item/organ/cyberimp/organ_target = target - premium = organ_target?.premium + premium_component = organ_target?.premium_component addtimer(CALLBACK(src, PROC_REF(update_quality_overlay)), 1) // Adresses a bug that the percentage is not visible at round start. /datum/action/item_action/organ_action/premium/IsAvailable(feedback = FALSE) . = ..() - if(!premium) + if(!premium_component) var/obj/item/organ/cyberimp/organ_target = target - premium = organ_target?.premium + premium_component = organ_target?.premium_component return . /datum/action/item_action/organ_action/premium/proc/update_quality_overlay() var/atom/movable/ui_element = get_atom_moveable() - if(!ui_element || !premium) + if(!ui_element || !premium_component) return ui_element.cut_overlay(quality_overlay) quality_overlay = new/mutable_appearance @@ -41,7 +41,7 @@ quality_overlay.maptext_height = 16 quality_overlay.maptext_x = 4 quality_overlay.maptext_y = 0 - var/percent = clamp(round(premium.quality), 0, 100) + var/percent = clamp(round(premium_component.quality), 0, 100) quality_overlay.maptext = MAPTEXT("[percent]%") ui_element.add_overlay(quality_overlay) build_all_button_icons(UPDATE_BUTTON_STATUS) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm index 181854cfc010fb..625eb7081c9f7d 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm @@ -1,6 +1,6 @@ -// // Responsible for handling most of the premium augment interactions away from the base cyberimplant. -/datum/premium_augment +/datum/component/premium_augment + dupe_mode = COMPONENT_DUPE_UNIQUE /// Host implant that owns this premium augment logic. var/obj/item/organ/cyberimp/host /// Current quality percentage (0..100) @@ -27,23 +27,27 @@ ) var/list/refurb_parts_remaining -/datum/premium_augment/New(obj/item/organ/cyberimp/_host) - host = _host +/datum/component/premium_augment/Initialize() + if(!istype(parent, /obj/item/organ/cyberimp)) + return COMPONENT_INCOMPATIBLE + host = parent + if(!host.premium_component) + host.premium_component = src last_decay_time = world.time START_PROCESSING(SSfastprocess, src) -/datum/premium_augment/Destroy() +/datum/component/premium_augment/Destroy() STOP_PROCESSING(SSfastprocess, src) premium_actions = null host = null return ..() /// Whether the premium augment can function at all. -/datum/premium_augment/proc/can_function() +/datum/component/premium_augment/proc/can_function() return quality > 0 /// Returns a tier label for UI or logic. -/datum/premium_augment/proc/quality_tier() +/datum/component/premium_augment/proc/quality_tier() if(quality > AUGMENTED_PREMIUM_QUALITY_OPTIMAL) return "optimal" if(quality > AUGMENTED_PREMIUM_QUALITY_HIGH) @@ -55,11 +59,11 @@ return "broken" /// Performance multiplier based purely on quality tiers. -/datum/premium_augment/proc/perf_mult() +/datum/component/premium_augment/proc/perf_mult() return get_efficiency() /// Returns the efficiency value based on quality tiers. -/datum/premium_augment/proc/get_efficiency() +/datum/component/premium_augment/proc/get_efficiency() if(quality > AUGMENTED_PREMIUM_QUALITY_OPTIMAL) return AUGMENTED_PREMIUM_EFFICIENCY_OPTIMAL if(quality > AUGMENTED_PREMIUM_QUALITY_HIGH) @@ -71,7 +75,7 @@ return AUGMENTED_PREMIUM_EFFICIENCY_BROKEN /// Adjust quality by amount, clamped to [0..AUGMENTED_PREMIUM_QUALITY_MAX] (or override). -/datum/premium_augment/proc/adjust_quality(amount, override_cap) +/datum/component/premium_augment/proc/adjust_quality(amount, override_cap) if(!isnum(amount)) return var/cap_to = isnum(override_cap) ? override_cap : AUGMENTED_PREMIUM_QUALITY_MAX @@ -79,7 +83,7 @@ update_quality_actions() /// Passive decay processing. -/datum/premium_augment/process(seconds_per_tick) +/datum/component/premium_augment/process(seconds_per_tick) if(decay_amount <= 0 || decay_interval <= 0) return if(world.time - last_decay_time < decay_interval) @@ -88,20 +92,20 @@ last_decay_time = world.time /// Register an action that should display the quality bar. -/datum/premium_augment/proc/register_quality_action(datum/action/item_action/organ_action/premium/action) +/datum/component/premium_augment/proc/register_quality_action(datum/action/item_action/organ_action/premium/action) if(!action) return LAZYADD(premium_actions, action) action.update_quality_overlay() /// Unregister a quality bar action. -/datum/premium_augment/proc/unregister_quality_action(datum/action/item_action/organ_action/premium/action) +/datum/component/premium_augment/proc/unregister_quality_action(datum/action/item_action/organ_action/premium/action) if(!premium_actions || !action) return premium_actions -= action /// Update all registered action quality bars. -/datum/premium_augment/proc/update_quality_actions() +/datum/component/premium_augment/proc/update_quality_actions() if(!LAZYLEN(premium_actions)) return for(var/datum/action/item_action/organ_action/premium/action as anything in premium_actions) @@ -111,19 +115,19 @@ action.update_quality_overlay() /// Premium maintenance: restores quality up to 75%. -/datum/premium_augment/proc/apply_premium_maintenance(amount) +/datum/component/premium_augment/proc/apply_premium_maintenance(amount) if(amount <= 0) return adjust_quality(amount, AUGMENTED_PREMIUM_QUALITY_START) /// Refurbish: restores quality up to 100%. -/datum/premium_augment/proc/refurbish(amount) +/datum/component/premium_augment/proc/refurbish(amount) if(amount <= 0) return adjust_quality(amount, AUGMENTED_PREMIUM_QUALITY_MAX) /// Handle refurbish interactions while the implant is out of the body. -/datum/premium_augment/proc/handle_refurbish_interaction(mob/user, obj/item/tool, obj/item/organ/cyberimp/implant) +/datum/component/premium_augment/proc/handle_refurbish_interaction(mob/user, obj/item/tool, obj/item/organ/cyberimp/implant) if(!user || !tool || !implant) return FALSE if(implant.owner) @@ -148,7 +152,7 @@ to_chat(user, span_warning("You need spare parts to refurbish [implant].")) return TRUE var/obj/item/stack/stack = tool - var/typepath = stack.type + var/typepath = stack.merge_type ? stack.merge_type : stack.type var/needed = refurb_parts_remaining[typepath] if(!needed) to_chat(user, span_warning("[stack] doesn't fit [implant]'s parts.")) @@ -190,23 +194,56 @@ return FALSE -/datum/premium_augment/proc/get_refurb_step() +/// Returns lines to show when examining a premium augment for refurbishing. +/datum/component/premium_augment/proc/get_refurb_examine_lines(obj/item/organ/cyberimp/implant) + var/list/lines = list() + if(!implant) + return lines + lines += span_notice("Premium quality: [round(quality)]%.") + if(implant.owner) + lines += span_warning("Remove [implant] before refurbishing it.") + return lines + + var/step = get_refurb_step() + if(!step) + return lines + + switch(step) + if(AUGMENTED_REFURBISH_OPEN) + lines += span_notice("Refurbish step: Open the casing with a screwdriver.") + if(AUGMENTED_REFURBISH_PARTS) + ensure_refurb_parts() + if(!LAZYLEN(refurb_parts_remaining)) + lines += span_notice("Refurbish step: Parts replaced. This isn't meant to show! Why is it not telling you to use a multitool?! PANIC!") + else + lines += span_notice("Refurbish step: Replace worn parts.") + for(var/typepath in refurb_parts_remaining) + var/amount = refurb_parts_remaining[typepath] + var/display_name = initial(typepath:name) + lines += span_notice(" - [display_name] x[amount]") + if(AUGMENTED_REFURBISH_CALIBRATE) + lines += span_notice("Refurbish step: Calibrate diagnostics with a multitool.") + if(AUGMENTED_REFURBISH_CLOSE) + lines += span_notice("Refurbish step: Close the casing with a screwdriver to finish.") + return lines + +/datum/component/premium_augment/proc/get_refurb_step() if(!LAZYLEN(refurb_sequence)) return null refurb_stage = clamp(refurb_stage, 1, refurb_sequence.len) return refurb_sequence[refurb_stage] -/datum/premium_augment/proc/advance_refurb_step() +/datum/component/premium_augment/proc/advance_refurb_step() refurb_stage++ refurb_parts_remaining = null if(refurb_stage > refurb_sequence.len) refurb_stage = refurb_sequence.len -/datum/premium_augment/proc/reset_refurb() +/datum/component/premium_augment/proc/reset_refurb() refurb_stage = 1 refurb_parts_remaining = null -/datum/premium_augment/proc/ensure_refurb_parts() +/datum/component/premium_augment/proc/ensure_refurb_parts() if(refurb_parts_remaining) return refurb_parts_remaining = list() 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 504e7d5a7a0807..6489337c60969c 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 @@ -18,7 +18,7 @@ icon_state = "brain_implant_connector" slot = ORGAN_SLOT_BRAIN_CNS actions_types = list(/datum/action/item_action/organ_action/premium/use) - + premium = TRUE var/enabled = TRUE // the factor with which we multiply the final cost of anti-mental @@ -26,9 +26,8 @@ /obj/item/organ/cyberimp/brain/mental_shielding/Initialize(mapload) . = ..() - if(!premium) - premium = new /datum/premium_augment(src) - premium.refurb_parts = list( + if(premium_component) + premium_component.refurb_parts = list( /obj/item/stack/sheet/iron = 3, /obj/item/stack/cable_coil = 2) @@ -49,20 +48,20 @@ // Direct hook for antimagic signals, avoids component deletion behavior. /obj/item/organ/cyberimp/brain/mental_shielding/proc/on_receive_magic(mob/living/carbon/source, casted_magic_flags, charge_cost, list/antimagic_sources) SIGNAL_HANDLER - if(!enabled || !premium?.can_function()) + if(!enabled || !premium_component?.can_function()) return NONE if(!(casted_magic_flags & MAGIC_RESISTANCE_MIND)) return NONE antimagic_sources += src var/adjusted_cost = process_quality_cost(max(1, charge_cost)) - premium.adjust_quality(-adjusted_cost) + premium_component.adjust_quality(-adjusted_cost) return COMPONENT_MAGIC_BLOCKED /// Convert an antimagic charge cost into a quality cost. /obj/item/organ/cyberimp/brain/mental_shielding/proc/process_quality_cost(raw_cost) - if(raw_cost <= 0 || !premium) + if(raw_cost <= 0 || !premium_component) return 0 - var/efficiency = premium?.get_efficiency() || 0 + var/efficiency = premium_component.get_efficiency() || 0 if(efficiency <= 0) return 0 var/mult = (AUGMENTED_PREMIUM_EFFICIENCY_HIGH / efficiency) * mental_mult diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm index 309be80b9f211e..125afe5427a5b2 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm @@ -134,7 +134,7 @@ to_chat(user, span_warning("[target] has no premium augments there to service!")) return ..() - target_implant.premium.apply_premium_maintenance(AUGMENTED_PREMIUM_QUALITY_START) + target_implant.premium_component?.apply_premium_maintenance(AUGMENTED_PREMIUM_QUALITY_START) if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH) target_zone = check_zone(target_zone) From 569b65fff10134da1a1733ec2fb2d9beb437b7a2 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 7 Mar 2026 21:34:22 +0100 Subject: [PATCH 124/212] Adds summonable. Which as the name implies, lets you summon people by saying a cool word. --- .../powers/resonant/aberrant/summonable.dm | 303 ++++++++++++++++++ tgstation.dme | 1 + .../powers/summonable_keyword.tsx | 7 + .../powers/summonable_rune_color.tsx | 7 + 4 files changed, 318 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_keyword.tsx create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_rune_color.tsx diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm new file mode 100644 index 00000000000000..9153f3fda79bc2 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm @@ -0,0 +1,303 @@ +/* + You can be summoned by speaking a specific keywords. +*/ +/datum/power/aberrant/summonable + name = "Summonable" + desc = "By speaking a specific name or word, you appear next to the speaker after a short delay. The summoning takes time, you are stunned throughout, is entirely involuntary and can only be stopped by being buckled or dispelled.\ + \n After being succesfuly summoned, you are unable to be summoned again for 1 minute. \ + \n The chosen word is a partial secret; the Security Records on your powers contain the word as well. It cannot contain any special characters, only standard letters and numbers." + value = 7 + + required_powers = list(/datum/power/aberrant_root/anomalous) + + var/datum/component/beetlejuice/summonable/summon_component + var/owns_summon_component = FALSE + +// Adds the custom beetlejuice component and sets the beetlejuiec word. +/datum/power/aberrant/summonable/post_add() + if(!power_holder) + return + + var/mob/living/holder = power_holder + var/datum/component/beetlejuice/summonable/component = holder.GetComponent(/datum/component/beetlejuice/summonable) + if(!component) + component = holder.AddComponent(/datum/component/beetlejuice/summonable) + owns_summon_component = TRUE + + summon_component = component + + var/keyword = holder.client?.prefs?.read_preference(/datum/preference/text/summonable_keyword) + if(!keyword) + var/datum/preference/text/summonable_keyword/pref_entry = GLOB.preference_entries[/datum/preference/text/summonable_keyword] + keyword = pref_entry?.create_default_value() || "Beetlejuice" + + component.keyword = keyword + component.update_regex() + component.rune_color = holder.client?.prefs?.read_preference(/datum/preference/color/summonable_rune_color) || component.rune_color + + . = ..() + +/datum/power/aberrant/summonable/remove() + . = ..() + if(owns_summon_component && summon_component) + QDEL_NULL(summon_component) + owns_summon_component = FALSE + +// Custom beetlejuice component for Summonable. +/datum/component/beetlejuice/summonable + min_count = 1 + cooldown = 60 SECONDS // for the love of god don't make this shorter than 10 seconds you will break things. + var/summon_delay = 1 SECONDS + var/float_time = 3.5 SECONDS + var/rune_orbit_radius = 30 + var/rune_rotation_speed = 30 + var/rune_count = 8 + var/rune_spawn_interval = 3.4 + var/rune_fade_time = 6 + var/rune_color = "#ff2a2a" + + // These below are there to allow us to be dispelled and end the teleporation without brekaing everything. It is so EXCEEDINGLY MESSY. + var/summoning = FALSE + var/beaming_up = FALSE + var/list/obj/effect/summonable_rune_orbiter/current_runes + +// Custom apport because frankly put its cooler this way. +/datum/component/beetlejuice/summonable/apport(atom/target) + var/atom/movable/summoned = parent + if(ismob(summoned)) + var/mob/living/living_summoned = summoned + if(living_summoned.buckled) + return + var/turf/target_turf = get_adjacent_open_turf(target) + if(QDELETED(summoned) || !target_turf) + return + active = FALSE + addtimer(VARSET_CALLBACK(src, active, TRUE), cooldown) + addtimer(CALLBACK(src, PROC_REF(begin_summon), summoned, target_turf), summon_delay) + +// Gets a valid nearby turf within the mob's area. +/datum/component/beetlejuice/summonable/proc/get_adjacent_open_turf(atom/target) + var/turf/center = get_turf(target) + if(!center) + return null + var/list/candidates = list() + for(var/turf/T in orange(1, center)) + if(T == center) + continue + if(T.is_blocked_turf(exclude_mobs = FALSE, ignore_atoms = list(/obj/structure/table), type_list = TRUE)) + continue + candidates += T + if(!length(candidates)) + return null + return pick(candidates) + +// Starts the timers and starts manifesting effects. +/datum/component/beetlejuice/summonable/proc/begin_summon(atom/movable/summoned, turf/target_turf) + if(QDELETED(summoned) || QDELETED(target_turf)) + return + summoning = TRUE + beaming_up = TRUE + // Start departure immediately while runes are appearing. + var/turf/origin_turf = get_turf(summoned) + var/obj/effect/temp_visual/spotlight/summonable/origin_spotlight = origin_turf ? new(origin_turf, rune_color) : null + + var/old_alpha = summoned.alpha + var/old_pixel_y = summoned.pixel_y + + RegisterSignal(summoned, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + summoned.anchored = TRUE + ADD_TRAIT(summoned, TRAIT_IMMOBILIZED, "summonable_apport") + // Keep them standing but unable to act; float without full levitation. + ADD_TRAIT(summoned, TRAIT_MOVE_FLOATING, "summonable_apport") + + // Depart: float up and fade out at the origin. + animate(summoned, alpha = 0, pixel_y = old_pixel_y + 32, time = float_time) + addtimer(CALLBACK(src, PROC_REF(clear_origin_spotlight), origin_spotlight), float_time) + + var/list/obj/effect/summonable_rune_orbiter/runes = list() + current_runes = runes + addtimer(CALLBACK(src, PROC_REF(spawn_rune_sequence), summoned, target_turf, runes, 1, old_alpha, old_pixel_y), 0) + +/datum/component/beetlejuice/summonable/proc/clear_origin_spotlight(obj/effect/temp_visual/spotlight/summonable/origin_spotlight) + QDEL_NULL(origin_spotlight) + +// Creates the cool floaty runes +/datum/component/beetlejuice/summonable/proc/spawn_rune_sequence(atom/movable/summoned, turf/target_turf, list/obj/effect/summonable_rune_orbiter/runes, rune_index, old_alpha, old_pixel_y) + if(!summoning) + QDEL_LIST(runes) + return + if(QDELETED(summoned) || QDELETED(target_turf)) + QDEL_LIST(runes) + return + if(rune_index > rune_count) + begin_arrival(summoned, target_turf, runes, old_alpha, old_pixel_y) + return + + var/obj/effect/summonable_rune_orbiter/rune = new(target_turf, rune_color) + rune.orbit(target_turf, rune_orbit_radius, rotation_speed = rune_rotation_speed, rotation_segments = rune_count, pre_rotation = FALSE) + runes += rune + + addtimer(CALLBACK(src, PROC_REF(spawn_rune_sequence), summoned, target_turf, runes, rune_index + 1, old_alpha, old_pixel_y), rune_spawn_interval) + +// BEGINS THE RAPTURE +/datum/component/beetlejuice/summonable/proc/begin_arrival(atom/movable/summoned, turf/target_turf, list/obj/effect/summonable_rune_orbiter/runes, old_alpha, old_pixel_y) + if(!summoning) + QDEL_LIST(runes) + return + if(QDELETED(summoned) || QDELETED(target_turf)) + QDEL_LIST(runes) + return + beaming_up = FALSE + + var/obj/effect/temp_visual/spotlight/summonable/spotlight = new(target_turf, rune_color) + playsound(target_turf, 'sound/effects/magic/voidblink.ogg', 50, TRUE) + fade_and_clear_runes(runes) + + summoned.forceMove(target_turf) + summoned.alpha = 0 + summoned.pixel_y = 32 + animate(summoned, alpha = old_alpha, pixel_y = old_pixel_y, time = float_time) + + addtimer(CALLBACK(src, PROC_REF(finish_summon), summoned, target_turf, old_alpha, old_pixel_y, spotlight), float_time) + +// Fade and clear the runes. +/datum/component/beetlejuice/summonable/proc/fade_and_clear_runes(list/obj/effect/summonable_rune_orbiter/runes) + for(var/obj/effect/summonable_rune_orbiter/rune in runes) + animate(rune, alpha = 0, time = rune_fade_time) + addtimer(CALLBACK(src, PROC_REF(clear_runes), runes), rune_fade_time) + +/datum/component/beetlejuice/summonable/proc/clear_runes(list/obj/effect/summonable_rune_orbiter/runes) + QDEL_LIST(runes) + +// Alright, shows over, he's here now. Tiem to pack up and go. +/datum/component/beetlejuice/summonable/proc/finish_summon(atom/movable/summoned, turf/target_turf, old_alpha, old_pixel_y, obj/effect/temp_visual/spotlight/summonable/spotlight) + if(QDELETED(summoned)) + QDEL_NULL(spotlight) + return + + summoned.alpha = old_alpha + summoned.pixel_y = old_pixel_y + summoned.anchored = FALSE + REMOVE_TRAIT(summoned, TRAIT_IMMOBILIZED, "summonable_apport") + REMOVE_TRAIT(summoned, TRAIT_MOVE_FLOATING, "summonable_apport") + if(target_turf) + summoned.forceMove(target_turf) + // Explicitly trigger glass table break checks on landing. This isn't clean, but its too funny to not have it. + if(isliving(summoned)) + var/mob/living/living_summoned = summoned + var/obj/structure/table/glass/glass_table = locate(/obj/structure/table/glass) in get_turf(living_summoned) + if(glass_table) + glass_table.check_break(living_summoned) + + QDEL_NULL(spotlight) + UnregisterSignal(summoned, COMSIG_ATOM_DISPEL) + summoning = FALSE + beaming_up = FALSE + current_runes = null + active = FALSE + addtimer(VARSET_CALLBACK(src, active, TRUE), cooldown) + +/datum/component/beetlejuice/summonable/proc/on_dispel(atom/movable/target, atom/dispeller) + SIGNAL_HANDLER + // Only cancel if they're currently being beamed up. + if(!beaming_up || !summoning) + return NONE + cancel_summon(target) + return DISPEL_RESULT_DISPELLED + +/datum/component/beetlejuice/summonable/proc/cancel_summon(atom/movable/summoned) + if(summoned) + summoned.alpha = initial(summoned.alpha) + summoned.pixel_y = initial(summoned.pixel_y) + summoned.anchored = FALSE + REMOVE_TRAIT(summoned, TRAIT_IMMOBILIZED, "summonable_apport") + REMOVE_TRAIT(summoned, TRAIT_MOVE_FLOATING, "summonable_apport") + UnregisterSignal(summoned, COMSIG_ATOM_DISPEL) + if(current_runes) + QDEL_LIST(current_runes) + current_runes = null + summoning = FALSE + beaming_up = FALSE + +// Preference choice for Summonable keyword selection. +/datum/preference/text/summonable_keyword + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "summonable_keyword" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + maximum_value_length = 32 + +/datum/preference/text/summonable_keyword/create_default_value() + return "Beetlejuice" + +/datum/preference/text/summonable_keyword/is_valid(value) + if(!istext(value)) + return FALSE + if(length(value) < 1 || length(value) >= maximum_value_length) + return FALSE + // Allow only ASCII letters and numbers. + var/quoted = REGEX_QUOTE(value) + var/static/regex/allowed_regex = regex("^" + ascii2text(91) + "A-Za-z0-9" + ascii2text(93) + "+$") + allowed_regex.next = 1 + return !!allowed_regex.Find(quoted) + +/datum/preference/text/summonable_keyword/deserialize(input, datum/preferences/preferences) + var/value = ..() + if(!is_valid(value)) + return null + return value + +/datum/preference/text/summonable_keyword/apply_to_human(mob/living/carbon/human/target, value) + return + +// Preference choice for Summonable rune/spotlight color. +/datum/preference/color/summonable_rune_color + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "summonable_rune_color" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/color/summonable_rune_color/create_default_value() + return "ff2a2a" + +/datum/preference/color/summonable_rune_color/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + return TRUE + +/datum/preference/color/summonable_rune_color/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/power_constant_data/summonable + associated_typepath = /datum/power/aberrant/summonable + customization_options = list(/datum/preference/text/summonable_keyword, /datum/preference/color/summonable_rune_color) + +// Orbiting rune for Summonable arrival. +/obj/effect/summonable_rune_orbiter + icon = 'icons/effects/eldritch.dmi' + icon_state = "small_rune_1" + layer = BELOW_MOB_LAYER + anchored = TRUE + mouse_opacity = 0 + +// We set the specific icons because we don't want the color shifting. Beyond that, colors! +/obj/effect/summonable_rune_orbiter/Initialize(mapload, rune_color = "#ff2a2a") + var/rune_state = "small_rune_[rand(1, 10)]" + var/icon/rune_icon = icon('icons/effects/eldritch.dmi', rune_state, frame = 1) + // Force the base green to a greyscale color. + rune_icon.MapColors(0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33, 0.33) + // Boost brightness before applying the chosen color. + rune_icon.Blend(rgb(160, 160, 160), ICON_ADD) + // Apply the color from prefs. + rune_icon.Blend(rune_color, ICON_MULTIPLY) + icon = rune_icon + icon_state = null + return ..() + +// Green spotlight at the destination. +/obj/effect/temp_visual/spotlight/summonable + color = COLOR_RED + duration = 3 SECONDS + +/obj/effect/temp_visual/spotlight/summonable/Initialize(mapload, spotlight_color = COLOR_RED) + color = spotlight_color + return ..() diff --git a/tgstation.dme b/tgstation.dme index ff7e29ce51fee9..9905813da10182 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7485,6 +7485,7 @@ #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\vent_crawl.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" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_keyword.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_keyword.tsx new file mode 100644 index 00000000000000..6701936f8a7dff --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_keyword.tsx @@ -0,0 +1,7 @@ +import { Feature, FeatureShortTextInput } from '../../base'; + +export const summonable_keyword: Feature = { + name: 'Summonable Keyword', + description: 'Single word used to summon you.', + component: FeatureShortTextInput, +}; diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_rune_color.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_rune_color.tsx new file mode 100644 index 00000000000000..0864302baa5c04 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/summonable_rune_color.tsx @@ -0,0 +1,7 @@ +import { FeatureColorInput, type Feature } from '../../base'; + +export const summonable_rune_color: Feature = { + name: 'Summonable Color', + description: 'Rune and spotlight color.', + component: FeatureColorInput, +}; From 684efb035a921aa29f51f7cbc51e9d80ea5d4460 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 8 Mar 2026 08:42:49 +0100 Subject: [PATCH 125/212] QoL for Premium Augments --- .../items/devices/scanners/health_analyzer.dm | 10 ++- .../mortal/augmented/_premium_augment.dm | 61 +++++++++++++------ .../surgery/_premium_surgery_steps.dm | 5 ++ 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/code/game/objects/items/devices/scanners/health_analyzer.dm b/code/game/objects/items/devices/scanners/health_analyzer.dm index eb285c0312dc01..fd2d2af87d1306 100644 --- a/code/game/objects/items/devices/scanners/health_analyzer.dm +++ b/code/game/objects/items/devices/scanners/health_analyzer.dm @@ -344,7 +344,15 @@ if(istype(target_organ, /obj/item/organ/cyberimp)) var/obj/item/organ/cyberimp/cy = target_organ if(cy.premium_component) - line = "[line] (quality: [round(cy.premium_component.quality)]%)" + var/quality = round(cy.premium_component.quality) + var/quality_text = "quality: [quality]%" + quality_text = conditional_tooltip(quality_text, "Premium augment quality affects performance. At 0% it must be refurbished. \ + Using premium augment maintenance surgery on the appropriate bodypart ([parse_zone(target_organ.zone)]) will restore up to 75% so long as it is not broken. \ + Removing the augment with organ manipulation and refurbishing it in-hand will restore up to 100% (examine the augment for instructions).", tochat) + if(quality <= 0) + line = "[line] ([quality_text] refurbish required)" + else + line = "[line] ([quality_text])" LAZYADD(cyberimps, line) // DOPPLER ADDITION END if(target_organ.organ_flags & ORGAN_MUTANT) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm index 625eb7081c9f7d..c5009bde7665a3 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm @@ -130,7 +130,7 @@ /datum/component/premium_augment/proc/handle_refurbish_interaction(mob/user, obj/item/tool, obj/item/organ/cyberimp/implant) if(!user || !tool || !implant) return FALSE - if(implant.owner) + if(implant.owner) // I don't even know how you would do this; the manual says to take it out first >:C to_chat(user, span_warning("You need to remove [implant] before refurbishing it.")) return TRUE var/step = get_refurb_step() @@ -143,34 +143,55 @@ to_chat(user, span_warning("You need a screwdriver to open [implant]'s casing.")) return TRUE to_chat(user, span_notice("You open [implant]'s casing.")) + tool.play_tool_sound(implant) advance_refurb_step() return TRUE if(AUGMENTED_REFURBISH_PARTS) ensure_refurb_parts() - if(!istype(tool, /obj/item/stack)) - to_chat(user, span_warning("You need spare parts to refurbish [implant].")) - return TRUE - var/obj/item/stack/stack = tool - var/typepath = stack.merge_type ? stack.merge_type : stack.type - var/needed = refurb_parts_remaining[typepath] - if(!needed) - to_chat(user, span_warning("[stack] doesn't fit [implant]'s parts.")) - return TRUE - var/available = stack.amount - if(available <= 0) - to_chat(user, span_warning("[stack] doesn't have anything left to use.")) - return TRUE - var/use_amount = min(needed, available) - if(!stack.use(use_amount)) - to_chat(user, span_warning("You need more [stack] to continue.")) - return TRUE + + // Saves typepath, amount needed and how much was used to pass on to later in the function. + var/typepath + var/needed + var/use_amount + + // Stack-specific interactions + if(istype(tool, /obj/item/stack)) + var/obj/item/stack/stack = tool + typepath = stack.merge_type ? stack.merge_type : stack.type + needed = refurb_parts_remaining[typepath] + + // Wrong item, right subtype. + if(!needed) + to_chat(user, span_warning("[stack] doesn't fit [implant]'s parts.")) + return TRUE + + // Not enough in a stack + var/available = stack.amount + use_amount = min(needed, available) + if(use_amount <= 0 || !stack.use(use_amount)) + to_chat(user, span_warning("You need more [stack] to continue.")) + return TRUE + // Non-stack parts. + else + typepath = tool.type + needed = refurb_parts_remaining[typepath] + + // Wrong item + if(!needed) + to_chat(user, span_warning("[tool] doesn't fit [implant]'s parts.")) + return TRUE + + qdel(tool) + + // Succesful use interaction needed -= use_amount if(needed <= 0) refurb_parts_remaining -= typepath else refurb_parts_remaining[typepath] = needed to_chat(user, span_notice("You replace worn parts inside [implant].")) + tool.play_tool_sound(implant) if(!LAZYLEN(refurb_parts_remaining)) advance_refurb_step() return TRUE @@ -180,6 +201,7 @@ to_chat(user, span_warning("You need a multitool to calibrate [implant].")) return TRUE to_chat(user, span_notice("You calibrate [implant]'s diagnostics.")) + tool.play_tool_sound(implant) advance_refurb_step() return TRUE @@ -188,8 +210,9 @@ to_chat(user, span_warning("You need a screwdriver to close [implant]'s casing.")) return TRUE refurbish(AUGMENTED_PREMIUM_QUALITY_MAX) + tool.play_tool_sound(implant) reset_refurb() - to_chat(user, span_notice("You finish refurbishing [implant]. It looks factory-new.")) + to_chat(user, span_notice("You finish refurbishing [implant]. Looks about as new as it can get.")) return TRUE return FALSE diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm index 125afe5427a5b2..5008c8abaf35ce 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm @@ -111,6 +111,11 @@ target_zone = check_zone(target_zone) to_chat(user, span_warning("You can't find any premium augments to service in [target]'s [target.parse_zone_with_bodypart(target_zone)].")) return SURGERY_STEP_FAIL + if(target_implant.premium_component && target_implant.premium_component.quality <= 0) + if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH) + target_zone = check_zone(target_zone) + to_chat(user, span_warning("[target]'s [target_implant.name] in [target.parse_zone_with_bodypart(target_zone)] is broken and needs refurbishing first.")) + return SURGERY_STEP_FAIL if(target_zone == BODY_ZONE_PRECISE_EYES || target_zone == BODY_ZONE_PRECISE_MOUTH) target_zone = check_zone(target_zone) From 526ffca781eff63ed4193c11896f5dfed4ba817f Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 8 Mar 2026 09:15:02 +0100 Subject: [PATCH 126/212] Summonable QoL (text, sounds, etc) --- .../powers/resonant/aberrant/summonable.dm | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm index 9153f3fda79bc2..8fd5559bc50031 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm @@ -56,7 +56,7 @@ var/rune_fade_time = 6 var/rune_color = "#ff2a2a" - // These below are there to allow us to be dispelled and end the teleporation without brekaing everything. It is so EXCEEDINGLY MESSY. + // These below are there to allow us to be dispelled and end the teleporation without brekaing everything. var/summoning = FALSE var/beaming_up = FALSE var/list/obj/effect/summonable_rune_orbiter/current_runes @@ -111,6 +111,7 @@ ADD_TRAIT(summoned, TRAIT_MOVE_FLOATING, "summonable_apport") // Depart: float up and fade out at the origin. + summoned.visible_message(span_warning("[summoned] leaves the ground, and begins to vanish into thin air!")) animate(summoned, alpha = 0, pixel_y = old_pixel_y + 32, time = float_time) addtimer(CALLBACK(src, PROC_REF(clear_origin_spotlight), origin_spotlight), float_time) @@ -150,7 +151,6 @@ beaming_up = FALSE var/obj/effect/temp_visual/spotlight/summonable/spotlight = new(target_turf, rune_color) - playsound(target_turf, 'sound/effects/magic/voidblink.ogg', 50, TRUE) fade_and_clear_runes(runes) summoned.forceMove(target_turf) @@ -158,6 +158,9 @@ summoned.pixel_y = 32 animate(summoned, alpha = old_alpha, pixel_y = old_pixel_y, time = float_time) + playsound(summoned, 'sound/effects/magic/voidblink.ogg', 50, TRUE) + summoned.visible_message(span_warning("[summoned] appears out of thin air!")) + addtimer(CALLBACK(src, PROC_REF(finish_summon), summoned, target_turf, old_alpha, old_pixel_y, spotlight), float_time) // Fade and clear the runes. @@ -203,6 +206,17 @@ if(!beaming_up || !summoning) return NONE cancel_summon(target) + if(ishuman(target)) + var/mob/living/carbon/human/failed_summon = target + // Do you have anything to brace your fall? Or do you possibly manage to get lucky? + var/obj/item/organ/wings/gliders = failed_summon.get_organ_by_type(/obj/item/organ/wings) + if(HAS_TRAIT(failed_summon, TRAIT_FREERUNNING) || gliders?.can_soften_fall() || prob(20)) + failed_summon.visible_message(span_warning("[failed_summon] suddenly reappears and lands back on the ground!"), span_warning("You drop to the ground, but manage to catch yourself!")) + else + failed_summon.visible_message(span_warning("[failed_summon] suddenly reappears and falls face-first onto the ground!"), span_userdanger("You suddenly fall face-first onto the ground!")) + playsound(failed_summon, 'sound/effects/desecration/desecration-02.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + failed_summon.adjustBruteLoss(5) + failed_summon.Knockdown(3 SECONDS) return DISPEL_RESULT_DISPELLED /datum/component/beetlejuice/summonable/proc/cancel_summon(atom/movable/summoned) From ee12b1439e230997b5ca5fe5ba74efb28e78c8f4 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 8 Mar 2026 17:20:42 +0100 Subject: [PATCH 127/212] Adds the pneumetic arm(s). Changes some premium augment stuff. --- code/__DEFINES/~doppler_defines/powers.dm | 15 ++- .../mortal/augmented/_premium_action.dm | 91 ++++++++++++++-- .../mortal/augmented/_premium_augment.dm | 16 +-- .../mortal/augmented/mental_shielding.dm | 15 ++- .../powers/mortal/augmented/pneumatic_arm.dm | 102 ++++++++++++++++++ tgstation.dme | 1 + 6 files changed, 214 insertions(+), 26 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index f724ef2c1fab2c..71b689add1747c 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -296,10 +296,17 @@ #define AUGMENTED_DECAY_AMOUNT 1 // Thresholds for Premium Quality tiers. As long as it is above the number = all is good -#define AUGMENTED_PREMIUM_QUALITY_OPTIMAL (AUGMENTED_PREMIUM_QUALITY_MAX * 0.75) -#define AUGMENTED_PREMIUM_QUALITY_HIGH (AUGMENTED_PREMIUM_QUALITY_MAX * 0.50) -#define AUGMENTED_PREMIUM_QUALITY_MEDIUM (AUGMENTED_PREMIUM_QUALITY_MAX * 0.25) -#define AUGMENTED_PREMIUM_QUALITY_LOW (AUGMENTED_PREMIUM_QUALITY_MAX * 0) +#define AUGMENTED_PREMIUM_THRESHOLD_OPTIMAL (AUGMENTED_PREMIUM_QUALITY_MAX * 0.75) +#define AUGMENTED_PREMIUM_THRESHOLD_HIGH (AUGMENTED_PREMIUM_QUALITY_MAX * 0.50) +#define AUGMENTED_PREMIUM_THRESHOLD_MEDIUM (AUGMENTED_PREMIUM_QUALITY_MAX * 0.25) +#define AUGMENTED_PREMIUM_THRESHOLD_LOW (AUGMENTED_PREMIUM_QUALITY_MAX * 0) + +// Percentage mods for quality. +#define AUGMENTED_PREMIUM_QUALITY_TRIVIAL (AUGMENTED_PREMIUM_QUALITY_MAX / 100) +#define AUGMENTED_PREMIUM_QUALITY_MINOR (AUGMENTED_PREMIUM_QUALITY_MAX / 10) +#define AUGMENTED_PREMIUM_QUALITY_MODERATE (AUGMENTED_PREMIUM_QUALITY_MAX / 5) +#define AUGMENTED_PREMIUM_QUALITY_MAJOR (AUGMENTED_PREMIUM_QUALITY_MAX / 2) +#define AUGMENTED_PREMIUM_QUALITY_CRUSHING (AUGMENTED_PREMIUM_QUALITY_MAX) // The amount of performance from each. We expect high to be the norm, so that is our 1, instead of optimal. #define AUGMENTED_PREMIUM_EFFICIENCY_OPTIMAL 1.2 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 ddeeb5687e1d0e..6a324b6c3e6429 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 @@ -2,12 +2,20 @@ /datum/action/item_action/organ_action/premium name = "Premium Augment" check_flags = AB_CHECK_CONSCIOUS + background_icon_state = "bg_default" + overlay_icon_state = "bg_mod_border" + var/base_overlay_icon_state + var/active_overlay_icon_state = "bg_spell_border_active_blue" var/datum/component/premium_augment/premium_component var/mutable_appearance/quality_overlay + /// Defers action button creation until hud exists. + var/pending_hud_grant = FALSE /datum/action/item_action/organ_action/premium/New(Target) ..() + if(active_overlay_icon_state) + base_overlay_icon_state ||= overlay_icon_state var/obj/item/organ/cyberimp/organ_target = target premium_component = organ_target?.premium_component premium_component?.register_quality_action(src) @@ -22,8 +30,41 @@ if(!premium_component) var/obj/item/organ/cyberimp/organ_target = target premium_component = organ_target?.premium_component + premium_component?.register_quality_action(src) + update_arm_label() addtimer(CALLBACK(src, PROC_REF(update_quality_overlay)), 1) // Adresses a bug that the percentage is not visible at round start. +// We have to delay giving the action because we communicate with the button, and this causes runtimes at roundstart. We use signalers to delay it until the huds there. +/datum/action/item_action/organ_action/premium/GiveAction(mob/viewer) + if(!viewer || !viewer.hud_used) + if(viewer && !pending_hud_grant) + pending_hud_grant = TRUE + RegisterSignal(viewer, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created), override = TRUE) + return + if(pending_hud_grant) + pending_hud_grant = FALSE + UnregisterSignal(viewer, COMSIG_MOB_HUD_CREATED) + return ..() + +/datum/action/item_action/organ_action/premium/proc/on_hud_created(mob/source) + SIGNAL_HANDLER + GiveAction(source) + +/datum/action/item_action/organ_action/premium/proc/update_arm_label() + if(!istype(src, /datum/action/item_action/organ_action/premium/use)) + return + var/obj/item/organ/organ_target = target + if(!organ_target) + return + name = "Toggle [organ_target.name][arm_side_suffix(organ_target)]" + build_all_button_icons(UPDATE_BUTTON_NAME | UPDATE_BUTTON_ICON | UPDATE_BUTTON_OVERLAY) + +/datum/action/item_action/organ_action/premium/Remove(mob/remove_from) + if(remove_from) + UnregisterSignal(remove_from, COMSIG_MOB_HUD_CREATED) + pending_hud_grant = FALSE + return ..() + /datum/action/item_action/organ_action/premium/IsAvailable(feedback = FALSE) . = ..() if(!premium_component) @@ -31,20 +72,21 @@ premium_component = organ_target?.premium_component return . +// Applies the maptext on the button indicating quality. /datum/action/item_action/organ_action/premium/proc/update_quality_overlay() var/atom/movable/ui_element = get_atom_moveable() if(!ui_element || !premium_component) return - ui_element.cut_overlay(quality_overlay) - quality_overlay = new/mutable_appearance - quality_overlay.maptext_width = 32 - quality_overlay.maptext_height = 16 - quality_overlay.maptext_x = 4 - quality_overlay.maptext_y = 0 + if(!quality_overlay) + quality_overlay = new/mutable_appearance + quality_overlay.plane = ABOVE_HUD_PLANE + quality_overlay.maptext_width = 32 + quality_overlay.maptext_height = 16 + quality_overlay.maptext_x = 4 + quality_overlay.maptext_y = 0 var/percent = clamp(round(premium_component.quality), 0, 100) quality_overlay.maptext = MAPTEXT("[percent]%") - ui_element.add_overlay(quality_overlay) - build_all_button_icons(UPDATE_BUTTON_STATUS) + build_all_button_icons(UPDATE_BUTTON_OVERLAY | UPDATE_BUTTON_STATUS) /datum/action/item_action/organ_action/premium/proc/get_atom_moveable() for(var/datum/hud/hud_instance as anything in viewers) @@ -52,17 +94,48 @@ if(istype(action_button_instance, /atom/movable/screen/movable/action_button)) return action_button_instance +/datum/action/item_action/organ_action/premium/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force = FALSE) + if(active_overlay_icon_state) + overlay_icon_state = is_action_active(current_button) ? active_overlay_icon_state : base_overlay_icon_state + . = ..() + if(!quality_overlay || !current_button) + return . + current_button.cut_overlay(quality_overlay) + current_button.add_overlay(quality_overlay) + return . + +// This is specifically to flip right-side arm augments to look visually distinct from the other button (since you can have 2 arm augments). + +/datum/action/item_action/organ_action/premium/is_action_active(atom/movable/screen/movable/action_button/current_button) + var/obj/item/organ/cyberimp/organ_target = target + return organ_target?.is_action_active() || FALSE + /datum/action/item_action/organ_action/premium/use name = "Toggle Premium Augment" /datum/action/item_action/organ_action/premium/use/New(Target) ..() var/obj/item/organ/organ_target = target - name = "Toggle [organ_target.name]" + name = "Toggle [organ_target.name][arm_side_suffix(organ_target)]" + +// Adds a suffix to left and right arm actions since you can have two actions and it might get confusing. +/datum/action/item_action/organ_action/premium/proc/arm_side_suffix(obj/item/organ/organ_target) + if(!istype(organ_target, /obj/item/organ/cyberimp/arm)) + return "" + if(organ_target.zone == BODY_ZONE_L_ARM) + return " (Left)" + if(organ_target.zone == BODY_ZONE_R_ARM) + return " (Right)" + return "" /datum/action/item_action/organ_action/premium/use/do_effect(trigger_flags) var/obj/item/organ/cyberimp/organ_target = target if(!organ_target) return FALSE organ_target.use_action() + build_all_button_icons(UPDATE_BUTTON_OVERLAY | UPDATE_BUTTON_STATUS) return TRUE + +// Premium augments can override this to report their "on" state for button overlays. +/obj/item/organ/cyberimp/proc/is_action_active() + return FALSE diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm index c5009bde7665a3..7fb46b6611b19d 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm @@ -48,13 +48,13 @@ /// Returns a tier label for UI or logic. /datum/component/premium_augment/proc/quality_tier() - if(quality > AUGMENTED_PREMIUM_QUALITY_OPTIMAL) + if(quality > AUGMENTED_PREMIUM_THRESHOLD_OPTIMAL) return "optimal" - if(quality > AUGMENTED_PREMIUM_QUALITY_HIGH) + if(quality > AUGMENTED_PREMIUM_THRESHOLD_HIGH) return "standard" - if(quality > AUGMENTED_PREMIUM_QUALITY_MEDIUM) + if(quality > AUGMENTED_PREMIUM_THRESHOLD_MEDIUM) return "compromised" - if(quality > AUGMENTED_PREMIUM_QUALITY_LOW) + if(quality > AUGMENTED_PREMIUM_THRESHOLD_LOW) return "failing" return "broken" @@ -64,13 +64,13 @@ /// Returns the efficiency value based on quality tiers. /datum/component/premium_augment/proc/get_efficiency() - if(quality > AUGMENTED_PREMIUM_QUALITY_OPTIMAL) + if(quality > AUGMENTED_PREMIUM_THRESHOLD_OPTIMAL) return AUGMENTED_PREMIUM_EFFICIENCY_OPTIMAL - if(quality > AUGMENTED_PREMIUM_QUALITY_HIGH) + if(quality > AUGMENTED_PREMIUM_THRESHOLD_HIGH) return AUGMENTED_PREMIUM_EFFICIENCY_HIGH - if(quality > AUGMENTED_PREMIUM_QUALITY_MEDIUM) + if(quality > AUGMENTED_PREMIUM_THRESHOLD_MEDIUM) return AUGMENTED_PREMIUM_EFFICIENCY_MEDIUM - if(quality > AUGMENTED_PREMIUM_QUALITY_LOW) + if(quality > AUGMENTED_PREMIUM_THRESHOLD_LOW) return AUGMENTED_PREMIUM_EFFICIENCY_LOW return AUGMENTED_PREMIUM_EFFICIENCY_BROKEN 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 6489337c60969c..57c51409570cf3 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 @@ -28,8 +28,10 @@ . = ..() if(premium_component) premium_component.refurb_parts = list( - /obj/item/stack/sheet/iron = 3, - /obj/item/stack/cable_coil = 2) + /obj/item/stack/sheet/iron = 1, + /obj/item/stack/sheet/mineral/uranium = 1, + /obj/item/stack/cable_coil = 2, + /obj/item/stock_parts/scanning_module/triphasic = 1) // Registers antimagic signals /obj/item/organ/cyberimp/brain/mental_shielding/on_mob_insert(mob/living/carbon/receiver, special, movement_flags) @@ -64,7 +66,7 @@ var/efficiency = premium_component.get_efficiency() || 0 if(efficiency <= 0) return 0 - var/mult = (AUGMENTED_PREMIUM_EFFICIENCY_HIGH / efficiency) * mental_mult + var/mult = AUGMENTED_PREMIUM_QUALITY_MINOR * (1 / efficiency) return max(1, round(raw_cost * mult)) /obj/item/organ/cyberimp/brain/mental_shielding/use_action() @@ -73,8 +75,11 @@ enabled = !enabled if(enabled) ADD_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT) - to_chat(owner, span_notice("Your [name] hums as it activates.")) + to_chat(owner, span_notice("Your [name] is toggled on; it will now block any mental effects targeting you.")) else REMOVE_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT) - to_chat(owner, span_notice("Your [name] powers down.")) + to_chat(owner, span_notice("Your [name] is toggled off!")) + return enabled + +/obj/item/organ/cyberimp/brain/mental_shielding/is_action_active() return enabled diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm new file mode 100644 index 00000000000000..38e09eb1642965 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm @@ -0,0 +1,102 @@ +/* + Its an arm that makes you punch harder and be activated to punch EVEN HARDER. +*/ +/datum/power/augmented/pneumatic_arm + name = "Premium DSTR Pneumatic Arm" + desc = "A popular choice for the augmented bodyguards, given it turns your arms into weapons; ideal for places that don't allow weapons. Passively increases your punch damage by +5 with that arm. \ + \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 2 spaces (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ + \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)." + + value = 5 // balance around 2 arms. + augment = /obj/item/organ/cyberimp/arm/pneumatic_arm + +/obj/item/organ/cyberimp/arm/pneumatic_arm + name = "Premium DSTR Pneumatic Arm" + desc = "A popular choice for the augmented bodyguards, given it turns your arms into weapons; ideal for places that don't allow weapons. Passively increases your punch damage by +5 with thar arm. \ + \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 2 spaces (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ + \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)." + icon_state = "toolkit_generic" + + actions_types = list(/datum/action/item_action/organ_action/premium/use) + premium = TRUE + + // Going to deal the extra damage + knockback on the next punch + var/overcharged = FALSE + + // Bonus damage + var/bonus_passive_damage = 5 + var/bonus_active_damage = 15 + + // Knockback on punch + var/knockback = 2 + // Is the throw 'throw'? False means it can cause wallstuns and such. + var/gentle_throw = FALSE + + +/obj/item/organ/cyberimp/arm/pneumatic_arm/Initialize(mapload) + . = ..() + if(premium_component) + premium_component.refurb_parts = list( + /obj/item/stack/sheet/iron = 5, + /obj/item/stack/sheet/plasteel = 2, + /obj/item/stack/cable_coil = 2, + /obj/item/stock_parts/servo/femto = 1) + +/obj/item/organ/cyberimp/arm/pneumatic_arm/on_mob_insert(mob/living/carbon/arm_owner) + . = ..() + RegisterSignal(arm_owner, COMSIG_HUMAN_UNARMED_HIT, PROC_REF(on_unarmed_hit)) + +/obj/item/organ/cyberimp/arm/pneumatic_arm/on_mob_remove(mob/living/carbon/arm_owner) + . = ..() + UnregisterSignal(arm_owner, COMSIG_HUMAN_UNARMED_HIT) + +/obj/item/organ/cyberimp/arm/pneumatic_arm/proc/on_unarmed_hit(mob/living/user, mob/living/target, obj/item/bodypart/affecting, damage, armor_block, limb_accuracy, limb_sharpness) + SIGNAL_HANDLER + if(!target || !premium_component?.can_function()) + return + + // Only applies bonus damage when the arm is the active arm. + if(user.get_active_hand() != hand) + return + + var/efficiency = premium_component.get_efficiency() + if(efficiency <= 0) + return + + // Bonus damage when punching + var/passive_damage = round(bonus_passive_damage * efficiency, DAMAGE_PRECISION) + if(passive_damage > 0) + target.apply_damage(passive_damage, BRUTE, affecting, armor_block, sharpness = limb_sharpness) + + // If active; smack extra-hard. + if(overcharged) + var/active_damage = round(bonus_active_damage * efficiency, DAMAGE_PRECISION) + if(active_damage > 0) + target.apply_damage(active_damage, BRUTE, affecting, armor_block, sharpness = limb_sharpness) + + if(ismovable(target)) + var/throw_dir = get_dir(user, target) + if(throw_dir) + var/atom/throw_target = get_edge_target_turf(target, throw_dir) + target.throw_at(throw_target, knockback, 2, user, gentle = gentle_throw) + to_chat(target, span_userdanger("[user]'s punch sends you flying!")) + playsound(target, 'sound/items/weapons/resonator_blast.ogg', 75, TRUE) + overcharged = FALSE + premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) + +/obj/item/organ/cyberimp/arm/pneumatic_arm/use_action() + if(!owner) + return FALSE + if(!premium_component?.can_function()) + to_chat(owner, span_warning("Your [name] fails to respond; it seems broken!")) + return FALSE + if(overcharged) + to_chat(owner, span_notice("You return your [name] to it's standard settings.")) + overcharged = FALSE + return TRUE + overcharged = TRUE + to_chat(owner, span_notice("You overcharge your [name]. Your next punch will knock back your target.")) + return TRUE + +/obj/item/organ/cyberimp/arm/pneumatic_arm/is_action_active() + return overcharged diff --git a/tgstation.dme b/tgstation.dme index 9905813da10182..ae7b3435b0aaa4 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7434,6 +7434,7 @@ #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_action.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_augment.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\mental_shielding.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\pneumatic_arm.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\surgery\_premium_surgery.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\surgery\_premium_surgery_steps.dm" From 6444c942d299d314dfe811560b08ad25cbcb2355 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 9 Mar 2026 11:48:45 +0100 Subject: [PATCH 128/212] Added EMP effects tao all premium augments. Added the auto retriever. Basically finishes augments. --- code/__DEFINES/~doppler_defines/powers.dm | 2 +- .../mortal/augmented/_premium_action.dm | 23 +- .../powers/mortal/augmented/auto_retriever.dm | 240 ++++++++++++++++++ .../mortal/augmented/mental_shielding.dm | 19 ++ .../powers/mortal/augmented/pneumatic_arm.dm | 28 +- tgstation.dme | 1 + 6 files changed, 296 insertions(+), 17 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 71b689add1747c..23369f07b75d35 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -292,7 +292,7 @@ #define AUGMENTED_PREMIUM_QUALITY_START 75 // How often augments will normally lose quality, and how much. -#define AUGMENTED_DECAY_INTERVAL 5 MINUTES +#define AUGMENTED_DECAY_INTERVAL 4 MINUTES #define AUGMENTED_DECAY_AMOUNT 1 // Thresholds for Premium Quality tiers. As long as it is above the number = all is good 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 6a324b6c3e6429..57db170acc05dc 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 @@ -1,7 +1,7 @@ // Custom actions for premium augments, meant to show the progress bar with quality wear. /datum/action/item_action/organ_action/premium name = "Premium Augment" - check_flags = AB_CHECK_CONSCIOUS + check_flags = AB_CHECK_CONSCIOUS | AB_CHECK_INCAPACITATED background_icon_state = "bg_default" overlay_icon_state = "bg_mod_border" var/base_overlay_icon_state @@ -77,16 +77,17 @@ var/atom/movable/ui_element = get_atom_moveable() if(!ui_element || !premium_component) return - if(!quality_overlay) - quality_overlay = new/mutable_appearance - quality_overlay.plane = ABOVE_HUD_PLANE - quality_overlay.maptext_width = 32 - quality_overlay.maptext_height = 16 - quality_overlay.maptext_x = 4 - quality_overlay.maptext_y = 0 + ui_element.cut_overlay(quality_overlay) + quality_overlay = new/mutable_appearance + quality_overlay.plane = ABOVE_HUD_PLANE + quality_overlay.maptext_width = 32 + quality_overlay.maptext_height = 16 + quality_overlay.maptext_x = 4 + quality_overlay.maptext_y = 0 var/percent = clamp(round(premium_component.quality), 0, 100) quality_overlay.maptext = MAPTEXT("[percent]%") - build_all_button_icons(UPDATE_BUTTON_OVERLAY | UPDATE_BUTTON_STATUS) + ui_element.add_overlay(quality_overlay) + build_all_button_icons(UPDATE_BUTTON_STATUS) /datum/action/item_action/organ_action/premium/proc/get_atom_moveable() for(var/datum/hud/hud_instance as anything in viewers) @@ -98,10 +99,6 @@ if(active_overlay_icon_state) overlay_icon_state = is_action_active(current_button) ? active_overlay_icon_state : base_overlay_icon_state . = ..() - if(!quality_overlay || !current_button) - return . - current_button.cut_overlay(quality_overlay) - current_button.add_overlay(quality_overlay) return . // This is specifically to flip right-side arm augments to look visually distinct from the other button (since you can have 2 arm augments). diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm new file mode 100644 index 00000000000000..a2711eb2cd5fad --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm @@ -0,0 +1,240 @@ +/* + Teleports you to medbay, once. Either on demand or when you soft-crit. Needs to be refurbished after & can be interupted. +*/ +/datum/power/augmented/auto_retriever + name = "Premium ANGL Auto Retriever" + desc = "Some assets are far too wealthy to risk losing. Created by DeForest, this allows their premium customers to be rescued from the most grievous of circumstances; and recently came with a support API for other healthcare providers.\ + \n Once you reach critical condition or when manually activated, you begin a slow (and obvious) 10 second teleport towards your station's medbay lobby (regardless of Z-level).\ + Once it fires, a warning message is issued over the radio. The teleportation sets the quality to 0%, and can be interupted by Epinephrine in the bloodstream, EMP, or healing you above the critical threshold; after which it loses 25% quality and enters a several minute cooldown period.\ + \n Decreases in quality twice as fast. Lower quality decreases the speed of the teleport." + + value = 5 + augment = /obj/item/organ/cyberimp/chest/auto_retriever + +/obj/item/organ/cyberimp/chest/auto_retriever + name = "ANGL Auto Retriever" + desc = "Some assets are far too wealthy to risk losing. Created by DeForest, this allows their premium customers to be rescued from the most grievous of circumstances; and recently came with a support API for other healthcare providers.\ + \n Once you reach critical condition or when manually activated, you begin a slow (and obvious) 10 second teleport towards your station's medbay lobby (regardless of Z-level).\ + Once it fires, a warning message is issued over the radio. The teleportation sets the quality to 0%, and can be interupted by Epinephrine in the bloodstream, EMP, or healing you above the critical threshold; after which it loses 25% quality and enters a several minute cooldown period.\ + \n Decreases in quality twice as fast. Lower quality decreases the speed of the teleport." + icon_state = "reviver_implant" + slot = ORGAN_SLOT_HEART_AID + + actions_types = list(/datum/action/item_action/organ_action/premium/use) + premium = TRUE + var/enabled = TRUE + + // Are we in the process of teleporting + var/teleporting = FALSE + var/teleport_timer_id + // Spool up time. + var/teleport_spool_time = 10 SECONDS + var/teleport_charge_sound = 'sound/effects/magic/lightning_chargeup.ogg' + + // Used to store the caps of the sound frequency when speeding up/slowing down the charge sound. + var/teleport_sound_base_frequency = 44000 + var/teleport_sound_min_frequency = 32000 + var/teleport_sound_max_frequency = 55000 + + // Cooldowns for TP and EMPs. + COOLDOWN_DECLARE(teleport_cooldown) + var/tp_cooldown = 3 MINUTES + COOLDOWN_DECLARE(emp_reenable_cooldown) + var/emp_cooldown = 30 SECONDS + + // Internal radio used for relaying to medbay. + var/obj/item/radio/internal_radio + + // Ref for the sparking overlay. + var/mutable_appearance/teleport_spark_overlay + var/teleport_spark_icon = 'icons/effects/effects.dmi' + var/teleport_spark_state = "lightning" + var/teleport_spark_layer = ABOVE_MOB_LAYER + +/obj/item/organ/cyberimp/chest/auto_retriever/Initialize(mapload) + . = ..() + if(premium_component) + premium_component.refurb_parts = list( + /obj/item/stack/sheet/iron = 1, + /obj/item/stack/sheet/bluespace_crystal = 1, + /obj/item/stack/cable_coil = 2, + /obj/item/stock_parts/scanning_module/triphasic = 1) + premium_component.decay_interval = AUGMENTED_DECAY_INTERVAL / 2 // decays twice as fast. + + // We give it a radio to be able to speak to the medbay frequency. + internal_radio = new /obj/item/radio(src) + internal_radio.keyslot = new /obj/item/encryptionkey/headset_med + internal_radio.subspace_transmission = TRUE + internal_radio.canhear_range = 0 // no free medbay radio 4u + internal_radio.recalculateChannels() + +/obj/item/organ/cyberimp/chest/auto_retriever/Destroy() + if(teleport_timer_id) + deltimer(teleport_timer_id) + teleport_timer_id = null + QDEL_NULL(internal_radio) + return ..() + +// Checks if we're in deep shit and need teleporting out. +/obj/item/organ/cyberimp/chest/auto_retriever/on_life(seconds_per_tick, times_fired) + if(!owner || !enabled) + return + if(teleporting) + if(should_cancel_teleport()) + cancel_teleport() + return + if(!premium_component?.can_function()) + return + if(!COOLDOWN_FINISHED(src, teleport_cooldown)) + return + if(owner.reagents?.has_reagent(/datum/reagent/medicine/epinephrine)) + return + if(owner.stat >= SOFT_CRIT && owner.stat != DEAD) + start_teleport() + +// Starts spooling up and notifying literally everoyne they are going to poof. +/obj/item/organ/cyberimp/chest/auto_retriever/proc/start_teleport() + if(!owner) + return + if(teleporting || !enabled) + return + if(!premium_component?.can_function()) + return + if(!COOLDOWN_FINISHED(src, teleport_cooldown)) + return + teleporting = TRUE + // Modifies the tp by efficiency + var/efficiency = premium_component?.get_efficiency() || 1 + var/spool_time = round(teleport_spool_time / max(efficiency, 0.01)) + var/teleport_seconds = round(spool_time / (1 SECONDS)) + var/message = "Patient health critical; commencing teleportation in [teleport_seconds] seconds. Stabilize patient to cancel." + augment_speak(message) + apply_teleport_effects(spool_time) + var/sound_frequency = clamp(round(teleport_sound_base_frequency * efficiency), teleport_sound_min_frequency, teleport_sound_max_frequency) + if(sound_frequency > teleport_sound_min_frequency && spool_time > 2 SECONDS) + var/sound_ratio = spool_time / max(spool_time - 2 SECONDS, 1) + sound_frequency = clamp(round(sound_frequency * sound_ratio), teleport_sound_min_frequency, teleport_sound_max_frequency) + owner.playsound_local(owner, teleport_charge_sound, 75, TRUE, frequency = sound_frequency) + teleport_timer_id = addtimer(CALLBACK(src, PROC_REF(finish_teleport)), spool_time, TIMER_STOPPABLE) + +// We go POOF, away. +/obj/item/organ/cyberimp/chest/auto_retriever/proc/finish_teleport() + if(!teleporting) + return + teleporting = FALSE + if(teleport_timer_id) + deltimer(teleport_timer_id) + teleport_timer_id = null + clear_teleport_effects() + if(!owner || owner.stat < SOFT_CRIT || owner.stat == DEAD) + return + + // We try to TP to the lobby first; if there's no lobby we teleport them to the medbay. + var/turf/destination = pick_open_turf_from_area(/area/station/medical/medbay/lobby) + if(!destination) + destination = pick_open_turf_from_area(/area/station/medical/medbay, subtypes = TRUE) + if(!destination) + return + + var/teleport_success = do_teleport(owner, destination, channel = TELEPORT_CHANNEL_QUANTUM) + if(!teleport_success) + return + + augment_speak("Auto Retriever alert: [owner.real_name] has teleported to Medbay for emergency treatment.", RADIO_CHANNEL_MEDICAL) + + // Sets it to 0. Go and get it refurbished. + if(premium_component) + premium_component.adjust_quality(-premium_component.quality) + +// Cancel if stabilized, epinephrine applied, or EMP'd. +/obj/item/organ/cyberimp/chest/auto_retriever/proc/should_cancel_teleport() + if(!owner) + return FALSE + if(owner.stat < SOFT_CRIT) + return TRUE + if(owner.reagents?.has_reagent(/datum/reagent/medicine/epinephrine)) + return TRUE + return FALSE + +// Stops a teleport that is in progress. +/obj/item/organ/cyberimp/chest/auto_retriever/proc/cancel_teleport() + if(!teleporting) + return + teleporting = FALSE + if(teleport_timer_id) + deltimer(teleport_timer_id) + teleport_timer_id = null + clear_teleport_effects() + augment_speak("Teleportation cancelled; entering cooldown.") + COOLDOWN_START(src, teleport_cooldown, tp_cooldown) + if(premium_component) + premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MODERATE) + +// When we get EMP'd. +/obj/item/organ/cyberimp/chest/auto_retriever/emp_act(severity) + . = ..() + if(. & EMP_PROTECT_SELF) + return + if(premium_component) + premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) + enabled = FALSE + COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown) + premium_component?.update_quality_actions() + to_chat(owner, span_warning("Your [name] becomes disabled!")) + cancel_teleport() + +// Makes the augment speak, either locally or through the radio. +/obj/item/organ/cyberimp/chest/auto_retriever/proc/augment_speak(message, channel) + if(!message) + return + var/list/message_mods = list(SAY_MOD_VERB = "states") + if(channel) + if(internal_radio) + internal_radio.talk_into(src, message, channel, message_mods = message_mods) + return + say(message, forced = "auto retriever", message_mods = message_mods) + +// Toggle the auto-retriever on/off (gate for activation). +/obj/item/organ/cyberimp/chest/auto_retriever/use_action() + if(!owner) + return FALSE + if(!enabled && !COOLDOWN_FINISHED(src, emp_reenable_cooldown)) + to_chat(owner, span_warning("Your [name] is temporarily disabled from EMP interference.")) + return FALSE + enabled = !enabled + if(enabled) + to_chat(owner, span_notice("Your [name] is toggled on; it will now activate when you reach critical condition.")) + else + to_chat(owner, span_notice("Your [name] is toggled off.")) + return enabled + +/obj/item/organ/cyberimp/chest/auto_retriever/is_action_active() + return enabled + +// Apply the sparking visual effect + jitter. +/obj/item/organ/cyberimp/chest/auto_retriever/proc/apply_teleport_effects(spool_time) + if(!owner) + return + owner.set_jitter_if_lower(spool_time) + if(!teleport_spark_overlay) + teleport_spark_overlay = mutable_appearance(teleport_spark_icon, teleport_spark_state, teleport_spark_layer) + teleport_spark_overlay.appearance_flags |= KEEP_APART + owner.add_overlay(teleport_spark_overlay) + +/obj/item/organ/cyberimp/chest/auto_retriever/proc/clear_teleport_effects() + if(!owner || !teleport_spark_overlay) + return + owner.cut_overlay(teleport_spark_overlay) + +// Finds an open space to teleport to. +/obj/item/organ/cyberimp/chest/auto_retriever/proc/pick_open_turf_from_area(area_type, subtypes = FALSE) + var/list/turfs = get_area_turfs(area_type, subtypes = subtypes) + if(!LAZYLEN(turfs)) + return null + var/list/open_turfs = list() + for(var/turf/turf_candidate as anything in turfs) + if(!turf_candidate.density) + open_turfs += turf_candidate + if(!LAZYLEN(open_turfs)) + return null + return pick(open_turfs) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/mental_shielding.dm index 57c51409570cf3..37069ba2a2b721 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 @@ -24,6 +24,10 @@ // the factor with which we multiply the final cost of anti-mental var/mental_mult = 5 + // EMP cooldown + COOLDOWN_DECLARE(emp_reenable_cooldown) + var/emp_cooldown = 30 SECONDS + /obj/item/organ/cyberimp/brain/mental_shielding/Initialize(mapload) . = ..() if(premium_component) @@ -46,6 +50,18 @@ UnregisterSignal(owner, COMSIG_MOB_RECEIVE_MAGIC) REMOVE_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT) +// When we get EMP'd. +/obj/item/organ/cyberimp/brain/mental_shielding/emp_act(severity) + . = ..() + if(. & EMP_PROTECT_SELF) + return + if(premium_component) + premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) + enabled = FALSE + COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown) + premium_component?.update_quality_actions() + to_chat(owner, span_warning("Your [name] becomes disabled!")) + // Listener to check if it can block. Basically we just check if the quality is not 0. // Direct hook for antimagic signals, avoids component deletion behavior. /obj/item/organ/cyberimp/brain/mental_shielding/proc/on_receive_magic(mob/living/carbon/source, casted_magic_flags, charge_cost, list/antimagic_sources) @@ -72,6 +88,9 @@ /obj/item/organ/cyberimp/brain/mental_shielding/use_action() if(!owner) return FALSE + if(!enabled && !COOLDOWN_FINISHED(src, emp_reenable_cooldown)) + to_chat(owner, span_warning("Your [name] is temporarily disabled from EMP interference.")) + return FALSE enabled = !enabled if(enabled) ADD_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, IMPLANT_TRAIT) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm index 38e09eb1642965..8fc0f2a38f3aa4 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm @@ -7,12 +7,12 @@ \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 2 spaces (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)." - value = 5 // balance around 2 arms. + value = 4 // balance around 2 arms. augment = /obj/item/organ/cyberimp/arm/pneumatic_arm /obj/item/organ/cyberimp/arm/pneumatic_arm - name = "Premium DSTR Pneumatic Arm" - desc = "A popular choice for the augmented bodyguards, given it turns your arms into weapons; ideal for places that don't allow weapons. Passively increases your punch damage by +5 with thar arm. \ + name = "DSTR Pneumatic Arm" + desc = "A popular choice for the augmented bodyguards, given it turns your arms into weapons; ideal for places that don't allow weapons. Passively increases your punch damage by +5 with that arm. \ \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 2 spaces (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)." icon_state = "toolkit_generic" @@ -32,6 +32,9 @@ // Is the throw 'throw'? False means it can cause wallstuns and such. var/gentle_throw = FALSE + // EMP cooldown + COOLDOWN_DECLARE(emp_reenable_cooldown) + var/emp_cooldown = 30 SECONDS /obj/item/organ/cyberimp/arm/pneumatic_arm/Initialize(mapload) . = ..() @@ -50,11 +53,27 @@ . = ..() UnregisterSignal(arm_owner, COMSIG_HUMAN_UNARMED_HIT) +// On EMP +/obj/item/organ/cyberimp/arm/pneumatic_arm/emp_act(severity) + . = ..() + if(. & EMP_PROTECT_SELF) + return + if(premium_component) + premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) + overcharged = FALSE + COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown) + premium_component?.update_quality_actions() + to_chat(owner, span_warning("Your [name] becomes disabled!")) + /obj/item/organ/cyberimp/arm/pneumatic_arm/proc/on_unarmed_hit(mob/living/user, mob/living/target, obj/item/bodypart/affecting, damage, armor_block, limb_accuracy, limb_sharpness) SIGNAL_HANDLER if(!target || !premium_component?.can_function()) return + // No bonus damage if EMP'd + if(!COOLDOWN_FINISHED(src, emp_reenable_cooldown)) + return + // Only applies bonus damage when the arm is the active arm. if(user.get_active_hand() != hand) return @@ -87,6 +106,9 @@ /obj/item/organ/cyberimp/arm/pneumatic_arm/use_action() if(!owner) return FALSE + if(!overcharged && !COOLDOWN_FINISHED(src, emp_reenable_cooldown)) + to_chat(owner, span_warning("Your [name] is temporarily disabled from EMP interference.")) + return FALSE if(!premium_component?.can_function()) to_chat(owner, span_warning("Your [name] fails to respond; it seems broken!")) return FALSE diff --git a/tgstation.dme b/tgstation.dme index ae7b3435b0aaa4..cf321ecbcd1d92 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7433,6 +7433,7 @@ #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_action.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_augment.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\auto_retriever.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\mental_shielding.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\pneumatic_arm.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" From 462f42b6417b9245ec26f25afc3aaf488a33ac76 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 10 Mar 2026 09:00:52 +0100 Subject: [PATCH 129/212] Tweaks some things; the space cube is now included in astral touched, rain has a cap put on its reagent duping, gale blast now properly works on diagonals. --- .../resonant/cultivator/astraltouched_root.dm | 6 +- .../sorcerous/thaumaturge/conjure_rain.dm | 86 ++++++++++++++----- .../sorcerous/thaumaturge/gale_blast.dm | 19 ++-- 3 files changed, 80 insertions(+), 31 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm index 824eb50b2ed9ee..1bd8f2038ff148 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -55,7 +55,7 @@ var/space_value = CULTIVATOR_AURA_FARM_MINOR * 0.6 // the real thing var/glass_value = CULTIVATOR_AURA_FARM_MINOR * 0.3 // not as cool but its something var/fake_space_value = CULTIVATOR_AURA_FARM_MINOR * 0.4 // looks pretty real. - var/space_cube_value = CULTIVATOR_AURA_FARM_MINOR * 0.5 // Praise the space cube poster. + var/space_cube_value = CULTIVATOR_AURA_FARM_MINOR * 0.5 // Praise the space cube. var/in_space_value = CULTIVATOR_AURA_FARM_MAJOR // Being out in space basically guarantees 50% charge. // Do we see space turfs? @@ -72,9 +72,11 @@ total += fake_space_value continue - // PRAISE THE CUBE POSTER. IT HAS SPACE ON IT - THAT COUNTS! + // PRAISE THE SPACE CUBE. IT HAS SPACE ON IT - THAT COUNTS! for(var/obj/structure/sign/poster/contraband/space_cube/cube in view(owner_mob)) total += space_cube_value + for(var/obj/item/dice/d6/space/cube in view(owner_mob)) + total += space_cube_value // Are we in space? var/turf/owner_turf = get_turf(owner_mob) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm index a640df2bf8523e..3b0caacef77f42 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm @@ -1,9 +1,9 @@ // bless my rains down with reagents. /datum/power/thaumaturge/conjure_rain name = "Conjure Rain" - desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u of water (for mobs, this is being splashed with that amount instead). \ - Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ - Requires Affinity 3. Higher affinity improves the reagent conversion ratio (10% per affinity)." + desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u water, up to a maximum of 60u spread out across all containers. Mobs are splashed with the same amount and don't count towards this limit. \ + \n Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ + \n Requires Affinity 3. Higher affinity increases the max amount of spreadable reagents by 20u." value = 4 action_path = /datum/action/cooldown/power/thaumaturge/conjure_rain @@ -11,8 +11,8 @@ /datum/action/cooldown/power/thaumaturge/conjure_rain name = "Conjure Rain" - desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u water (for mobs, this is being splashed with that amount instead). \ - Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ " + desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u water, up to a maximum of 60u spread out across all containers. Mobs are splashed with the same amount and don't count towards this limit. \ + \n Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ " button_icon = 'icons/effects/weather_effects.dmi' button_icon_state = "rain_low" @@ -22,15 +22,40 @@ anti_magic_on_target = FALSE use_time_overlay_type = /obj/effect/temp_visual/conjure_rain - use_time = 5 + use_time = 1 SECONDS use_time_flags = IGNORE_USER_LOC_CHANGE // the chem that the base rain uses - var/rain_chem = /datum/reagent/water + var/datum/reagent/rain_chem = /datum/reagent/water // the base conversion ratio of the chem. var/base_chem_ratio = 1 - // how much the ratio increases per affinity level - var/affinity_chem_bonus = 0.1 + // the max amount we put in a single container + var/max_reagents_per_container = 20 + // max amount of reagents we can spread across containers (not including mobs) + var/max_reagents_dupe = 60 + // bonus to max reagents per affinity above 3. + var/affinity_max_reagents = 20 + +// We piggyback into do_use_time to add a telegraph of the rain. +/datum/action/cooldown/power/thaumaturge/conjure_rain/do_use_time(mob/living/user, atom/target) + if(use_time <= 0) + return TRUE + var/turf/target_turf = get_turf(target) + if(!target_turf) + return FALSE + + // we cheekily get the color of the held reagent container so we can color the rain even if we haven't calculated the buffer yet. May not be 100% accurate, but close enuff. + var/rain_color + var/obj/item/reagent_containers/held_container = user.get_active_held_item() + if(istype(held_container) && held_container.reagents?.reagent_list?.len) + rain_color = mix_color_from_reagents(held_container.reagents.reagent_list) + else // no reagent container, default to rain_chem + rain_color = initial(rain_chem.color) + + // displays the telgraphed rain + for(var/turf/area_turf in range(1, target_turf)) + new /obj/effect/temp_visual/thaum_rain_buildup(area_turf, rain_color) + return ..() /datum/action/cooldown/power/thaumaturge/conjure_rain/use_action(mob/living/user, atom/target) var/turf/target_turf = get_turf(target) @@ -51,10 +76,7 @@ var/drain_amount = min(buffer.buffer_volume, synth_volume) if(drain_amount > 0) buffer.reagents.remove_reagent(rain_chem, drain_amount) // 1:1 water consumption - var/chem_ratio = base_chem_ratio + (affinity_chem_bonus * (affinity - required_affinity)) - // in some alt universe you get negative chem ratio - if(chem_ratio < 0) - chem_ratio = 0 + var/chem_ratio = base_chem_ratio var/part = drain_amount / synth_volume for(var/datum/reagent/reagent as anything in held_container.reagents.reagent_list) var/transfer_amount = reagent.volume * part @@ -65,14 +87,24 @@ var/rain_color = mix_color_from_reagents(buffer.reagents.reagent_list) playsound(target, 'sound/effects/splat.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + var/list/obj/item/reagent_containers/area_containers = list() + for(var/turf/area_turf in range(1, target_turf)) + for(var/obj/item/reagent_containers/target_container in area_turf) + if(target_container.reagents) + area_containers += target_container + + var/bonus_affinity = max(0, affinity - 3) + var/max_spread = max_reagents_dupe + (bonus_affinity * affinity_max_reagents) + var/per_container = 0 + + // Get every reagent container in range and calculate how we spread the rain. + if(length(area_containers)) + per_container = min(max_reagents_per_container, max_spread / length(area_containers)) + // every tile in range... for(var/turf/area_turf in range(1, target_turf)) // splash it onto the space. buffer.reagents.expose(area_turf, TOUCH) - // applies it to every reagent container in the area - for(var/obj/item/reagent_containers/target_container in area_turf) - if(target_container.reagents) - buffer.reagents.trans_to(target_container, buffer.buffer_volume, transferred_by = user, copy_only = TRUE) // splashes it onto every mob in the area for(var/mob/living/area_mob in area_turf) buffer.reagents.expose(area_mob, TOUCH) @@ -80,12 +112,14 @@ // rain fx new /obj/effect/temp_visual/thaum_rain(area_turf, rain_color) + // Adds reagents to containers based on the calculated per_container. + if(per_container > 0) + for(var/obj/item/reagent_containers/target_container in area_containers) + buffer.reagents.trans_to(target_container, per_container, transferred_by = user, copy_only = TRUE) + qdel(buffer) return TRUE -// Adds a cast effect, just to make it clear to EVEROYNE we're about to rain some shit down on them. - - // We create a temporary buffer for holding the reagents. /obj/effect/abstract/thaum_rain_buffer name = "resonant beaker" @@ -111,6 +145,18 @@ icon_state = "rain_high" duration = 1 SECONDS +/obj/effect/temp_visual/thaum_rain_buildup + name = "light magical rain" + icon = 'icons/effects/weather_effects.dmi' + icon_state = "rain_low" + duration = 1 SECONDS + +// lets us recolor the rain +/obj/effect/temp_visual/thaum_rain_buildup/Initialize(mapload, set_color) + if(set_color) + add_atom_colour(set_color, FIXED_COLOUR_PRIORITY) + return ..() + /obj/effect/temp_visual/thaum_rain/Initialize(mapload, set_color) if(set_color) add_atom_colour(set_color, FIXED_COLOUR_PRIORITY) 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 064fed7804a691..1df5373cac9fc9 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 @@ -48,36 +48,37 @@ var/turf/current_turf = get_turf(src) if(!current_turf) return + var/turf/old_turf = get_turf(old_loc) // Handless moving objects along with it. - drag_along_movables(current_turf, dir) + drag_along_movables(old_turf, current_turf) // Extinguishes hotspots. Doesn't mess with atmos. extinguish_hotspots_on_turf(current_turf) -/obj/projectile/resonant/gale_blast/proc/drag_along_movables(turf/current_turf, travel_dir) - if(!current_turf || !travel_dir) +/obj/projectile/resonant/gale_blast/proc/drag_along_movables(turf/from_turf, turf/to_turf) + if(!from_turf || !to_turf) return - var/turf/next_turf = get_step(current_turf, travel_dir) - if(!next_turf) + var/travel_dir = get_dir(from_turf, to_turf) + if(!travel_dir) return var/pushed_atoms = 0 // Checks if we're allowed to drag it and if the space can be passed through. - for(var/atom/movable/movable_instance as anything in current_turf) + for(var/atom/movable/movable_instance as anything in from_turf) // We cap the amount of items that can be moved similar to push brooms to prevent you from casting LAGIMUS MAXIMUS. if(pushed_atoms >= THAUMATURGE_GALE_BLAST_PUSH_LIMIT) break - if(!can_wind_drag(movable_instance, current_turf)) + if(!can_wind_drag(movable_instance, from_turf)) continue - if(!movable_instance.CanPass(movable_instance, next_turf, travel_dir)) + if(!movable_instance.CanPass(movable_instance, to_turf, travel_dir)) continue // Drags along the object. - movable_instance.Move(next_turf) + movable_instance.Move(to_turf) // Also extinguishes it. movable_instance.extinguish() pushed_atoms++ From 742b91c484fb1a8352b43a8d2243f2456fc68abf Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 10 Mar 2026 18:25:24 +0100 Subject: [PATCH 130/212] Adds reagent cannon, tweaks so that all augments have company names. --- .../mortal/augmented/_augmented_power.dm | 19 +- .../powers/mortal/augmented/auto_retriever.dm | 2 +- .../mortal/augmented/mental_shielding.dm | 4 +- .../powers/mortal/augmented/pneumatic_arm.dm | 4 +- .../powers/mortal/augmented/reagent_cannon.dm | 199 ++++++++++++++++++ tgstation.dme | 1 + 6 files changed, 209 insertions(+), 20 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm index 097f1be33af889..23364032b17fb5 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm @@ -42,13 +42,14 @@ right_implant.slot = ORGAN_SLOT_RIGHT_ARM_AUG right_implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED) return - var/arm_zone = get_assigned_arm_zone(client_source) - if(arm_zone == BODY_ZONE_L_ARM) + else if(left_match) implant.zone = BODY_ZONE_L_ARM implant.slot = ORGAN_SLOT_LEFT_ARM_AUG - else if(arm_zone == BODY_ZONE_R_ARM) + else if(right_match) implant.zone = BODY_ZONE_R_ARM implant.slot = ORGAN_SLOT_RIGHT_ARM_AUG + else + return implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED) return @@ -120,18 +121,6 @@ GLOBAL_LIST_INIT(arm_augment_values, generate_arm_augment_values()) values += initial(power_type.name) return values -// This gets the /datum/preference/choiced for left and right augments telling us which arm is where. -/datum/power/augmented/proc/get_assigned_arm_zone(client/client_source) - if(!client_source) - return null - var/augment_left = client_source.prefs?.read_preference(/datum/preference/choiced/augment_left) - var/augment_right = client_source.prefs?.read_preference(/datum/preference/choiced/augment_right) - if(augment_left && augment_left != AUGMENTED_NO_AUGMENT && augment_matches_pref(augment_left)) - return BODY_ZONE_L_ARM - if(augment_right && augment_right != AUGMENTED_NO_AUGMENT && augment_matches_pref(augment_right)) - return BODY_ZONE_R_ARM - return null - // Bit of validation to make sure the augment is in fact in the user's prefs. /datum/power/augmented/proc/augment_matches_pref(value) if(isnull(value) || value == AUGMENTED_NO_AUGMENT || !augment) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm index a2711eb2cd5fad..e4f937f619a955 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm @@ -8,7 +8,7 @@ Once it fires, a warning message is issued over the radio. The teleportation sets the quality to 0%, and can be interupted by Epinephrine in the bloodstream, EMP, or healing you above the critical threshold; after which it loses 25% quality and enters a several minute cooldown period.\ \n Decreases in quality twice as fast. Lower quality decreases the speed of the teleport." - value = 5 + value = 6 augment = /obj/item/organ/cyberimp/chest/auto_retriever /obj/item/organ/cyberimp/chest/auto_retriever 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 37069ba2a2b721..5dd13c8339e044 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 @@ -3,7 +3,7 @@ */ /datum/power/augmented/mental_shielding name = "Premium TNFL Mental Shielding Implant" - desc = " Based on the nullifying effects that tinfoil has on certain magical phenomena, this dermal implant creates a protective coating around your brain.\ + desc = " Based on the nullifying effects that tinfoil has on certain magical phenomena, this dermal implant created by Oracle Neuro-Systems creates a protective coating around your brain.\ \n Creates a barrier that blocks resonant based scrying, as well as mental abilities used on you (including magic stronger than Resonant).\ \n Blocking mental abilities consumes quality, increasing consumption rate the lower the quality is." @@ -12,7 +12,7 @@ /obj/item/organ/cyberimp/brain/mental_shielding name = "TNFL Mental Shielding Implant" - desc = "Based on the nullifying effects that tinfoil has on certain magical phenomena, this dermal implant creates a protective coating around your brain. \ + desc = "Based on the nullifying effects that tinfoil has on certain magical phenomena, this dermal implant created by Oracle Neuro-Systems creates a protective coating around your brain. \ Creates a barrier that blocks resonant based scrying, as well as mental abilities used on you (including magic stronger than Resonant). \ Blocking mental abilities consumes quality, increasing consumption rate the lower the quality is." icon_state = "brain_implant_connector" diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm index 8fc0f2a38f3aa4..199f588ab2d946 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm @@ -3,7 +3,7 @@ */ /datum/power/augmented/pneumatic_arm name = "Premium DSTR Pneumatic Arm" - desc = "A popular choice for the augmented bodyguards, given it turns your arms into weapons; ideal for places that don't allow weapons. Passively increases your punch damage by +5 with that arm. \ + desc = "A popular choice for the augmented bodyguards and manufactured by Praetor Dynamics. Passively increases your punch damage by +5 with that arm. \ \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 2 spaces (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)." @@ -12,7 +12,7 @@ /obj/item/organ/cyberimp/arm/pneumatic_arm name = "DSTR Pneumatic Arm" - desc = "A popular choice for the augmented bodyguards, given it turns your arms into weapons; ideal for places that don't allow weapons. Passively increases your punch damage by +5 with that arm. \ + desc = "A popular choice for the augmented bodyguards and manufactured by Praetor Dynamics. Passively increases your punch damage by +5 with that arm. \ \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 2 spaces (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)." icon_state = "toolkit_generic" diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm new file mode 100644 index 00000000000000..376e12445e527c --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm @@ -0,0 +1,199 @@ +/* + Spray reagents EVERYWHERE! +*/ +/datum/power/augmented/reagent_cannon + name = "Premium SPRY Reagent Cannon" + desc = "Usually included in various company contracts, those who work in mega-scale botanics and cleaning need to push for optimal efficiency. Manufcatured by Nex-Zephyr, this beauty will be your lifelong replacement of a spray bottle.\ + \n When activated, transform your arm into a chemsprayer, allowing you to deploy chemicals rapidly in a large area. Capable of containing up to 300 chemicals. \ + \n Because this is an incredibly invasive augment, this requires a cybernetic arm to wield effectively. Your arm will be replaced with a synthetic variant at roundstart to facilitate this." + + value = 5 + augment = /obj/item/organ/cyberimp/arm/toolkit/reagent_cannon + +// Replaces the existing arm with a robot limb. +/datum/power/augmented/reagent_cannon/add_unique(client/client_source) + var/mob/living/carbon/human/human_holder = power_holder + if(!augment || !human_holder) + return + var/augment_left = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_left) + var/augment_right = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_right) + var/left_match = augment_matches_pref(augment_left) + var/right_match = augment_matches_pref(augment_right) + + if(left_match) + replace_arm_with_robot(human_holder, BODY_ZONE_L_ARM) + if(right_match) + replace_arm_with_robot(human_holder, BODY_ZONE_R_ARM) + return ..() + +/datum/power/augmented/reagent_cannon/proc/replace_arm_with_robot(mob/living/carbon/human/human_holder, arm_zone) + if(!human_holder) + return + var/obj/item/bodypart/existing = human_holder.get_bodypart(arm_zone) + if(existing && (existing.bodytype & BODYTYPE_ROBOTIC)) // we already have robo arms. + return + if(arm_zone == BODY_ZONE_L_ARM) + human_holder.del_and_replace_bodypart(new /obj/item/bodypart/arm/left/robot, special = TRUE) + else if(arm_zone == BODY_ZONE_R_ARM) + human_holder.del_and_replace_bodypart(new /obj/item/bodypart/arm/right/robot, special = TRUE) + +/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon + name = "Premium SPRY Reagent Cannon" + desc = "Usually included in various company contracts, those who work in mega-scale botanics and cleaning need to push for optimal efficiency. Manufcatured by Nex-Zephyr, this beauty will be your lifelong replacement of a spray bottle.\ + \n When activated, transform your arm into a chemsprayer, allowing you to deploy chemicals rapidly in a large area. Capable of containing up to 300 chemicals. \ + \n Because this is an incredibly invasive augment, this requires a cybernetic arm to wield effectively." + icon = 'icons/obj/weapons/guns/ballistic.dmi' + icon_state = "chemsprayer" + + actions_types = list(/datum/action/item_action/organ_action/premium/use) + premium = TRUE + + items_to_create = list(/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon) + + // Chance not to consume quality + var/quality_chance = 40 + + // EMP cooldown + COOLDOWN_DECLARE(emp_reenable_cooldown) + var/emp_cooldown = 30 SECONDS + +/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/Initialize(mapload) + . = ..() + if(premium_component) + premium_component.refurb_parts = list( + /obj/item/stack/sheet/plastic = 5, + /obj/item/stack/sheet/iron = 2, + /obj/item/stack/cable_coil = 2, + /obj/item/stock_parts/matter_bin/bluespace = 1) + +// Only fits in cybernetic arms because fluff and also how the fuck does it fit elsewhere. +/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/on_mob_insert(mob/living/carbon/arm_owner) + . = ..() + if(!has_robotic_arm()) + to_chat(arm_owner, span_warning("Your [name] does not fit in a non-cybernetic arm!")) + Remove(arm_owner, special = TRUE) + if(arm_owner?.loc) + forceMove(get_turf(arm_owner)) + return + +// On EMP +/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/emp_act(severity) + . = ..() + if(. & EMP_PROTECT_SELF) + return + if(premium_component) + premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) + Retract() + COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown) + premium_component?.update_quality_actions() + to_chat(owner, span_warning("Your [name] becomes disabled!")) + +/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/use_action() + if(!owner) + return FALSE + if(!has_robotic_arm()) + to_chat(owner, span_warning("Your [name] can't function with a non-cybernetic arm.")) + return FALSE + if(!premium_component?.can_function()) + to_chat(owner, span_warning("Your [name] fails to respond; it seems broken!")) + return FALSE + if(!COOLDOWN_FINISHED(src, emp_reenable_cooldown) && !is_action_active()) + to_chat(owner, span_warning("Your [name] is temporarily disabled from EMP interference.")) + return FALSE + var/obj/item/active = active_item + if(active && !(active in src)) + return Retract() + if(!LAZYLEN(contents)) + return FALSE + Extend(contents[1]) + return TRUE + +/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/is_action_active() + return active_item && !(active_item in src) + +// All around check if theres a robotic arm. +/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/proc/has_robotic_arm() + var/obj/item/bodypart/arm_part = hand + if(!arm_part) + return FALSE + return (arm_part.bodytype & BODYTYPE_ROBOTIC) + +// Chance to deduct quality based on amount used. +/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/proc/on_spray_used(reagents_used) + if(!premium_component) + return + if(!premium_component.can_function()) + return + var/efficiency = premium_component.get_efficiency() + var/chance_no_consume = (quality_chance * efficiency) - max(reagents_used, 0) + if(prob(clamp(chance_no_consume, 0, 100))) + return + premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_TRIVIAL * 2) + +// The chem sprayer specifically designed for the augment. +/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon + name = "Premium SPRY Reagent Cannon" + desc = "A chem sprayer integrated into a premium arm augment. Really its a miracle you even have an operable hand with the size of this thing." + var/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/host_implant + /// 0 = spray wide, 1 = stream wide, 2 = spray focused, 3 = stream focused + var/mode = 0 + /// Focused mode only targets the center tile (1-wide) + var/focused_mode = FALSE + +/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/Initialize(mapload) + . = ..() + if(istype(loc, /obj/item/organ/cyberimp/arm/toolkit/reagent_cannon)) + host_implant = loc + +// We use a delta to get the amount we used and then pass that along to the augment for quality degredation. +/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/try_spray(atom/target, mob/user) + var/before = reagents?.total_volume || 0 + . = ..() + if(.) + var/after = reagents?.total_volume || 0 + var/used = max(before - after, 0) + host_implant?.on_spray_used(used) + return . + +// Allows us to basically toggle between 1x or 3x spray. +/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/spray(atom/A, mob/user) + var/turf/T = get_turf(A) + if(focused_mode) + call(src, /obj/item/reagent_containers/spray/proc/spray)(T, user) // only way we can get a 1x1 spray because the chemsprayer is our parent. + return + var/direction = get_dir(src, A) + var/turf/T1 = get_step(T, turn(direction, 90)) + var/turf/T2 = get_step(T, turn(direction, -90)) + var/list/the_targets = list(T, T1, T2) + + for(var/i in 1 to 3) // intialize sprays + if(reagents.total_volume < 1) + return + ..(the_targets[i], user) + +// Allows us to switch between focused (1x wide) or unfocused (3x wide) +/obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/toggle_stream_mode(mob/user) + if(stream_range == spray_range || !stream_range || !spray_range || possible_transfer_amounts.len > 2 || !can_toggle_range) + return + mode = (mode + 1) % 4 + switch(mode) + if(0) + stream_mode = FALSE + focused_mode = FALSE + current_range = spray_range + to_chat(user, span_notice("You switch the nozzle setting to \"spray\".")) + if(1) + stream_mode = TRUE + focused_mode = FALSE + current_range = stream_range + to_chat(user, span_notice("You switch the nozzle setting to \"stream\".")) + if(2) + stream_mode = FALSE + focused_mode = TRUE + current_range = spray_range + to_chat(user, span_notice("You switch the nozzle setting to \"spray (focused)\".")) + if(3) + stream_mode = TRUE + focused_mode = TRUE + current_range = stream_range + to_chat(user, span_notice("You switch the nozzle setting to \"stream (focused)\".")) diff --git a/tgstation.dme b/tgstation.dme index cf321ecbcd1d92..cee7222def1068 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7436,6 +7436,7 @@ #include "modular_doppler\modular_powers\code\powers\mortal\augmented\auto_retriever.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\mental_shielding.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\pneumatic_arm.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\reagent_cannon.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\surgery\_premium_surgery.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\surgery\_premium_surgery_steps.dm" From 8bc8a6eba7a16ebed1f31a86732302753b8a0283 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 10 Mar 2026 19:36:48 +0100 Subject: [PATCH 131/212] Moved premium augment logic to organ as to allow organ replacements. Add precognition eyes, which let you dodge literally everything. --- .../items/devices/scanners/health_analyzer.dm | 24 ++- code/modules/surgery/organs/_organ.dm | 31 ++++ .../internal/cyberimp/augments_internal.dm | 20 --- .../mortal/augmented/_premium_action.dm | 14 +- .../mortal/augmented/_premium_augment.dm | 48 +++--- .../powers/mortal/augmented/auto_retriever.dm | 4 +- .../mortal/augmented/precognition_eyes.dm | 147 ++++++++++++++++++ .../powers/mortal/augmented/reagent_cannon.dm | 3 +- .../surgery/_premium_surgery_steps.dm | 18 +-- tgstation.dme | 1 + 10 files changed, 231 insertions(+), 79 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm diff --git a/code/game/objects/items/devices/scanners/health_analyzer.dm b/code/game/objects/items/devices/scanners/health_analyzer.dm index fd2d2af87d1306..e0af4589d26b7a 100644 --- a/code/game/objects/items/devices/scanners/health_analyzer.dm +++ b/code/game/objects/items/devices/scanners/health_analyzer.dm @@ -339,20 +339,18 @@ var/list/cyberimps for(var/obj/item/organ/target_organ as anything in humantarget.organs) if(IS_ROBOTIC_ORGAN(target_organ) && !(target_organ.organ_flags & ORGAN_HIDDEN)) - // DOPPLER ADDITION START - Shows quality for special augments from Powers + // DOPPLER ADDITION START - Adds Premium augment support to organs. var/line = target_organ.examine_title(user) - if(istype(target_organ, /obj/item/organ/cyberimp)) - var/obj/item/organ/cyberimp/cy = target_organ - if(cy.premium_component) - var/quality = round(cy.premium_component.quality) - var/quality_text = "quality: [quality]%" - quality_text = conditional_tooltip(quality_text, "Premium augment quality affects performance. At 0% it must be refurbished. \ - Using premium augment maintenance surgery on the appropriate bodypart ([parse_zone(target_organ.zone)]) will restore up to 75% so long as it is not broken. \ - Removing the augment with organ manipulation and refurbishing it in-hand will restore up to 100% (examine the augment for instructions).", tochat) - if(quality <= 0) - line = "[line] ([quality_text] refurbish required)" - else - line = "[line] ([quality_text])" + if(target_organ.premium_component) // DOPPLER ADDITION + var/quality = round(target_organ.premium_component.quality) + var/quality_text = "quality: [quality]%" + quality_text = conditional_tooltip(quality_text, "Premium augment quality affects performance. At 0% it must be refurbished. \ + Using premium augment maintenance surgery on the appropriate bodypart ([parse_zone(target_organ.zone)]) will restore up to 75% so long as it is not broken. \ + Removing the augment with organ manipulation and refurbishing it in-hand will restore up to 100% (examine the augment for instructions).", tochat) + if(quality <= 0) + line = "[line] ([quality_text] refurbish required)" + else + line = "[line] ([quality_text])" LAZYADD(cyberimps, line) // DOPPLER ADDITION END if(target_organ.organ_flags & ORGAN_MUTANT) diff --git a/code/modules/surgery/organs/_organ.dm b/code/modules/surgery/organs/_organ.dm index 69f0809dc89d0a..8be3ca0c5da86d 100644 --- a/code/modules/surgery/organs/_organ.dm +++ b/code/modules/surgery/organs/_organ.dm @@ -76,6 +76,13 @@ /// The maximum cells we can spawn var/cells_maximum = 0 + // DOPPLER ADDITION START - Adds Premium augment support to organs. + /// Whether this organ supports premium augment quality mechanics. + var/premium = FALSE + /// Component for premium augment quality mechanics. + var/datum/component/premium_augment/premium_component + // DOPPLER ADDITION END + // Players can look at prefs before atoms SS init, and without this // they would not be able to see external organs, such as moth wings. // This is also necessary because assets SS is before atoms, and so @@ -101,6 +108,11 @@ INITIALIZE_IMMEDIATE(/obj/item/organ) if(cell_line && (organ_flags & ORGAN_ORGANIC)) AddElement(/datum/element/swabable, cell_line, cell_line_amount = rand(cells_minimum, cells_maximum)) + // DOPPLER ADDITION START - Adds Premium augment support to organs. + if(premium && !premium_component) + premium_component = AddComponent(/datum/component/premium_augment) + // DOPPLER ADDITION END + START_PROCESSING(SSobj, src) /obj/item/organ/Destroy() @@ -196,6 +208,10 @@ INITIALIZE_IMMEDIATE(/obj/item/organ) . = ..() . += zones_tip() + // DOPPLER ADDITION START - Adds Premium augment support to organs. + if(premium_component) + . += premium_component.get_refurb_examine_lines(src) + // DOPPLER ADDITION END if(HAS_MIND_TRAIT(user, TRAIT_ENTRAILS_READER) || isobserver(user)) if(HAS_TRAIT(src, TRAIT_CLIENT_STARTING_ORGAN)) @@ -211,6 +227,21 @@ INITIALIZE_IMMEDIATE(/obj/item/organ) return . += span_warning("[src] is starting to look discolored.") +// DOPPLER ADDITION START - Adds Premium augment support to organs. +/obj/item/organ/attackby(obj/item/tool, mob/user, params) + if(premium_component && premium_component.handle_refurbish_interaction(user, tool, src)) + return + return ..() + +/// Default premium action hook. Override per organ. +/obj/item/organ/proc/use_action() + return FALSE + +/// Premium augments can override this to report their "on" state for button overlays. +/obj/item/organ/proc/is_action_active() + return FALSE +// DOPPLER ADDITION END + /// Returns a line to be displayed regarding valid insertion zones /obj/item/organ/proc/zones_tip() if (!valid_zones) diff --git a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm index 37eba9260ee4e5..71ab09f5b22b72 100644 --- a/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm +++ b/code/modules/surgery/organs/internal/cyberimp/augments_internal.dm @@ -14,30 +14,10 @@ /// Bodypart overlay we're going to apply to whoever we're implanted into var/datum/bodypart_overlay/augment/bodypart_aug = null - var/premium = FALSE // DOPPLER EDIT ADDITION - Allows Premium augments to hook into cyberimp and still be semi-modular - var/datum/component/premium_augment/premium_component // DOPPLER EDIT ADDITION - Component for quality mechanics. - -// DOPPLER ADDITION START - Handles powers related additions /obj/item/organ/cyberimp/Initialize(mapload) . = ..() if (aug_overlay) bodypart_aug = new(src) - if(premium && !premium_component) - premium_component = AddComponent(/datum/component/premium_augment) -/// Default premium action hook. Override per implant. -/obj/item/organ/cyberimp/proc/use_action() - return FALSE -// Handles refurbishing -/obj/item/organ/cyberimp/attackby(obj/item/tool, mob/user, params) - if(premium_component && premium_component.handle_refurbish_interaction(user, tool, src)) - return - return ..() -// Extra details for examining premium parts. -/obj/item/organ/cyberimp/examine(mob/user) - . = ..() - if(premium_component) - . += premium_component.get_refurb_examine_lines(src) -// DOPPLER ADDITION END /obj/item/organ/cyberimp/Destroy() QDEL_NULL(bodypart_aug) 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 57db170acc05dc..8ff3e73ca1caf2 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 @@ -16,7 +16,7 @@ ..() if(active_overlay_icon_state) base_overlay_icon_state ||= overlay_icon_state - var/obj/item/organ/cyberimp/organ_target = target + var/obj/item/organ/organ_target = target premium_component = organ_target?.premium_component premium_component?.register_quality_action(src) update_quality_overlay() @@ -28,7 +28,7 @@ /datum/action/item_action/organ_action/premium/Grant(mob/grant_to) . = ..() if(!premium_component) - var/obj/item/organ/cyberimp/organ_target = target + var/obj/item/organ/organ_target = target premium_component = organ_target?.premium_component premium_component?.register_quality_action(src) update_arm_label() @@ -68,7 +68,7 @@ /datum/action/item_action/organ_action/premium/IsAvailable(feedback = FALSE) . = ..() if(!premium_component) - var/obj/item/organ/cyberimp/organ_target = target + var/obj/item/organ/organ_target = target premium_component = organ_target?.premium_component return . @@ -104,7 +104,7 @@ // This is specifically to flip right-side arm augments to look visually distinct from the other button (since you can have 2 arm augments). /datum/action/item_action/organ_action/premium/is_action_active(atom/movable/screen/movable/action_button/current_button) - var/obj/item/organ/cyberimp/organ_target = target + var/obj/item/organ/organ_target = target return organ_target?.is_action_active() || FALSE /datum/action/item_action/organ_action/premium/use @@ -126,13 +126,9 @@ return "" /datum/action/item_action/organ_action/premium/use/do_effect(trigger_flags) - var/obj/item/organ/cyberimp/organ_target = target + var/obj/item/organ/organ_target = target if(!organ_target) return FALSE organ_target.use_action() build_all_button_icons(UPDATE_BUTTON_OVERLAY | UPDATE_BUTTON_STATUS) return TRUE - -// Premium augments can override this to report their "on" state for button overlays. -/obj/item/organ/cyberimp/proc/is_action_active() - return FALSE diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm index 7fb46b6611b19d..81e58db54ee3db 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm @@ -1,8 +1,8 @@ // Responsible for handling most of the premium augment interactions away from the base cyberimplant. /datum/component/premium_augment dupe_mode = COMPONENT_DUPE_UNIQUE - /// Host implant that owns this premium augment logic. - var/obj/item/organ/cyberimp/host + /// Host organ that owns this premium augment logic. + var/obj/item/organ/host /// Current quality percentage (0..100) var/quality = AUGMENTED_PREMIUM_QUALITY_START /// Passive decay configuration. @@ -28,7 +28,7 @@ var/list/refurb_parts_remaining /datum/component/premium_augment/Initialize() - if(!istype(parent, /obj/item/organ/cyberimp)) + if(!istype(parent, /obj/item/organ)) return COMPONENT_INCOMPATIBLE host = parent if(!host.premium_component) @@ -127,11 +127,11 @@ adjust_quality(amount, AUGMENTED_PREMIUM_QUALITY_MAX) /// Handle refurbish interactions while the implant is out of the body. -/datum/component/premium_augment/proc/handle_refurbish_interaction(mob/user, obj/item/tool, obj/item/organ/cyberimp/implant) - if(!user || !tool || !implant) +/datum/component/premium_augment/proc/handle_refurbish_interaction(mob/user, obj/item/tool, obj/item/organ/augment) + if(!user || !tool || !augment) return FALSE - if(implant.owner) // I don't even know how you would do this; the manual says to take it out first >:C - to_chat(user, span_warning("You need to remove [implant] before refurbishing it.")) + if(augment.owner) // I don't even know how you would do this; the manual says to take it out first >:C + to_chat(user, span_warning("You need to remove [augment] before refurbishing it.")) return TRUE var/step = get_refurb_step() if(!step) @@ -140,10 +140,10 @@ switch(step) if(AUGMENTED_REFURBISH_OPEN) if(tool.tool_behaviour != TOOL_SCREWDRIVER) - to_chat(user, span_warning("You need a screwdriver to open [implant]'s casing.")) + to_chat(user, span_warning("You need a screwdriver to open [augment]'s casing.")) return TRUE - to_chat(user, span_notice("You open [implant]'s casing.")) - tool.play_tool_sound(implant) + to_chat(user, span_notice("You open [augment]'s casing.")) + tool.play_tool_sound(augment) advance_refurb_step() return TRUE @@ -163,7 +163,7 @@ // Wrong item, right subtype. if(!needed) - to_chat(user, span_warning("[stack] doesn't fit [implant]'s parts.")) + to_chat(user, span_warning("[stack] doesn't fit [augment]'s parts.")) return TRUE // Not enough in a stack @@ -179,7 +179,7 @@ // Wrong item if(!needed) - to_chat(user, span_warning("[tool] doesn't fit [implant]'s parts.")) + to_chat(user, span_warning("[tool] doesn't fit [augment]'s parts.")) return TRUE qdel(tool) @@ -190,41 +190,41 @@ refurb_parts_remaining -= typepath else refurb_parts_remaining[typepath] = needed - to_chat(user, span_notice("You replace worn parts inside [implant].")) - tool.play_tool_sound(implant) + to_chat(user, span_notice("You replace worn parts inside [augment].")) + tool.play_tool_sound(augment) if(!LAZYLEN(refurb_parts_remaining)) advance_refurb_step() return TRUE if(AUGMENTED_REFURBISH_CALIBRATE) if(tool.tool_behaviour != TOOL_MULTITOOL) - to_chat(user, span_warning("You need a multitool to calibrate [implant].")) + to_chat(user, span_warning("You need a multitool to calibrate [augment].")) return TRUE - to_chat(user, span_notice("You calibrate [implant]'s diagnostics.")) - tool.play_tool_sound(implant) + to_chat(user, span_notice("You calibrate [augment]'s diagnostics.")) + tool.play_tool_sound(augment) advance_refurb_step() return TRUE if(AUGMENTED_REFURBISH_CLOSE) if(tool.tool_behaviour != TOOL_SCREWDRIVER) - to_chat(user, span_warning("You need a screwdriver to close [implant]'s casing.")) + to_chat(user, span_warning("You need a screwdriver to close [augment]'s casing.")) return TRUE refurbish(AUGMENTED_PREMIUM_QUALITY_MAX) - tool.play_tool_sound(implant) + tool.play_tool_sound(augment) reset_refurb() - to_chat(user, span_notice("You finish refurbishing [implant]. Looks about as new as it can get.")) + to_chat(user, span_notice("You finish refurbishing [augment]. Looks about as new as it can get.")) return TRUE return FALSE /// Returns lines to show when examining a premium augment for refurbishing. -/datum/component/premium_augment/proc/get_refurb_examine_lines(obj/item/organ/cyberimp/implant) +/datum/component/premium_augment/proc/get_refurb_examine_lines(obj/item/organ/augment) var/list/lines = list() - if(!implant) + if(!augment) return lines lines += span_notice("Premium quality: [round(quality)]%.") - if(implant.owner) - lines += span_warning("Remove [implant] before refurbishing it.") + if(augment.owner) + lines += span_warning("Remove [augment] before refurbishing it.") return lines var/step = get_refurb_step() diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm index e4f937f619a955..f5b43c3cf9ba74 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm @@ -87,7 +87,7 @@ return if(!COOLDOWN_FINISHED(src, teleport_cooldown)) return - if(owner.reagents?.has_reagent(/datum/reagent/medicine/epinephrine)) + if(owner.reagents?.has_reagent(/datum/reagent/medicine/epinephrine) || owner.reagents?.has_reagent(/datum/reagent/medicine/atropine)) return if(owner.stat >= SOFT_CRIT && owner.stat != DEAD) start_teleport() @@ -152,7 +152,7 @@ return FALSE if(owner.stat < SOFT_CRIT) return TRUE - if(owner.reagents?.has_reagent(/datum/reagent/medicine/epinephrine)) + if(owner.reagents?.has_reagent(/datum/reagent/medicine/epinephrine) || owner.reagents?.has_reagent(/datum/reagent/medicine/atropine)) return TRUE return FALSE diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm new file mode 100644 index 00000000000000..f940b73dfbb7a1 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm @@ -0,0 +1,147 @@ +/* + Its an arm that makes you punch harder and be activated to punch EVEN HARDER. +*/ +/datum/power/augmented/precognition_eyes + name = "Premium PRCG Precognitive Scanners" + desc = "Though some market it as being able to see the future, this invention by Oracle Neuro-Systems is instead a specialized AI recognition model hooked into a BULLET DODGER skillchip, allowing you to automatically dodge any incoming projectiles.\ + \n This doesn't come without drawbacks, as the visual load is exhausting and suffers from the same drawbacks as the skillchip by tiring you out, causing more exhaustion than usual.\ + \n Requires a BULLET DODGER Skillchip to function; comes pre-packaged with one at roundstart." + + value = 8 + augment = /obj/item/organ/eyes/robotic/precognition_eyes + +/obj/item/organ/eyes/robotic/precognition_eyes + name = "Premium PRCG Precognitive Scanners" + desc = "Though some market it as being able to see the future, this invention by Oracle Neuro-Systems is instead a specialized AI recognition model hooked into a BULLET DODGER skillchip, allowing you to automatically dodge any incoming projectiles.\ + \n This doesn't come without drawbacks, as the visual load is exhausting and suffers from the same drawbacks as the skillchip by tiring you out, causing more exhaustion than usual.\ + \n Requires a BULLET DODGER Skillchip to function." + icon_state = "eyes_cyber_xray" + + actions_types = list(/datum/action/item_action/organ_action/premium/use) + premium = TRUE + var/enabled = TRUE + + /// Skillchip installed by this augment. + var/obj/item/skillchip/installed_chip + /// Did we add an extra skillchip slot? + var/added_skillchip_slot = FALSE + // The minimum stamloss gained from this. Normally it is the projectile's damage * efficiency. + var/dodge_stamloss = 30 // higher than normal taunting. Git gud. + // EMP cooldown + COOLDOWN_DECLARE(emp_reenable_cooldown) + var/emp_cooldown = 30 SECONDS + + +/obj/item/organ/eyes/robotic/precognition_eyes/Initialize(mapload) + . = ..() + if(premium_component) + premium_component.refurb_parts = list( + /obj/item/stack/sheet/glass = 2, + /obj/item/stack/cable_coil = 1, + /obj/item/stock_parts/scanning_module/triphasic = 1) + +// Listeners if we are about to be hit by projectiles. +/obj/item/organ/eyes/robotic/precognition_eyes/on_mob_insert(mob/living/carbon/owner_mob) + . = ..() + grant_matrix_taunt(owner_mob) + RegisterSignal(owner_mob, COMSIG_PROJECTILE_PREHIT, PROC_REF(on_projectile_prehit)) + +/obj/item/organ/eyes/robotic/precognition_eyes/on_mob_remove(mob/living/carbon/owner_mob) + . = ..() + if(owner_mob) + UnregisterSignal(owner_mob, COMSIG_PROJECTILE_PREHIT) + remove_matrix_taunt(owner_mob) + +// Grants the skillchip +/obj/item/organ/eyes/robotic/precognition_eyes/proc/grant_matrix_taunt(mob/living/carbon/owner_mob) + if(!owner_mob || installed_chip) + return + var/obj/item/organ/brain/brain = owner_mob.get_organ_slot(ORGAN_SLOT_BRAIN) + if(!brain) + return + if(has_matrix_taunt(brain)) + return + brain.max_skillchip_slots += 1 + added_skillchip_slot = TRUE + installed_chip = new /obj/item/skillchip/matrix_taunt() + owner_mob.implant_skillchip(installed_chip, force = TRUE) + installed_chip.try_activate_skillchip(silent = TRUE, force = TRUE) + +// Removes the skillchip +/obj/item/organ/eyes/robotic/precognition_eyes/proc/remove_matrix_taunt(mob/living/carbon/owner_mob) + if(!owner_mob) + return + var/obj/item/organ/brain/brain = owner_mob.get_organ_slot(ORGAN_SLOT_BRAIN) + if(added_skillchip_slot && brain) + brain.max_skillchip_slots = max(brain.max_skillchip_slots - 1, 0) + brain.update_skillchips() + added_skillchip_slot = FALSE + if(installed_chip) + owner_mob.remove_skillchip(installed_chip, silent = TRUE) + QDEL_NULL(installed_chip) + +// Checks if we have the skillchip. +/obj/item/organ/eyes/robotic/precognition_eyes/proc/has_matrix_taunt(obj/item/organ/brain/brain) + if(!brain || !length(brain.skillchips)) + return FALSE + for(var/obj/item/skillchip/skillchip as anything in brain.skillchips) + if(istype(skillchip, /obj/item/skillchip/matrix_taunt)) + return TRUE + return FALSE + +// On EMP +/obj/item/organ/eyes/robotic/precognition_eyes/emp_act(severity) + . = ..() + if(. & EMP_PROTECT_SELF) + return + if(premium_component) + premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) + enabled = FALSE + COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown) + premium_component?.update_quality_actions() + to_chat(owner, span_warning("Your [name] becomes disabled!")) + +// On using the action. +/obj/item/organ/eyes/robotic/precognition_eyes/use_action() + if(!owner) + return FALSE + if(!enabled && !COOLDOWN_FINISHED(src, emp_reenable_cooldown)) + to_chat(owner, span_warning("Your [name] is temporarily disabled from EMP interference.")) + return FALSE + enabled = !enabled + if(enabled) + to_chat(owner, span_notice("Your [name] is toggled on; it will now auto-dodge projectiles.")) + else + to_chat(owner, span_notice("Your [name] is toggled off.")) + return enabled + +/obj/item/organ/eyes/robotic/precognition_eyes/is_action_active() + return enabled + +// Applies the dodge effect prehit. +/obj/item/organ/eyes/robotic/precognition_eyes/proc/on_projectile_prehit(mob/living/source, obj/projectile/proj) + SIGNAL_HANDLER + if(source != owner) + return NONE + if(!enabled) + return NONE + if(!premium_component?.can_function()) + return NONE + if(source.stat != CONSCIOUS || HAS_TRAIT(source, TRAIT_INCAPACITATED)) + return NONE + if(HAS_TRAIT(source, TRAIT_UNHITTABLE_BY_PROJECTILES)) + return NONE + ADD_TRAIT(source, TRAIT_UNHITTABLE_BY_PROJECTILES, AUGMENTATION_TRAIT) + + // stam + quality loss. + var/efficiency = premium_component?.get_efficiency() || 1 + var/base_cost = dodge_stamloss + // If the projectile deals more damage, we use that for stamina cost instead of dodge_stamloss. + if(proj) + base_cost = max(base_cost, proj.damage) + source.adjustStaminaLoss(round(base_cost * (1 / max(efficiency, 0.01)))) + premium_component?.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) + source.visible_message(span_warning("[source] dodges the [proj] with little effort!"), span_danger("You automatically dodge the [proj]!")) + + addtimer(TRAIT_CALLBACK_REMOVE(source, TRAIT_UNHITTABLE_BY_PROJECTILES, AUGMENTATION_TRAIT), TAUNT_EMOTE_DURATION * 1.5) + return PROJECTILE_INTERRUPT_HIT diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm index 376e12445e527c..356e9eca677f8b 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm @@ -130,10 +130,11 @@ return premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_TRIVIAL * 2) + // The chem sprayer specifically designed for the augment. /obj/item/reagent_containers/spray/chemsprayer/reagent_cannon name = "Premium SPRY Reagent Cannon" - desc = "A chem sprayer integrated into a premium arm augment. Really its a miracle you even have an operable hand with the size of this thing." + desc = "A chem sprayer integrated into a premium arm augment. Really its a miracle you even have an operable hand with the size of this thing. Comes with a 'focused' mode which tightens the spread of the cannon." var/obj/item/organ/cyberimp/arm/toolkit/reagent_cannon/host_implant /// 0 = spray wide, 1 = stream wide, 2 = spray focused, 3 = stream focused var/mode = 0 diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm index 5008c8abaf35ce..82e35fd73c331c 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/surgery/_premium_surgery_steps.dm @@ -19,13 +19,12 @@ var/list/organs = target.get_organs_for_zone(target_zone) var/list/premium_augments = list() for(var/obj/item/organ/organ as anything in organs) - var/obj/item/organ/cyberimp/implant = organ - if(istype(implant) && implant.premium) - premium_augments += implant + if(organ.premium) + premium_augments += organ return premium_augments /datum/surgery_step/premium_augment_access/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery) - var/obj/item/organ/cyberimp/target_implant + var/obj/item/organ/target_implant var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery if(istype(premium_surgery)) target_implant = premium_surgery.get_selected_premium(user, target, target_zone, tool) @@ -52,7 +51,7 @@ display_pain(target, "You feel a sharp, uncomfortable pressure in your [target.parse_zone_with_bodypart(target_zone)]!") /datum/surgery_step/premium_augment_access/success(mob/living/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery, default_display_results = FALSE) - var/obj/item/organ/cyberimp/target_implant + var/obj/item/organ/target_implant var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery if(istype(premium_surgery)) target_implant = premium_surgery.selected_premium @@ -91,13 +90,12 @@ var/list/organs = target.get_organs_for_zone(target_zone) var/list/premium_augments = list() for(var/obj/item/organ/organ as anything in organs) - var/obj/item/organ/cyberimp/implant = organ - if(istype(implant) && implant.premium) - premium_augments += implant + if(organ.premium) + premium_augments += organ return premium_augments /datum/surgery_step/premium_augment_maintenance/preop(mob/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery) - var/obj/item/organ/cyberimp/target_implant + var/obj/item/organ/target_implant var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery if(istype(premium_surgery)) target_implant = premium_surgery.get_selected_premium(user, target, target_zone, tool) @@ -129,7 +127,7 @@ display_pain(target, "You feel a sharp, uncomfortable pressure in your [target.parse_zone_with_bodypart(target_zone)]!") /datum/surgery_step/premium_augment_maintenance/success(mob/living/user, mob/living/carbon/target, target_zone, obj/item/tool, datum/surgery/surgery, default_display_results = FALSE) - var/obj/item/organ/cyberimp/target_implant + var/obj/item/organ/target_implant var/datum/surgery/premium_augment_maintenance/premium_surgery = surgery if(istype(premium_surgery)) target_implant = premium_surgery.selected_premium diff --git a/tgstation.dme b/tgstation.dme index cee7222def1068..f30ca249220952 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7436,6 +7436,7 @@ #include "modular_doppler\modular_powers\code\powers\mortal\augmented\auto_retriever.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\mental_shielding.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\pneumatic_arm.dm" +#include "modular_doppler\modular_powers\code\powers\mortal\augmented\precognition_eyes.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\reagent_cannon.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\simple_augments.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\surgery\_premium_surgery.dm" From 7518b141b7b416570e651a9ad78fdbe9dfb6b9ac Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 10 Mar 2026 19:38:14 +0100 Subject: [PATCH 132/212] Adjusts the dodge timer on the precog eyes to be way shorter. --- .../code/powers/mortal/augmented/precognition_eyes.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm index f940b73dfbb7a1..c4d469ab1877b9 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm @@ -143,5 +143,5 @@ premium_component?.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) source.visible_message(span_warning("[source] dodges the [proj] with little effort!"), span_danger("You automatically dodge the [proj]!")) - addtimer(TRAIT_CALLBACK_REMOVE(source, TRAIT_UNHITTABLE_BY_PROJECTILES, AUGMENTATION_TRAIT), TAUNT_EMOTE_DURATION * 1.5) + addtimer(TRAIT_CALLBACK_REMOVE(source, TRAIT_UNHITTABLE_BY_PROJECTILES, AUGMENTATION_TRAIT), 0.3 SECONDS) return PROJECTILE_INTERRUPT_HIT From 0757402d3dde073ff13a78d30dd622e847752ec3 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 10 Mar 2026 19:51:18 +0100 Subject: [PATCH 133/212] Undoes a whiteline I accidentaly gave to action. Fixes some text descriptions. --- code/datums/actions/action.dm | 1 + .../code/powers/mortal/augmented/precognition_eyes.dm | 4 ++-- .../code/powers/mortal/augmented/reagent_cannon.dm | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/code/datums/actions/action.dm b/code/datums/actions/action.dm index 88913edcf1e723..65c314d4fc976b 100644 --- a/code/datums/actions/action.dm +++ b/code/datums/actions/action.dm @@ -311,6 +311,7 @@ * force - whether an update is forced regardless of existing status */ /datum/action/proc/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force = FALSE) + SEND_SIGNAL(src, COMSIG_ACTION_OVERLAY_APPLY, current_button, force) if(!overlay_icon || !overlay_icon_state || (current_button.active_overlay_icon_state == overlay_icon_state && !force)) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm index c4d469ab1877b9..821e6a17634817 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm @@ -4,7 +4,7 @@ /datum/power/augmented/precognition_eyes name = "Premium PRCG Precognitive Scanners" desc = "Though some market it as being able to see the future, this invention by Oracle Neuro-Systems is instead a specialized AI recognition model hooked into a BULLET DODGER skillchip, allowing you to automatically dodge any incoming projectiles.\ - \n This doesn't come without drawbacks, as the visual load is exhausting and suffers from the same drawbacks as the skillchip by tiring you out, causing more exhaustion than usual.\ + \n This doesn't come without drawbacks, as the visual load is exhausting and suffers from the same drawbacks as the skillchip by tiring you out, causing more exhaustion than usual. This has no safeguard, meaning you can be stamina-critted by any projectiles.\ \n Requires a BULLET DODGER Skillchip to function; comes pre-packaged with one at roundstart." value = 8 @@ -13,7 +13,7 @@ /obj/item/organ/eyes/robotic/precognition_eyes name = "Premium PRCG Precognitive Scanners" desc = "Though some market it as being able to see the future, this invention by Oracle Neuro-Systems is instead a specialized AI recognition model hooked into a BULLET DODGER skillchip, allowing you to automatically dodge any incoming projectiles.\ - \n This doesn't come without drawbacks, as the visual load is exhausting and suffers from the same drawbacks as the skillchip by tiring you out, causing more exhaustion than usual.\ + \n This doesn't come without drawbacks, as the visual load is exhausting and suffers from the same drawbacks as the skillchip by tiring you out, causing more exhaustion than usual. This has no safeguard, meaning you can be stamina-critted by any projectiles.\ \n Requires a BULLET DODGER Skillchip to function." icon_state = "eyes_cyber_xray" diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm index 356e9eca677f8b..90e6514c32b314 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm @@ -4,7 +4,7 @@ /datum/power/augmented/reagent_cannon name = "Premium SPRY Reagent Cannon" desc = "Usually included in various company contracts, those who work in mega-scale botanics and cleaning need to push for optimal efficiency. Manufcatured by Nex-Zephyr, this beauty will be your lifelong replacement of a spray bottle.\ - \n When activated, transform your arm into a chemsprayer, allowing you to deploy chemicals rapidly in a large area. Capable of containing up to 300 chemicals. \ + \n When activated, transform your arm into a chemsprayer, allowing you to deploy chemicals rapidly in a large area. Capable of containing up to 600 chemicals. \ \n Because this is an incredibly invasive augment, this requires a cybernetic arm to wield effectively. Your arm will be replaced with a synthetic variant at roundstart to facilitate this." value = 5 @@ -40,7 +40,7 @@ /obj/item/organ/cyberimp/arm/toolkit/reagent_cannon name = "Premium SPRY Reagent Cannon" desc = "Usually included in various company contracts, those who work in mega-scale botanics and cleaning need to push for optimal efficiency. Manufcatured by Nex-Zephyr, this beauty will be your lifelong replacement of a spray bottle.\ - \n When activated, transform your arm into a chemsprayer, allowing you to deploy chemicals rapidly in a large area. Capable of containing up to 300 chemicals. \ + \n When activated, transform your arm into a chemsprayer, allowing you to deploy chemicals rapidly in a large area. Capable of containing up to 600 chemicals. \ \n Because this is an incredibly invasive augment, this requires a cybernetic arm to wield effectively." icon = 'icons/obj/weapons/guns/ballistic.dmi' icon_state = "chemsprayer" From 5a808efd9d6ba302293772623f6a941087cd7db3 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 14 Mar 2026 12:53:22 +0100 Subject: [PATCH 134/212] Adds sechud and secrecords for powers (actual power texts are staged in a seperate to come commit) --- code/__DEFINES/~doppler_defines/powers.dm | 9 +++ code/datums/records/manifest.dm | 3 + code/datums/records/record.dm | 14 +++++ .../machinery/computer/records/security.dm | 3 + code/modules/mob/living/carbon/examine.dm | 1 + code/modules/mob/living/carbon/human/human.dm | 37 +++++++++++++ modular_doppler/modular_powers/code/_power.dm | 14 +++++ .../modular_powers/code/powers_living.dm | 55 +++++++++++++++++++ .../modular_powers/code/powers_vv.dm | 8 ++- .../interfaces/SecurityRecords/RecordView.tsx | 24 +++++++- .../tgui/interfaces/SecurityRecords/types.ts | 3 + 11 files changed, 168 insertions(+), 3 deletions(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 23369f07b75d35..2978e8a9e7dd2a 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -34,6 +34,15 @@ /// This power is has a visual aspect in that it changes how the player looks. Used in generating dummies. #define POWER_CHANGES_APPEARANCE (1<<2) +/// Security record categories for powers. +#define CAT_POWER_ALL 0 +#define CAT_POWER_MINOR_THREAT 1 +#define CAT_POWER_MAJOR_THREAT 2 + +/// Threat level tags used by /datum/power.security_threat +#define POWER_THREAT_MINOR "minor" +#define POWER_THREAT_MAJOR "major" + // Trait for when you are unable to use resonant powers #define TRAIT_RESONANCE_SILENCED "RESONANCE_SILENCED" diff --git a/code/datums/records/manifest.dm b/code/datums/records/manifest.dm index 1abcaf82147e11..1f416f40cc2e00 100644 --- a/code/datums/records/manifest.dm +++ b/code/datums/records/manifest.dm @@ -168,6 +168,9 @@ GLOBAL_DATUM_INIT(manifest, /datum/manifest, new) minor_disabilities_desc = person.get_quirk_string(TRUE, CAT_QUIRK_MINOR_DISABILITY), quirk_notes = person.get_quirk_string(TRUE, CAT_QUIRK_NOTES), // DOPPLER EDIT BEGIN - records & flavor text + power_notes = person.get_sec_power_string(CAT_POWER_ALL), + power_notes_minor = person.get_sec_power_string(CAT_POWER_MINOR_THREAT, include_empty_text = FALSE), + power_notes_major = person.get_sec_power_string(CAT_POWER_MAJOR_THREAT, include_empty_text = FALSE), past_general_records = person_client?.prefs.read_preference(/datum/preference/text/past_general_records), past_medical_records = person_client?.prefs.read_preference(/datum/preference/text/past_medical_records), past_security_records = person_client?.prefs.read_preference(/datum/preference/text/past_security_records), diff --git a/code/datums/records/record.dm b/code/datums/records/record.dm index 155403d7dea6df..ee647e2f3d4df8 100644 --- a/code/datums/records/record.dm +++ b/code/datums/records/record.dm @@ -87,6 +87,12 @@ ///Photo used for records, which we store here so we don't have to constantly make more of. var/list/obj/item/photo/record_photos + // DOPPLER ADDITION START - Security facing power notes + var/power_notes + var/power_notes_minor + var/power_notes_major + // DOPPLER ADDITION END + /datum/record/crew/New( age = 18, blood_type = "?", @@ -109,6 +115,9 @@ mental_status = MENTAL_STABLE, quirk_notes, // DOPPLER EDIT START - records & flavor text + power_notes = "No powers declared.", + power_notes_minor = "", + power_notes_major = "", past_general_records = "", past_medical_records = "", past_security_records = "", @@ -126,6 +135,9 @@ src.mental_status = mental_status src.quirk_notes = quirk_notes // DOPPLER EDIT START + src.power_notes = power_notes + src.power_notes_minor = power_notes_minor + src.power_notes_major = power_notes_major src.past_general_records = past_general_records src.past_medical_records = past_medical_records src.past_security_records = past_security_records @@ -274,6 +286,8 @@ if(past_general_records != "") final_paper_text += "
General Records:" final_paper_text += "
[past_general_records]
" + final_paper_text += "
Powers:" + final_paper_text += "
[power_notes || "No powers declared."]
" // DOPPLER EDIT END final_paper_text += "
Security Data


" diff --git a/code/game/machinery/computer/records/security.dm b/code/game/machinery/computer/records/security.dm index 05d3bea5fb3938..f9e2284910096b 100644 --- a/code/game/machinery/computer/records/security.dm +++ b/code/game/machinery/computer/records/security.dm @@ -131,6 +131,9 @@ trim = target.trim, wanted_status = target.wanted_status, // DOPPLER EDIT BEGIN - records & flavor text + power_notes = target.power_notes, + power_notes_minor = target.power_notes_minor, + power_notes_major = target.power_notes_major, past_general_records = target.past_general_records, past_security_records = target.past_security_records, age_chronological = target.age_chronological, diff --git a/code/modules/mob/living/carbon/examine.dm b/code/modules/mob/living/carbon/examine.dm index 94b860b13479af..7b5432d11a1367 100644 --- a/code/modules/mob/living/carbon/examine.dm +++ b/code/modules/mob/living/carbon/examine.dm @@ -605,6 +605,7 @@ . += "Criminal status: [wanted_status]" . += "Important Notes: [security_note]" . += "Security record: \[View\]" + . += "\[Show powers\]" // DOPPLER EDIT - Adds the ability for sechuds to see powers. if(ishuman(user)) . += "\[Add citation\]\ \[Add crime\]\ diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index 7f2d0f5d20309e..9a329fd69b25a1 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -334,6 +334,43 @@ sec_record_message += "\nAdded by [crime.author] at [crime.time]" to_chat(human_or_ghost_user, boxed_message(sec_record_message)) return + // DOPPLER ADDITION START - Lets SecHuds see powers from sec records. + if(href_list["powers"]) + if(ishuman(human_or_ghost_user)) + var/mob/living/carbon/human/human_user = human_or_ghost_user + if(!human_user.canUseHUD()) + return + if(!HAS_TRAIT(human_or_ghost_user, TRAIT_SECURITY_HUD)) + return + var/list/powers_major = splittext(target_record.power_notes_major || "", "
") + var/list/powers_minor = splittext(target_record.power_notes_minor || "", "
") + var/has_major = FALSE + var/has_minor = FALSE + for(var/entry in powers_major) + if(entry != "") + has_major = TRUE + break + for(var/entry in powers_minor) + if(entry != "") + has_minor = TRUE + break + if(has_major || has_minor) + if(has_major) + to_chat(human_or_ghost_user, "Detected high-threat powers:") + for(var/power_entry in powers_major) + if(power_entry == "") + continue + to_chat(human_or_ghost_user, "[power_entry]") + if(has_minor) + to_chat(human_or_ghost_user, "Detected powers:") + for(var/power_entry in powers_minor) + if(power_entry == "") + continue + to_chat(human_or_ghost_user, "[power_entry]") + else + to_chat(human_or_ghost_user, "No registered powers found.") + return + // DOPPLER ADDITION END. if(ishuman(human_or_ghost_user)) var/mob/living/carbon/human/human_user = human_or_ghost_user if(href_list["add_citation"]) diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index f82965060da531..466e83f50aafee 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -43,6 +43,12 @@ var/required_allow_subtypes /// Any one of the required powers satisfies the requirement list. var/required_allow_any + /// The text in security records for this power. + var/security_record_text + /// Security threat classification used for records output. + var/security_threat = POWER_THREAT_MINOR + /// If FALSE, this specific power instance is hidden from security record power listings. + var/include_in_security_records = TRUE // The path, if applicable, to the action. var/datum/action/cooldown/power/action_path @@ -139,6 +145,9 @@ remove() + if(!QDELETED(power_holder)) + power_holder.refresh_security_power_records() + power_holder = null /** @@ -158,6 +167,10 @@ /datum/power/proc/add(client/client_source) return +/// Returns the text this power should contribute to security records. +/datum/power/proc/get_security_record_text() + return security_record_text + /// Any effects from the proc that should not be done multiple times if the power is transferred between mobs. /// Put stuff like spawning items in here. /datum/power/proc/add_unique(client/client_source) @@ -187,6 +200,7 @@ to_chat(power_holder, chat_string) where_items_spawned = null + power_holder?.refresh_security_power_records() // ensures that post_add features are included in the records. return // Adds activateable power buttons. diff --git a/modular_doppler/modular_powers/code/powers_living.dm b/modular_doppler/modular_powers/code/powers_living.dm index 6b744f8f221283..862d0196888f1e 100644 --- a/modular_doppler/modular_powers/code/powers_living.dm +++ b/modular_doppler/modular_powers/code/powers_living.dm @@ -55,6 +55,61 @@ return power return null +/** + * get_power_string() is used to get a printable string of all powers this mob has. + * + * Arguments: + * * security - If TRUE, uses each power's security record text. If FALSE, uses the power names. + * * category - Which threat categories of powers should be included. + * * include_empty_text - If FALSE, returns an empty string when no entries match. + */ +/mob/living/proc/get_power_string(security = FALSE, category = CAT_POWER_ALL, include_empty_text = TRUE) + var/list/dat = list() + for(var/datum/power/candidate as anything in powers) + if(security && !candidate.include_in_security_records) + continue + + switch(category) + if(CAT_POWER_MINOR_THREAT) + if(candidate.security_threat != POWER_THREAT_MINOR) + continue + if(CAT_POWER_MAJOR_THREAT) + if(candidate.security_threat != POWER_THREAT_MAJOR) + continue + + if(security) + var/security_text = candidate.get_security_record_text() + if(!isnull(security_text) && security_text != "") + dat += security_text + else + dat += candidate.name + + if(!length(dat)) + if(!include_empty_text) + return "" + return security ? "No powers declared." : "None" + + return security ? dat.Join("
") : dat.Join(", ") + +/// Compatibility helper for security record formatting. +/mob/living/proc/get_sec_power_string(category = CAT_POWER_ALL, include_empty_text = TRUE) + return get_power_string(TRUE, category, include_empty_text) + +// Refreshes the sec records when powers are added/removed. +/mob/living/proc/refresh_security_power_records() + var/lookup_name = name + if(ishuman(src)) + var/mob/living/carbon/human/human_self = src + lookup_name = human_self.real_name + + var/datum/record/crew/target = find_record(lookup_name) + if(!target) + return + + target.power_notes = get_sec_power_string(CAT_POWER_ALL) + target.power_notes_minor = get_sec_power_string(CAT_POWER_MINOR_THREAT, include_empty_text = FALSE) + target.power_notes_major = get_sec_power_string(CAT_POWER_MAJOR_THREAT, include_empty_text = FALSE) + /mob/living/proc/cleanse_power_datums() QDEL_LIST(powers) diff --git a/modular_doppler/modular_powers/code/powers_vv.dm b/modular_doppler/modular_powers/code/powers_vv.dm index 3e7a2d2053485b..a9125b20083763 100644 --- a/modular_doppler/modular_powers/code/powers_vv.dm +++ b/modular_doppler/modular_powers/code/powers_vv.dm @@ -29,7 +29,8 @@ if(has_power(chosen)) remove_power(chosen) else - add_power(chosen) + var/include_in_security_records = (alert(usr, "Also include this power in security records?", "Power Mod", "No", "Yes") == "Yes") + add_power(chosen, include_in_security_records = include_in_security_records) // Checks if a power is on the selected target /mob/living/carbon/proc/has_power(powertype) @@ -39,16 +40,18 @@ return FALSE // Adds a power by calling the power subsystem. -/mob/living/carbon/proc/add_power(datum/power/powertype, power_transfer = FALSE, client/override_client, unique = TRUE) +/mob/living/carbon/proc/add_power(datum/power/powertype, power_transfer = FALSE, client/override_client, unique = TRUE, include_in_security_records = TRUE) if(has_power(powertype)) return FALSE var/pname = initial(powertype.name) if(!SSpowers || !SSpowers.powers[pname]) return FALSE var/datum/power/power = new powertype() + power.include_in_security_records = include_in_security_records if(!power.add_to_holder(new_holder = src, power_transfer = power_transfer, client_source = override_client, unique = unique)) qdel(power) return FALSE + refresh_security_power_records() return TRUE // Removes a power. @@ -57,5 +60,6 @@ if(power.type != powertype) continue qdel(power) + refresh_security_power_records() return TRUE return FALSE diff --git a/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx b/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx index 6508d1182b5977..1fe2209ff69690 100644 --- a/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx +++ b/tgui/packages/tgui/interfaces/SecurityRecords/RecordView.tsx @@ -13,8 +13,8 @@ import { import { CharacterPreview } from '../common/CharacterPreview'; import { EditableText } from '../common/EditableText'; -import { CRIMESTATUS2COLOR, CRIMESTATUS2DESC } from './constants'; import { CrimeWatcher } from './CrimeWatcher'; +import { CRIMESTATUS2COLOR, CRIMESTATUS2DESC } from './constants'; import { getSecurityRecord } from './helpers'; import { RecordPrint } from './RecordPrint'; import type { SecurityRecordsData } from './types'; @@ -69,6 +69,9 @@ const RecordInfo = (props) => { wanted_status, voice, // DOPPLER EDIT START - records & flavor text + power_notes, + power_notes_minor, + power_notes_major, past_general_records, past_security_records, age_chronological, @@ -77,6 +80,12 @@ const RecordInfo = (props) => { const [isValid, setIsValid] = useState(true); + // DOPPLER ADDITION START - Power sec notes + const major_power_notes_array = power_notes_major?.split('
') || []; + const minor_power_notes_array = power_notes_minor?.split('
') || []; + const has_power_notes = + major_power_notes_array.length > 0 || minor_power_notes_array.length > 0; + // DOPPLER ADDITION END const hasValidCrimes = !!crimes.find((crime) => !!crime.valid); return ( @@ -222,6 +231,19 @@ const RecordInfo = (props) => { /> {/* DOPPLER EDIT START - records & flavor text */} + + {major_power_notes_array.map((power, index) => ( + + • {power} + + ))} + {minor_power_notes_array.map((power, index) => ( + • {power} + ))} + {!has_power_notes && ( + • {power_notes || 'No powers declared.'} + )} + {past_general_records || 'N/A'} diff --git a/tgui/packages/tgui/interfaces/SecurityRecords/types.ts b/tgui/packages/tgui/interfaces/SecurityRecords/types.ts index ff5df1fb560bb9..e96b5c2a850dcc 100644 --- a/tgui/packages/tgui/interfaces/SecurityRecords/types.ts +++ b/tgui/packages/tgui/interfaces/SecurityRecords/types.ts @@ -29,6 +29,9 @@ export type SecurityRecord = { wanted_status: string; voice: string; // DOPPLER EDIT START - records & flavor text + power_notes: string; + power_notes_minor: string; + power_notes_major: string; past_general_records: string; past_security_records: string; // DOPPLER EDIT END From 72bccc3f8a0ab6675e7761609dfbb3cb93e54e45 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 14 Mar 2026 13:33:58 +0100 Subject: [PATCH 135/212] Adds securiy record text to all Mortal & Resonant Powers (Sorcerous still to go) --- .../mortal/augmented/_augmented_power.dm | 11 +++++++ .../powers/mortal/augmented/auto_retriever.dm | 2 ++ .../mortal/augmented/mental_shielding.dm | 3 +- .../powers/mortal/augmented/pneumatic_arm.dm | 2 ++ .../mortal/augmented/precognition_eyes.dm | 4 ++- .../powers/mortal/augmented/reagent_cannon.dm | 2 ++ .../powers/mortal/expert/creature_tamer.dm | 2 +- .../mortal/expert/eye_for_ingredients.dm | 1 + .../code/powers/mortal/expert/filthy_rich.dm | 1 + .../code/powers/mortal/expert/heavy_lifter.dm | 1 + .../powers/mortal/expert/master_surgeon.dm | 1 + .../powers/mortal/expert/obfuscate_voice.dm | 1 + .../code/powers/mortal/expert/omnilingual.dm | 6 +++- .../code/powers/mortal/expert/punt.dm | 1 + .../code/powers/mortal/expert/rich.dm | 2 +- .../code/powers/mortal/expert/strider.dm | 1 + .../code/powers/mortal/expert/zoologist.dm | 1 + .../powers/mortal/warfighter/command_grit.dm | 3 +- .../mortal/warfighter/command_recover.dm | 2 +- .../powers/mortal/warfighter/dual_wielder.dm | 2 ++ .../warfighter/explosives_specialist.dm | 2 ++ .../powers/mortal/warfighter/focused_block.dm | 2 ++ .../mortal/warfighter/greater_tackler.dm | 2 ++ .../powers/mortal/warfighter/heavy_slam.dm | 2 ++ .../powers/mortal/warfighter/krav_maga.dm | 2 ++ .../mortal/warfighter/martial_artist.dm | 2 ++ .../powers/mortal/warfighter/quick_draw.dm | 2 ++ .../code/powers/mortal/warfighter/tackler.dm | 2 ++ .../aberrant/_aberrant_root_anomalous.dm | 1 + .../aberrant/_aberrant_root_beastial.dm | 9 ++++++ .../aberrant/_aberrant_root_monstrous.dm | 1 + .../code/powers/resonant/aberrant/armblade.dm | 2 ++ .../resonant/aberrant/bioluminescence.dm | 1 + .../powers/resonant/aberrant/bloodhound.dm | 1 + .../code/powers/resonant/aberrant/cocoon.dm | 2 ++ .../powers/resonant/aberrant/darkvision.dm | 1 + .../resonant/aberrant/healing_factor.dm | 1 + .../resonant/aberrant/miasmic_conversion.dm | 5 +-- .../resonant/aberrant/radiosynthesis.dm | 1 + .../resonant/aberrant/resonant_immune.dm | 2 ++ .../powers/resonant/aberrant/shapechange.dm | 32 +++++++++++++++++++ .../powers/resonant/aberrant/summonable.dm | 11 +++++++ .../powers/resonant/aberrant/vent_crawl.dm | 2 ++ .../aberrant/web_crafter/binding_webs.dm | 2 ++ .../aberrant/web_crafter/snare_webs.dm | 2 ++ .../aberrant/web_crafter/tripwire_webs.dm | 1 + .../aberrant/web_crafter/web_crafter.dm | 1 + .../resonant/cultivator/astraltouched_root.dm | 2 ++ .../resonant/cultivator/flamesoul_root.dm | 2 ++ .../cultivator/fly_like_a_shooting_star.dm | 2 +- .../cultivator/from_friction_comes_flame.dm | 2 ++ .../powers/resonant/cultivator/many_stars.dm | 3 +- .../cultivator/set_fire_to_dry_hay.dm | 3 +- .../resonant/cultivator/shadowwalker_root.dm | 3 +- .../travel_under_the_veil_of_night.dm | 2 ++ .../cultivator/vanish_unseen_into_shadow.dm | 2 ++ .../powers/resonant/psyker/_psyker_root.dm | 2 +- .../code/powers/resonant/psyker/levitate.dm | 3 +- .../code/powers/resonant/psyker/manipulate.dm | 3 +- .../code/powers/resonant/psyker/scrying.dm | 3 +- .../powers/resonant/psyker/telekinesis.dm | 4 +-- 61 files changed, 158 insertions(+), 21 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm index 23364032b17fb5..25b9a3a503e0e5 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm @@ -14,6 +14,17 @@ // Should the augment be disabled if they're a prisoner. var/disable_if_prisoner = TRUE +// default text for augments +/datum/power/augmented/get_security_record_text() + if(security_record_text) + return security_record_text + if(!augment) + return "" + + var/obj/item/organ/augment_path = augment + var/augment_name = initial(augment_path.name) + return "Subject has a [augment_name]." + // Responsible for adding augments /datum/power/augmented/add_unique(client/client_source) var/mob/living/carbon/carbon_holder = power_holder diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm index f5b43c3cf9ba74..104924edea67ee 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm @@ -7,6 +7,8 @@ \n Once you reach critical condition or when manually activated, you begin a slow (and obvious) 10 second teleport towards your station's medbay lobby (regardless of Z-level).\ Once it fires, a warning message is issued over the radio. The teleportation sets the quality to 0%, and can be interupted by Epinephrine in the bloodstream, EMP, or healing you above the critical threshold; after which it loses 25% quality and enters a several minute cooldown period.\ \n Decreases in quality twice as fast. Lower quality decreases the speed of the teleport." + security_record_text = "Subject has a ANGL Auto Retriever and will teleport to medbay if critically injured." + security_threat = POWER_THREAT_MAJOR value = 6 augment = /obj/item/organ/cyberimp/chest/auto_retriever 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 5dd13c8339e044..d77959a6f9d1b5 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 @@ -6,7 +6,8 @@ desc = " Based on the nullifying effects that tinfoil has on certain magical phenomena, this dermal implant created by Oracle Neuro-Systems creates a protective coating around your brain.\ \n Creates a barrier that blocks resonant based scrying, as well as mental abilities used on you (including magic stronger than Resonant).\ \n Blocking mental abilities consumes quality, increasing consumption rate the lower the quality is." - + security_record_text = "Subject has a TNFL Mental Shielding Implant and is immune to scrying and mental-based resonance." + security_threat = POWER_THREAT_MAJOR value = 6 augment = /obj/item/organ/cyberimp/brain/mental_shielding diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm index 199f588ab2d946..e0bcad91266b40 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm @@ -6,6 +6,8 @@ desc = "A popular choice for the augmented bodyguards and manufactured by Praetor Dynamics. Passively increases your punch damage by +5 with that arm. \ \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 2 spaces (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)." + security_record_text = "Subject has a DSTR Pneumatic Arm, increasing their lethality with unarmed strikes." + security_threat = POWER_THREAT_MAJOR value = 4 // balance around 2 arms. augment = /obj/item/organ/cyberimp/arm/pneumatic_arm diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm index 821e6a17634817..2c8df789c31328 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm @@ -6,6 +6,8 @@ desc = "Though some market it as being able to see the future, this invention by Oracle Neuro-Systems is instead a specialized AI recognition model hooked into a BULLET DODGER skillchip, allowing you to automatically dodge any incoming projectiles.\ \n This doesn't come without drawbacks, as the visual load is exhausting and suffers from the same drawbacks as the skillchip by tiring you out, causing more exhaustion than usual. This has no safeguard, meaning you can be stamina-critted by any projectiles.\ \n Requires a BULLET DODGER Skillchip to function; comes pre-packaged with one at roundstart." + security_record_text = "Subject has PRCG Precognitive Scanners, allowing them to automatically dodge projectiles at the cost of their stamina." + security_threat = POWER_THREAT_MAJOR // it is still a chemsprayer if you put murder chems in this it will kill value = 8 augment = /obj/item/organ/eyes/robotic/precognition_eyes @@ -143,5 +145,5 @@ premium_component?.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) source.visible_message(span_warning("[source] dodges the [proj] with little effort!"), span_danger("You automatically dodge the [proj]!")) - addtimer(TRAIT_CALLBACK_REMOVE(source, TRAIT_UNHITTABLE_BY_PROJECTILES, AUGMENTATION_TRAIT), 0.3 SECONDS) + addtimer(TRAIT_CALLBACK_REMOVE(source, TRAIT_UNHITTABLE_BY_PROJECTILES, AUGMENTATION_TRAIT), 0.2 SECONDS) return PROJECTILE_INTERRUPT_HIT diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm index 90e6514c32b314..f8c56a8075c857 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm @@ -6,6 +6,8 @@ desc = "Usually included in various company contracts, those who work in mega-scale botanics and cleaning need to push for optimal efficiency. Manufcatured by Nex-Zephyr, this beauty will be your lifelong replacement of a spray bottle.\ \n When activated, transform your arm into a chemsprayer, allowing you to deploy chemicals rapidly in a large area. Capable of containing up to 600 chemicals. \ \n Because this is an incredibly invasive augment, this requires a cybernetic arm to wield effectively. Your arm will be replaced with a synthetic variant at roundstart to facilitate this." + security_record_text = "Subject has an industrial SRPY Reagent cannon embedded in their arm." + security_threat = POWER_THREAT_MAJOR // it is still a chemsprayer if you put murder chems in this it will kill value = 5 augment = /obj/item/organ/cyberimp/arm/toolkit/reagent_cannon diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm index ca8273504f96ed..bdbbbb05b6a7a3 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm @@ -1,7 +1,7 @@ /datum/power/expert/creature_tamer name = "Creature Tamer" desc = "You're always met with success when taming creatures. Grants you the 'Tame Creature' ability, allowing you to automatically tame any normally tameable creatures. Now you too can have your very own space carp pet." - + security_record_text = "Subject has an affinity for taming creatures." value = 2 required_powers = list(/datum/power/expert/zoologist) action_path = /datum/action/cooldown/power/expert/creature_tamer 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 efc778e03ba318..e86e14697223fd 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 @@ -5,6 +5,7 @@ /datum/power/expert/eye_for_ingredients name = "Eye for Ingredients" desc = "You've interacted with food, drinks and/or chemicals so often, you can see at a glance if something's off with it. You can see the precise reagent contents of all containers by simply examining it." + security_record_text = "Subject has a keen eye for spotting substances inside food, drinks and chemicals." value = 3 /datum/power/expert/eye_for_ingredients/add() 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 18ea06eaad139b..8f9bf1d8726b25 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 @@ -6,6 +6,7 @@ /datum/power/expert/filthy_rich name = "Filthy Rich" desc = "With this much disposable money it's even a question as to why you even work anymore. You start with 10000 extra credits (includes the amount from being Rich already). And probably tons more in off-shore savings accounts." + security_record_text = "Subject has an exorbant amount of wealth and resources at their disposal." value = 8 required_powers = list(/datum/power/expert/rich) 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 bc444723913d64..d8e93951401559 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 @@ -6,6 +6,7 @@ name = "Heavy Lifter" desc = "A strong back does a lot when it comes to carrying closets. You ignore the slowdown from dragging objects and having creatures grabbed/ and/or carried. You also start off as a Journeyman in the Athletics skill. \ All other slowdowns such as stamina, items, damage, etc. still apply as normal." + security_record_text = "Subject possesses a high degree of strength and is capable of hauling objects without being slowed down." value = 5 // how much xp we start with on average. var/starting_xp_base = SKILL_EXP_JOURNEYMAN 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 4c84643b14db40..76fde591ca6d84 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 @@ -6,6 +6,7 @@ /datum/power/expert/master_surgeon name = "Master Surgeon" desc = " Surgery takes composure and skill which you have aplenty. Increases your success rate and action speed with surgery by a factor of 1.5x." + security_record_text = "Subject has an unusual skill in surgery." value = 4 /// 1.5x faster => multiply time by 1/1.5 var/surgery_speed_mult = 1 / 1.5 diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm index e2d3982e0f80a6..25649bcc826623 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/obfuscate_voice.dm @@ -5,6 +5,7 @@ Hides your voice as unknown while active. Act out the machivalean you always wan /datum/power/expert/obfuscate_voice name = "Obfuscate Voice" desc = "Like an actor, the sheer range in your voice is enough, with a little effort, to sound like someone entirely unfamiliar. Grants the 'Obfuscate Voice' action, making your voice unrecognizeable while active." + security_record_text = "Subject can change their voice to be distinctly different from their normal voice." value = 5 action_path = /datum/action/cooldown/power/expert/obfuscate_voice 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 6849253d1a47c2..239f9048015ea6 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/omnilingual.dm @@ -2,11 +2,15 @@ /datum/power/expert/omnilingual name = "Omnilingual" desc = "You speak an absurd amount of languages; you are able to understand and speak every language at full proficiency. Does not apply to languages not available to your character at character selection." - value = 4 // Saved list of languages that were given by this power to remove when the power is removed. var/list/given_languages_list = list() +/datum/power/expert/omnilingual/get_security_record_text() + var/datum/language_holder/holder = power_holder?.get_language_holder() + var/total_languages = LAZYLEN(holder?.spoken_languages) + return "Subject has fluency in [total_languages] languages." + // Iterate through the language prefs list. If they have it, skip, otherwise, give it to them and add it to given_languages_list. /datum/power/expert/omnilingual/add() if(!power_holder) diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm index 6cf033ba528c44..f459beaf728293 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm @@ -8,6 +8,7 @@ name = "Punt" desc = "Using your foot or some other part of your body, you send an object barreling down a long distance away from you. If someone is hit by the object and it is solid, they are knocked down and take damage. \ Distance (and damage) scale with your Athletics skill. Double distance on crates and non-bulky objects! Requires Heavy Lifter." + security_record_text = "Subject has expertise in punting objects across large distances." value = 3 required_powers = list(/datum/power/expert/heavy_lifter) action_path = /datum/action/cooldown/power/expert/punt 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 a84e5900c006ed..e1df4e26bfa752 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/rich.dm @@ -6,7 +6,7 @@ name = "Rich" desc = "Whether through good savings, connections or just nepotism; you have way more spendable cash on hand than your peers. You start the shift with 2500 extra credits in your account." value = 5 - + security_record_text = "Subject has access to a high amount of wealth and resources." // how rich are we? var/riches = 2500 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 bd96c124de4993..9ecd4e89c3f478 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm @@ -6,6 +6,7 @@ name = "Strider" desc = "Your strength is herculean. You ignore all slowdowns from held & worn items. \ You also start out at Master proficiency athletics." + security_record_text = "Subject has an incredibly strong physique and carry heavy equipment without issue." value = 6 required_powers = list(/datum/power/expert/heavy_lifter) diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm index 6986dae6fd9cfb..189ddaf0f7afc6 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/zoologist.dm @@ -5,6 +5,7 @@ name = "Zoologist" desc = "You are capable of befriending just about any creature, given the opportunity. You gain the 'Befriend Creature' ability; using it on a mob in melee range will befriend it and any of it's other nearby cousins. \ This doesn't prevent them from turning hostile on other creatures. You can befriend just about any creature that can also be revived with a Lazurs Injector. There's no limit to how many creatures you can befriend." + security_record_text = "Subject has an unusual ability to befriend any and all animals." value = 4 action_path = /datum/action/cooldown/power/expert/zoologist diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm index dc1526b0ff5273..db7599136b0b6e 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm @@ -4,7 +4,8 @@ /datum/power/warfighter/command_grit name = "Command: Grit" desc = "Whilst active, the target ignores pain for 15 seconds, as well as slowdown from damage and stamina loss. Has a long cooldown. Increased effect lenghtens duration." - + security_record_text = "Subject has an unusual charisma and can motivate others to grit through any pain or injury without slowing down." + security_threat = POWER_THREAT_MAJOR // you dont want this guy supporting your takedown target value = 5 required_powers action_path = /datum/action/cooldown/power/warfighter/command/grit diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm index 27f3f963210a32..d4a250b5faf088 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm @@ -8,7 +8,7 @@ You gain the 'Command: Recover' ability. Using it on someone will cause them to recover from stuns faster (as if shook on help intent). Has a moderate cooldown. \ For any and all command abilities in this category, the effect is increased if you are in the same department as the target, and even further if you are a head of staff (regardless of department). \ Command abilities can never be used on yourself, and require the target to be able to see or hear you." - + security_record_text = "Subject has an unusual charisma and can motivate others to recover from incapacitating effects faster." value = 4 action_path = /datum/action/cooldown/power/warfighter/command/recover diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm index 9479b38af6e758..4d883ca734bae5 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm @@ -12,6 +12,8 @@ /datum/power/warfighter/dual_wielder name = "Dual Wielder" desc = "You can toggle a dual-wield stance. While active, striking with a melee weapon immediately follows with an off-hand strike. Both strikes have a 30% chance to miss." + security_record_text = "Subject knows how to efficiently fight with two melee weapons at once." + security_threat = POWER_THREAT_MAJOR value = 5 required_powers = list(/datum/power/warfighter/quick_draw) 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 7795bf73fa6008..c81417796e4cbc 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 @@ -1,6 +1,8 @@ /datum/power/warfighter/explosives_specialist name = "Explosives Specialist" desc = "Bombs and grenades are your forte. You can see the countdown on grenades (and bombs, but practically all bombs already come with a display for DRAMATIC FLAIR)." + security_record_text = "Subject is specialized in explosives, and can estimate the detonation time on grenades and explosives." + security_threat = POWER_THREAT_MAJOR value = 4 required_powers = list(/datum/power/warfighter/quick_draw) mob_trait = TRAIT_POWER_EXPLOSIVES_SPECIALIST diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm index eafa1d44713623..882fd753d19c51 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/focused_block.dm @@ -2,6 +2,8 @@ name = "Focused Block" desc = "Using what you have on you, you raise your block chance by 50 for 1.5 seconds, as long as you are holding a bulky-sized item or an item with a block chance. \ This stacks on-top of any existing block you may have, guaranteeing blocks with most shields. Has a short cooldown." + security_record_text = "Subject can block attacks with extreme efficiency while wielding a shield or large object." + security_threat = POWER_THREAT_MAJOR value = 6 action_path = /datum/action/cooldown/power/warfighter/focused_block 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 9533885b8b9633..a069e0e06da2b0 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 @@ -4,6 +4,8 @@ /datum/power/warfighter/tackler/greater_tackler name = "Greater Tackler" desc = "Your chances of landing a succesful tackle are greatly increased, as are your range and the duration you knockdown tackled foes." + security_record_text = "Subject is exceedingly good at landing tackles." + security_threat = POWER_THREAT_MAJOR value = 5 required_powers = list(/datum/power/warfighter/tackler) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm index 8400e08990e3bd..6b09126c50bb27 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/heavy_slam.dm @@ -6,6 +6,8 @@ name = "Heavy Slam" desc = "You perform a massive, arcing strike that hits a large area. You strike the 2x3 area adjacent to you in the target direction, hitting everyone in the area (and everything, if in combat mode). \ A creature can only be hit once by this power, but large creatures take double damage. Requires you to actively be wielding a two-handed weapon." + security_record_text = "Subject can swing two-handed weapons in an enormous area." + security_threat = POWER_THREAT_MAJOR value = 4 action_path = /datum/action/cooldown/power/warfighter/heavy_slam diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm index 36e9f900fc48f7..9fc4c5d1e67268 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm @@ -5,6 +5,8 @@ name = "Krav Maga" desc = "Trained in various disarming moves, you can wield the martial arts of Krav Maga without any external assistance.\ (Powers that give you access to Martial Arts override your unarmed attacks and thusly do not stack with any modifier that affect your punches)" + security_record_text = "Subject has studied Martial Arts and understands Krav Maga." + security_threat = POWER_THREAT_MAJOR value = 8 required_powers = list(/datum/power/warfighter/martial_artist) /// Mindbound martial art component so the style follows mind transfers 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 29b5ee0a83980e..4fbd487f4523a8 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 @@ -4,6 +4,8 @@ /datum/power/warfighter/martial_artist name = "Martial Artist" desc = "Trained in specialized combat maneuvers, you know where to best strike your opponents. Your punches deal extra damage." + security_record_text = "Subject is trained in hand-to-hand combat and throws stronger punches." + security_threat = POWER_THREAT_MAJOR value = 2 power_flags = POWER_HUMAN_ONLY diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm index 9b1a26fb3a5268..ea1d929c231797 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm @@ -7,6 +7,8 @@ The power itself grants you the 'Quick Draw' ability, letting you 'acclimate' with an item of your choice. \ Whilst acclimated, you can use the power to instantly draw that type of item to your hand, as long as it is anywhere on your person, or within melee range of you. \ You can even use this to snag it back from your enemies." + security_record_text = "Subject has a high amount of manual dexterity and is hard to disarm." + security_threat = POWER_THREAT_MAJOR value = 3 action_path = /datum/action/cooldown/power/warfighter/quick_draw 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 3de3c0683c4b4a..4e836bf1c57bac 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/tackler.dm @@ -5,6 +5,8 @@ /datum/power/warfighter/tackler name = "Tackler" desc = "You know how to throw a well-trained tackle. Allows you to perform tackles without assistive items and allows you to perform them better." + security_record_text = "Subject is trained in using tackles for takedowns." + security_threat = POWER_THREAT_MAJOR value = 4 required_powers = list(/datum/power/warfighter/martial_artist) diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm index 4c2e7145808bd2..3648a2a561eb97 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/_aberrant_root_anomalous.dm @@ -5,6 +5,7 @@ /datum/power/aberrant_root/anomalous name = "Anomalous Origin" desc = "Things just don't add up with you. You can interact with anomalies to close them, as if you were using an anomaly neutralizer." + security_record_text = "Subject has unusual properties when interacting with anomalies." value = 1 /datum/power/aberrant_root/anomalous/add(client/client_source) 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 f35f630b78cb78..fac207a47e42c0 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 @@ -4,6 +4,14 @@ \nHerbivore: Vegetables, Fruit & Nuts. \ \nCarnivore: Raw, Gore, Meat, Bugs & Seafood." value = 2 + /// Saved preference value used for security records snapshotting. + var/chosen_diet = "None" + +/datum/power/aberrant_root/beastial/get_security_record_text() + switch(chosen_diet) + if("Herbivore", "Carnivore") + return "Subject has a [LOWER_TEXT(chosen_diet)] dietary adaptation." + return "" /datum/power/aberrant_root/beastial/add(client/client_source) var/obj/item/organ/tongue/tongue = power_holder.get_organ_slot(ORGAN_SLOT_TONGUE) @@ -12,6 +20,7 @@ var/diet_choice = client_source?.prefs?.read_preference(/datum/preference/choiced/beastial_diet) if(isnull(diet_choice)) diet_choice = "None" + chosen_diet = diet_choice switch(diet_choice) if("Herbivore") 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 80c7de51ecc38b..e23b18bc7f8834 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 @@ -3,6 +3,7 @@ name = "Monstrous Body" desc = "If it bleeds, you can kill it. Just with you, blood doesn't really matter. You have 125% the normal blood capacity of your species, and regenerate blood that much faster as well.\ \n The thresholds for being low on blood are unchanged, meaning you are extra resistent to bloodloss." + security_record_text = "Subject's body contains and regenerates more blood." value = 3 // Target blood level while this power is active. 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 75e937b446994a..8797832fbc0b08 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/armblade.dm @@ -3,6 +3,8 @@ name = "Armblade" desc = "Allows you to transform your arm into a deadly blade. The weapon itself has high damage, pierces armor and can destroy tables that block your way.\ \n Requires an empty hand to use." + security_record_text = "Subject can manifest a sharp-edged blade from their arm." + security_threat = POWER_THREAT_MAJOR value = 4 required_powers = list(/datum/power/aberrant_root/monstrous) action_path = /datum/action/cooldown/power/aberrant/armblade 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 7ef3ef3387240a..527cd2f76878b8 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bioluminescence.dm @@ -3,6 +3,7 @@ name = "Bioluminescence" desc = "You can glow! You passively emit the chosen light color; which can be toggled on or off at will. Very slightly increases passive hunger when enabling or disabling the light." value = 1 + security_record_text = "Subject has been observed to glow through bioluminescence." required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous) required_allow_any = TRUE 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 980e38df61b61a..746e4bf96cac02 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm @@ -3,6 +3,7 @@ name = "Bloodhound" desc = "A whiff of someone's blood, and you're right on their tail. Select a source of blood and it will be your currently active scent. You can only have one active source of scent, and it only lasts for a few minutes.\ \n Whilst you have someone's blood, you have an indicator of your quarry's direction. Does not work on scrying immune creatures." + security_record_text = "Subject can track down a creature's direction using blood samples." value = 10 required_powers = list(/datum/power/aberrant_root/beastial) 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 7380fa5468dcfa..607764bea46503 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm @@ -6,6 +6,8 @@ \n Targeting a prone creature that you have aggressively grabbed bundles them up. The creature is buckled inside the cocoon and can't interact with the world or escape without struggling. \ Creature cocoons can be dragged around with less slow down commpared to normal.\ \n Costs hunger to use, and cannot be used while starving." + security_record_text = "Subject can produce enough silk to fully cocoon creatures and objects in webs." + security_threat = POWER_THREAT_MAJOR value = 3 required_powers = list(/datum/power/aberrant/web_crafter) 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 eb1e24a15428c1..61be0b0b6bcf51 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/darkvision.dm @@ -2,6 +2,7 @@ /datum/power/aberrant/darkvision name = "Darkvision" desc = "Your eyes see perfectly in the dark; but your vision gains a blue-ish hue to it." + security_record_text = "Subject sees perfectly in the dark." mob_trait = TRAIT_TRUE_NIGHT_VISION value = 3 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 455a247208b4cc..4f9d29c0a90f29 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,6 +4,7 @@ /datum/power/aberrant/healing_factor name = "Healing Factor" desc = "Your physical injuries heal without assistance. You heal 0.2 damage per second, randomly split between brute and burn damage while not in critical condition. Wounds such as bleeding still require medical treatment." + security_record_text = "Subject passively regenerates any injuries they sustain." value = 4 power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES 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 d3de921a37e4ed..5c6a776ea601c8 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,7 +3,8 @@ */ /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 70% ratio. You also passively heal 0.1 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. You also passively heal 0.1 toxins damage per second." + security_record_text = "Subject extremely rapidly regenerates, but experiences toxic backlash when they do." value = 4 power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES @@ -14,7 +15,7 @@ // how much we heal per second var/healing = 1 // the ratio at which we convert. - var/conversion_rate = 0.70 + var/conversion_rate = 0.90 /datum/power/aberrant/miasmic_conversion/process(seconds_per_tick) // Does not work if you're in crit diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm index 68d64ae4ffe176..425cbc67410b6a 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm @@ -4,6 +4,7 @@ /datum/power/aberrant/radiosyntehsis name = "Radiosynthesis" desc = "Rather than the molecular degredation you experience from radioactivity, your body instead uses it as an energy source to rapidly heal your body. Radioactivity heals you instead of damaging you. Because this healing is anomalous, it heals synthetic and biological bodyparts." + security_record_text = "Subject's body regenerates instead of degenerate from exposure to radiation." value = 3 mob_trait = TRAIT_HALT_RADIATION_EFFECTS // we don't give radimmune cause we want to ENCOURAGE people to get irradiated. power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm index 973df7614eeb74..707a3f8f2662da 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/resonant_immune.dm @@ -5,6 +5,8 @@ name = "Counter-Resonance Anomaly" desc = "You have a counteractive effect on resonance-based phenomena. You are immune to resonance-based effects (but not the highly advanced magics wielded by some antagonistic forces), and you cannot use any resonance-based powers.\ \n (Silencing only affects active powers; passive powers, such as Radiosyntehsis, are unaffected.)" + security_record_text = "Subject is immune to resonance-based phenomena and is unable to wield them." + security_threat = POWER_THREAT_MAJOR value = 9 required_powers = list(/datum/power/aberrant_root/anomalous) 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 cbb35a6e0c3b70..9a07afa83ec49c 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm @@ -8,12 +8,44 @@ \n Activating the ability transforms you into the chosen animal. It does not have your name or any other identifying traits, but the number is always the same when you use it (and the security record for this power elaborates on what creature and numbers). \ \n Using the ability makes you hungry, and cannot be used while you're starving.\ \n If the creature dies or the effect ends, you are reverted to your normal form (prone on the ground), and all damage taken is transfered to your original form (halved if reverting back manually)." + security_threat = POWER_THREAT_MAJOR value = 5 required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous) required_allow_any = TRUE action_path = /datum/action/cooldown/power/aberrant/shapechange +/datum/power/aberrant/shapechange/get_security_record_text() + var/datum/action/cooldown/power/aberrant/shapechange/shape_action = action_path + if(!istype(shape_action)) + return "" + + // Resolve from the action itself so overrides (wolf/spider) are reflected in records. + if(!ispath(shape_action.animal_form)) + shape_action.animal_form = shape_action.get_shapechange_type(power_holder?.client) + + var/animal_name = "animal" // if we don't get anything good + if(ispath(shape_action.animal_form, /mob/living)) + var/mob/living/animal_type = shape_action.animal_form + animal_name = initial(animal_type.name) + + if(!shape_action.shape_identifier) + return "" + return "Subject can shapechange into a [animal_name] with persistent identifier #[shape_action.shape_identifier]." + +// Sets a persistent number identifier for the mob used in both sec records and the mob. +/datum/power/aberrant/shapechange/post_add() + . = ..() + var/datum/action/cooldown/power/aberrant/shapechange/shape_action = action_path + if(!istype(shape_action)) + return + if(!shape_action.shape_identifier) + shape_action.shape_identifier = rand(1, 999) + if(!ispath(shape_action.animal_form)) + shape_action.animal_form = shape_action.get_shapechange_type(power_holder?.client) + + power_holder?.refresh_security_power_records() + /datum/action/cooldown/power/aberrant/shapechange name = "Shapechange" desc = "Change into your chosen animal form!" diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm index 8fd5559bc50031..aee47407131a03 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm @@ -6,6 +6,7 @@ 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 buckled or dispelled.\ \n After being succesfuly summoned, you are unable to be summoned again for 1 minute. \ \n The chosen word is a partial secret; the Security Records on your powers contain the word as well. It cannot contain any special characters, only standard letters and numbers." + security_threat = POWER_THREAT_MAJOR value = 7 required_powers = list(/datum/power/aberrant_root/anomalous) @@ -13,6 +14,16 @@ var/datum/component/beetlejuice/summonable/summon_component var/owns_summon_component = FALSE +// Lists the word in sec records. +/datum/power/aberrant/summonable/get_security_record_text() + var/keyword = summon_component?.keyword + if(!keyword) + keyword = power_holder?.client?.prefs?.read_preference(/datum/preference/text/summonable_keyword) + if(!keyword) + var/datum/preference/text/summonable_keyword/pref_entry = GLOB.preference_entries[/datum/preference/text/summonable_keyword] + keyword = pref_entry?.create_default_value() || "Beetlejuice" + return "Subject is summonable via keyword \"[keyword]\"." + // Adds the custom beetlejuice component and sets the beetlejuiec word. /datum/power/aberrant/summonable/post_add() if(!power_holder) 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 5eeeee1374c277..d0b49dc1f5e3ce 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 @@ -3,6 +3,8 @@ name = "Vent Crawl" desc = "Your anatomy is capable of fitting in tight spaces. You can crawl into vents if you are not wearing anything in your back slot, helmet slot or suit slot. \ \nIf you are undersized, you can crawl in vents while wearing your normal equipment. Does not work on oversized mobs." + security_record_text = "Subject can crawl through ventilation shafts." + security_threat = POWER_THREAT_MAJOR value = 5 power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous) 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 146dd1fc74db9c..d145dc408e7a63 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 @@ -2,6 +2,8 @@ /datum/power/aberrant/binding_webs name = "Binding Webs" desc = " Allows you to craft web restraints and web bolas using web crafter. Web restraints are functionally similar to zipties. Web Bolas can be thrown just like regular bolas." + security_record_text = "Subject can craft bolas and restraints from their spider silk." + security_threat = POWER_THREAT_MAJOR value = 3 required_powers = list(/datum/action/cooldown/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 85fa37f237a843..8e89242ae6a015 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 @@ -4,6 +4,8 @@ desc = "Allows you to craft snares. These are placed on the ground and are hard to see; but can be disarmed.\ \n Mobs without the ability to walk through webs will be legcuffed if they walk through it.\ \n Simple mobs instead receive a slowing status effect for 8 seconds." + security_record_text = "Subject can craft leg snaring traps from their spider silk." + security_threat = POWER_THREAT_MAJOR value = 3 required_powers = list(/datum/action/cooldown/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 88b17760f0e643..a99f26c382676a 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 @@ -3,6 +3,7 @@ desc = "Allows you to place near- invisible tripwires using web crafter.\ \n Any creature that isn't able to safely pass webs will trigger the tripwire when they pass through it, destroying it and warning you of which wire was triggered.\ \n Creatures immune to resonant scrying can trigger the webs without notifying you. Extreme distances and non-movement destruction will also not notify you." + security_record_text = "Subject can craft tripwires from their spider silk." value = 3 required_powers = list(/datum/action/cooldown/power/aberrant/web_crafter) 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 13fc56d4772c9e..c539b9ab83ef43 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 @@ -3,6 +3,7 @@ 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." + security_record_text = "Subject can craft spider-like silk from their body." value = 3 required_powers = list(/datum/power/aberrant_root/beastial) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm index 1bd8f2038ff148..d119de64469ef1 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -3,6 +3,8 @@ desc = "You gain your Dantian's aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do extra burn damage.\ Passively, your cold temprature tolerance is increased by 40C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\ You gain armor IV across your whole body. Has diminishing effects with your worn armor." + security_record_text = "Subject is capable of entering a heightened state by observing space, granting them resistance to damage, deadlier punches and the ability to ignore cold tempratures and low pressure." + security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/cultivator/alignment/astral_touched value = 6 diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm index 5383d16133c318..2d45dd47cdfe79 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -3,6 +3,8 @@ desc = "You gain your Dantian's aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do extra burn damage.\ Passively, your high temprature threshold is increased by 60C regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ You gain armor III (with laser VI and fire X) across your whole body. Has diminishing effects with your worn armor." + security_record_text = "Subject is capable of entering a heightened state by observing fires, granting them resistance to damage (especially lasers & fire), deadlier punches and the ability to ignore hot tempratures and fire." + security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/cultivator/alignment/flame_soul value = 6 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 c5dbdc910e18d4..a4f5f7eaa905b2 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 @@ -2,7 +2,7 @@ name = "Fly Like A Shooting Star" desc = "Whilst your alignment is active, you can fly. You can propel yourself through the air and space as if wearing a jetpack. \ If you aren't able to use your legs, you're able to move around with this ability, regardless of the current gravity." - + security_record_text = "Subject can fly regardless of gravitational environment whilst in their heightened state." value = 3 required_powers = list(/datum/power/cultivator_root/astral_touched) 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 e94ffe64aeb99f..6da44992f665c0 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 @@ -4,6 +4,8 @@ /datum/power/cultivator/from_friction_comes_flame name = "From Friction Comes Flame" desc = "Your punches while in alignment cause the target to heat up. Once they reach 80C, your strikes also combust the target." + security_record_text = "Subject heats up and ignites targets with their punches while in their heightened state." + security_threat = POWER_THREAT_MAJOR value = 3 required_powers = list(/datum/power/cultivator_root/flame_soul) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm index 8b84b1865b5f8f..ceaf72eb277737 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm @@ -4,7 +4,8 @@ ou can have up to 8 of these active. \ While in alignment, you can right click with this ability to explode all active stars that are not in motion dealing 20 burn damage to all creatures in a 3x3 area centered on it. \ Exploding the stars consumes Dantian per star. No cooldown." - + security_record_text = "Subject can shoot lights to illuminate an area, which can be detonated while in a heightened state to explode and damage those around it." + security_threat = POWER_THREAT_MAJOR value = 5 required_powers = list(/datum/power/cultivator_root/astral_touched) action_path = /datum/action/cooldown/power/cultivator/many_stars diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm index dd3abce49652b4..e14703b931e014 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm @@ -3,7 +3,8 @@ desc = "You can set fire onto anything you touch. This works similary to a ligher in terms of functionality. \ While in Alignment, you can right click shoot a flameblast that ignite everything in the area where it lands. \ Using the alignment version consumes Dantian. No cooldown." - + security_record_text = "Subject can set fire to any object in melee range. While in a heightened state, they can shoot motes of flame to ignite anything hit as well." + security_threat = POWER_THREAT_MAJOR value = 5 required_powers = list(/datum/power/cultivator_root/flame_soul) action_path = /datum/action/cooldown/power/cultivator/set_fire_to_dry_hay diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm index 38f43a2f5d4f1b..bb432371b34f67 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm @@ -4,8 +4,9 @@ You are entirely unrecognizeable in this state and your punches do extra brute damage.\ Passively, you have enhanced darkvision, and gain full on night vision while your alignment is activated.\ You gain armor IV across your whole body. Has diminishing effects with your worn armor." + security_record_text = "Subject can enter a heightened state by observing darkness, granting them resistance to damage, deadlier punches, the abiliy to become unrecognizeable as a dark silhouette and the ability to see perfectly in the dark." + security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/cultivator/alignment/shadow_walker - value = 5 // Lets you see in the dark. diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm index 3bc2f42ad00764..3f84e6bc4d5529 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm @@ -7,6 +7,8 @@ /datum/power/cultivator/travel_under_the_veil_of_night name = "Travel Under the Veil of Night" desc = "Whilst your alignment is active, you can spend 2 seconds channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Dantian cost; no cooldown." + security_record_text = "Subject can teleport in darkness while in their heightened state." + security_threat = POWER_THREAT_MAJOR value = 5 required_powers = list(/datum/power/cultivator_root/shadow_walker) action_path = /datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night 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 2d290a39a47e20..c2d24aaaf28334 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 @@ -5,6 +5,8 @@ /datum/power/cultivator/vanish_unseen_into_shadow name = "Vanish Unseen into Shadow" desc = "You are untrackable within the shadows. You are immune to resonant scrying and slowdowns while you're stood in darkness or are in alignment." + security_record_text = "Subject is exceedingly fast and immune to resonant-based detection while stood in darkness." + security_threat = POWER_THREAT_MAJOR value = 5 required_powers = list(/datum/power/cultivator_root/shadow_walker) power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES 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 de744536b24e6c..93a5e4eca35757 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 @@ -4,7 +4,7 @@ desc = "An organ found only in the central nervous system of Psykers \ grown by prolonged exposure to certain types of Resonance. \ The catalyst for psychic abilities; but beware overexerting it." - + security_record_text = "Subjects has a Paracausal Gland and wields psionic abilities." value = 2 power_flags = POWER_HUMAN_ONLY mob_trait = TRAIT_ARCHETYPE_RESONANT 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 3b662308188de1..01cc349a43be69 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -1,9 +1,8 @@ /datum/power/psyker_power/levitate name = "Levitate" desc = "Grants the ability to levitate yourself above surfaces and letting you propel yourself in zero-gravity. Passively drains stress while in use." - + security_record_text = "Subject can levitate their body regardless of the current gravity." value = 4 - priority = POWER_PRIORITY_BASIC required_powers = list(/datum/power/psyker_root) action_path = /datum/action/cooldown/power/psyker/levitate 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 75dfb4bf72c121..551c851f24b56f 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm @@ -10,7 +10,8 @@ /datum/power/psyker_power/manipulate name = "Manipulate" desc = "Allows you to interact with machinery and various other structures within line of sight as if it were next to you." - + security_record_text = "Subject can psychically interact with objects from a distance." + security_threat = POWER_THREAT_MAJOR value = 2 action_path = /datum/action/cooldown/power/psyker/manipulate mob_trait = TRAIT_NO_UI_DISTANCE 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 dd4c81ee3b1f14..25d5176e9d2558 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -7,9 +7,8 @@ desc = "Using a sample of a creature's blood, you can see the world through their eyes remotely. Creatures will be vague and hard to distinguish, but their environment will appear clear. \ In this state, you use their sight instead of your own; but you cannot target creatures that are immune to magic, scrying; or lack the brain activity required to be detectable (dumb). \ Passively builds up stress. The target sometimes gets preminations to indicate they are watched." - + security_record_text = "Subject can psychically observe people's locations based on blood samples from extreme distances." value = 10 - priority = POWER_PRIORITY_BASIC action_path = /datum/action/cooldown/power/psyker/scrying /datum/action/cooldown/power/psyker/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 a03eff37e7d1c9..e8c1235a36622a 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -6,9 +6,9 @@ TODO: FIX THAT /datum/power/psyker_power/telekinesis name = "Telekinesis" desc = "Grants the ability to manipulate and move various objects. Generates stress based upon weight on pick-up and throw, as well as passively while holding an object." - + security_record_text = "Subject can wield telekinesis to maneuver and fling objects." + security_threat = POWER_THREAT_MAJOR value = 5 - priority = POWER_PRIORITY_BASIC required_powers = list(/datum/power/psyker_root) action_path = /datum/action/cooldown/power/psyker/telekinesis From 760e1437ef0df1288a1d369d6604b671f3fc2d3f Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 14 Mar 2026 13:50:36 +0100 Subject: [PATCH 136/212] Changes the unarmed hit signaler to work with martial arts. Made Krav Maga more expensive to compensate. --- code/datums/martial/_martial.dm | 29 ++++++++++++++++++- .../powers/mortal/warfighter/krav_maga.dm | 7 ++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/code/datums/martial/_martial.dm b/code/datums/martial/_martial.dm index 4a0c9c84b3089f..8962eb202291e2 100644 --- a/code/datums/martial/_martial.dm +++ b/code/datums/martial/_martial.dm @@ -102,10 +102,37 @@ if(HAS_TRAIT(source, TRAIT_PACIFISM) && !pacifist_style) return NONE - return harm_act(source, attack_target) + // DOPPLER EDIT START - Sends unarmed hit signaler with martial arts + var/harm_result = harm_act(source, attack_target) + if(harm_result & COMPONENT_CANCEL_ATTACK_CHAIN) + send_unarmed_hit_signal(source, attack_target) + return harm_result + // DOPPLER EDIT END return help_act(source, attack_target) +// DOPPLER EDIT START - Sends unarmed hit signaler with martial arts +/// Emits the same unarmed-hit signal as the default species punch path, so power riders also fire for martial arts. +/datum/martial_art/proc/send_unarmed_hit_signal(mob/living/attacker, mob/living/defender) + PROTECTED_PROC(TRUE) + if(!attacker || !defender) + return + + var/obj/item/bodypart/affecting = defender.get_bodypart(defender.get_random_valid_zone(attacker.zone_selected)) + var/armor_block = 0 + if(affecting) + armor_block = defender.run_armor_check(affecting, MELEE) + + var/obj/item/bodypart/attacking_limb = get_attacking_limb(attacker, defender) + if(!attacking_limb && ishuman(attacker)) + var/mob/living/carbon/human/human_attacker = attacker + attacking_limb = human_attacker.get_active_hand() + + var/limb_accuracy = attacking_limb?.unarmed_effectiveness || 0 + var/limb_sharpness = attacking_limb?.unarmed_sharpness + SEND_SIGNAL(attacker, COMSIG_HUMAN_UNARMED_HIT, defender, affecting, 0, armor_block, limb_accuracy, limb_sharpness) +// DOPPLER EDIT END + /// Signal proc for [COMSIG_LIVING_GRAB] to hook into the grab /datum/martial_art/proc/attempt_grab(mob/living/source, mob/living/grabbing) SIGNAL_HANDLER diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm index 9fc4c5d1e67268..0d5a317e3708e5 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/krav_maga.dm @@ -3,11 +3,10 @@ */ /datum/power/warfighter/krav_maga name = "Krav Maga" - desc = "Trained in various disarming moves, you can wield the martial arts of Krav Maga without any external assistance.\ - (Powers that give you access to Martial Arts override your unarmed attacks and thusly do not stack with any modifier that affect your punches)" - security_record_text = "Subject has studied Martial Arts and understands Krav Maga." + desc = "Trained in various disarming moves, you can wield the martial arts of Krav Maga without any external assistance." + security_record_text = "Subject can wield Krav Maga in unarmed combat." security_threat = POWER_THREAT_MAJOR - value = 8 + value = 10 required_powers = list(/datum/power/warfighter/martial_artist) /// Mindbound martial art component so the style follows mind transfers var/datum/component/mindbound_martial_arts/krav_component From f62cdf3e47c17786fa7636669332cac0c086527e Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 14 Mar 2026 14:56:22 +0100 Subject: [PATCH 137/212] Adds false power and hidden power, letting you mess with your sec records. --- .../code/powers/mortal/expert/false_power.dm | 99 +++++++++++++++++++ .../powers/mortal/expert/hidden_powers.dm | 41 ++++++++ tgstation.dme | 2 + .../powers/false_power.tsx | 18 ++++ 4 files changed, 160 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/false_power.dm create mode 100644 modular_doppler/modular_powers/code/powers/mortal/expert/hidden_powers.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/false_power.tsx diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/false_power.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/false_power.dm new file mode 100644 index 00000000000000..9425575eedad71 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/false_power.dm @@ -0,0 +1,99 @@ +/* + Fill the sec records with a fake power. Or really anything else you want to write down. +*/ + +/datum/power/expert/false_power + name = "False Power" + desc = "A bit of misinformation about your capabilities and its immediately on record. Allows you to add a 'fake' power entry to your Security Records, tailored to your design." + value = 1 + +/datum/power/expert/false_power/add(client/client_source) + apply_false_power_prefs(client_source) + +/datum/power/expert/false_power/post_add() + apply_false_power_prefs(power_holder?.client) + . = ..() + +/datum/power/expert/false_power/get_security_record_text() + var/custom_record = power_holder?.client?.prefs?.read_preference(/datum/preference/text/false_power_entry) + if(isnull(custom_record)) + var/datum/preference/text/false_power_entry/pref_entry = GLOB.preference_entries[/datum/preference/text/false_power_entry] + custom_record = pref_entry?.create_default_value() || security_record_text + + if(!istext(custom_record)) + return security_record_text + + custom_record = trim(custom_record) + if(isnull(reject_bad_text(custom_record, 100, ascii_only = TRUE))) + return security_record_text + + return custom_record + +/datum/power/expert/false_power/proc/apply_false_power_prefs(client/client_source) + if(!client_source) + security_threat = POWER_THREAT_MINOR + return + + var/severity_pref = client_source.prefs?.read_preference(/datum/preference/choiced/false_power_severity) + switch(severity_pref) + if("Major") + security_threat = POWER_THREAT_MAJOR + else + security_threat = POWER_THREAT_MINOR + +// Preference choice for the fake security record entry text. +/datum/preference/text/false_power_entry + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "false_power_entry" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + maximum_value_length = 100 + +/datum/preference/text/false_power_entry/create_default_value() + return "Subject has been observed displaying unusual abilities." + +/datum/preference/text/false_power_entry/is_valid(value) + if(!istext(value)) + return FALSE + + var/trimmed_value = trim(value) + if(length(trimmed_value) < 1) + return FALSE + + return !isnull(reject_bad_text(trimmed_value, maximum_value_length, ascii_only = TRUE)) + +/datum/preference/text/false_power_entry/deserialize(input, datum/preferences/preferences) + var/value = ..() + if(!istext(value)) + return null + + value = trim(value) + if(!is_valid(value)) + return null + + return value + +/datum/preference/text/false_power_entry/apply_to_human(mob/living/carbon/human/target, value) + return + +// Preference choice for fake power severity in security records. +/datum/preference/choiced/false_power_severity + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "false_power_severity" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/false_power_severity/create_default_value() + return "Minor" + +/datum/preference/choiced/false_power_severity/init_possible_values() + return list("Minor", "Major") + +/datum/preference/choiced/false_power_severity/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/power_constant_data/false_power + associated_typepath = /datum/power/expert/false_power + customization_options = list( + /datum/preference/text/false_power_entry, + /datum/preference/choiced/false_power_severity + ) diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/hidden_powers.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/hidden_powers.dm new file mode 100644 index 00000000000000..85909dc4ac6eaa --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/hidden_powers.dm @@ -0,0 +1,41 @@ +/datum/power/expert/hidden_powers + name = "Hidden Powers" + desc = "Your capabilities were never put on paper, for one reason or another. Your powers are not visible in the security records.\ + \n If you have False Power, it will be the only preserved record of your powers." + value = 3 + /// Tracks each power's original security-record visibility so we can restore it on remove. + var/list/original_visibility = list() + +/datum/power/expert/hidden_powers/add(client/client_source) + apply_hidden_visibility() + +/datum/power/expert/hidden_powers/remove() + restore_hidden_visibility() + +/datum/power/expert/hidden_powers/proc/apply_hidden_visibility() + if(!power_holder) + return + + for(var/datum/power/power_instance as anything in power_holder.powers) + if(!(power_instance in original_visibility)) + original_visibility[power_instance] = power_instance.include_in_security_records + + if(istype(power_instance, /datum/power/expert/false_power)) + power_instance.include_in_security_records = TRUE + else + power_instance.include_in_security_records = FALSE + + power_holder.refresh_security_power_records() + +/datum/power/expert/hidden_powers/proc/restore_hidden_visibility() + if(!power_holder) + original_visibility.Cut() + return + + for(var/datum/power/power_instance as anything in original_visibility) + if(QDELETED(power_instance)) + continue + power_instance.include_in_security_records = original_visibility[power_instance] + + original_visibility.Cut() + power_holder.refresh_security_power_records() diff --git a/tgstation.dme b/tgstation.dme index f30ca249220952..83203963f5025c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7445,8 +7445,10 @@ #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" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/false_power.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/false_power.tsx new file mode 100644 index 00000000000000..279e9b2cdc11bc --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/false_power.tsx @@ -0,0 +1,18 @@ +import { + type Feature, + type FeatureChoiced, + FeatureShortTextInput, +} from '../../base'; +import { FeatureDropdownInput } from '../../dropdowns'; + +export const false_power_entry: Feature = { + name: 'False Power Entry', + description: 'Custom security record text (max 100 chars).', + component: FeatureShortTextInput, +}; + +export const false_power_severity: FeatureChoiced = { + name: 'False Power Severity', + description: 'Threat severity shown in security records.', + component: FeatureDropdownInput, +}; From d617e2ba483467d7a35a75752b12e3e698bca0af Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 14 Mar 2026 15:47:41 +0100 Subject: [PATCH 138/212] Silenced affects summonable, cleaned up meditate and made sure you only ever ge tone copy of it. Removed enigmatist stuff (we'll get back to it later) --- .../powers/resonant/aberrant/summonable.dm | 8 +- .../resonant/cultivator/_cultivator_root.dm | 8 +- .../code/powers/resonant/meditate.dm | 94 ++++++++++++++----- .../powers/resonant/psyker/_psyker_root.dm | 10 +- .../sorcerous/enigmatist/_enigmatist_root.dm | 8 +- .../sorcerous/enigmatist/lodestone_legends.dm | 4 +- .../enigmatist/test_powerthatrequirespower.dm | 8 -- tgstation.dme | 1 - 8 files changed, 99 insertions(+), 42 deletions(-) delete mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm index aee47407131a03..feb3a596eee776 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/summonable.dm @@ -3,7 +3,7 @@ */ /datum/power/aberrant/summonable name = "Summonable" - desc = "By speaking a specific name or word, you appear next to the speaker after a short delay. The summoning takes time, you are stunned throughout, is entirely involuntary and can only be stopped by being buckled or dispelled.\ + desc = "By speaking a specific name or word, you appear next to the speaker after a short delay. The summoning takes time, you are stunned throughout, is entirely involuntary and can only be stopped by being silenced, buckled or dispelled.\ \n After being succesfuly summoned, you are unable to be summoned again for 1 minute. \ \n The chosen word is a partial secret; the Security Records on your powers contain the word as well. It cannot contain any special characters, only standard letters and numbers." security_threat = POWER_THREAT_MAJOR @@ -77,7 +77,7 @@ var/atom/movable/summoned = parent if(ismob(summoned)) var/mob/living/living_summoned = summoned - if(living_summoned.buckled) + if(living_summoned.buckled || HAS_TRAIT(living_summoned, TRAIT_RESONANCE_SILENCED)) return var/turf/target_turf = get_adjacent_open_turf(target) if(QDELETED(summoned) || !target_turf) @@ -106,6 +106,10 @@ /datum/component/beetlejuice/summonable/proc/begin_summon(atom/movable/summoned, turf/target_turf) if(QDELETED(summoned) || QDELETED(target_turf)) return + if(isliving(summoned)) + var/mob/living/living_summoned = summoned + if(HAS_TRAIT(living_summoned, TRAIT_RESONANCE_SILENCED)) + return summoning = TRUE beaming_up = TRUE // Start departure immediately while runes are appearing. 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 7f90aad70add3b..7d882a08b514f9 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 @@ -15,7 +15,13 @@ // We pass along the piety component to actually handle most of the piety stuff. power_holder.AddComponent(/datum/component/cultivator_dantian, power_holder) // Passes along meditation. - grant_action(/datum/action/cooldown/power/resonant_meditate) + var/has_meditate = FALSE + for(var/datum/action/action as anything in power_holder.actions) + if(istype(action, /datum/action/cooldown/power/resonant_meditate)) + has_meditate = TRUE + break + if(!has_meditate) + grant_action(/datum/action/cooldown/power/resonant_meditate) . = ..() /datum/power/cultivator_root/remove() diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index a662c56b1694c2..b3f9f772b14b49 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -8,6 +8,7 @@ Reduces stress for psykers and restores Dantian for cultivators button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "chuuni" // Both Cultivator and Psyker can benefit from meditate. + var/psyker_spotlight_color = "#ba2cc9" // The components responsible for meditation. var/obj/item/organ/resonant/psyker/psyker_organ @@ -32,8 +33,8 @@ Reduces stress for psykers and restores Dantian for cultivators // Gets the owner's psyker organ & cultivator component update_components() // Adds visual effects - var/datum/status_effect/spotlight_light/light = get_spotlight_color() - spotlighttarget.apply_status_effect(light, 3000) + var/list/spotlight_config = get_meditation_spotlight_config(owner) + spotlighttarget.apply_status_effect(/datum/status_effect/spotlight_light/meditation, 3000, null, spotlight_config["color"], spotlight_config["emit_light"]) do active = TRUE if(do_after(owner, 25, target = owner)) @@ -58,24 +59,48 @@ Reduces stress for psykers and restores Dantian for cultivators to_chat(owner, "You stop meditating.") active = FALSE - spotlighttarget.remove_status_effect(light) + spotlighttarget.remove_status_effect(/datum/status_effect/spotlight_light/meditation) return -/datum/action/cooldown/power/resonant_meditate/proc/get_spotlight_color() - if(psyker_organ && cultivator_dantian) - return /datum/status_effect/spotlight_light/resonant - else if(psyker_organ) - return /datum/status_effect/spotlight_light/psyker - else if(cultivator_dantian) - return /datum/status_effect/spotlight_light/cultivator - else - return /datum/status_effect/spotlight_light +// Changes the colors on meditate to whatever matches alignment. +/datum/action/cooldown/power/resonant_meditate/proc/get_meditation_spotlight_config(mob/living/user) + var/list/config = list( + "color" = null, + "emit_light" = TRUE, + ) + var/datum/action/cooldown/power/cultivator/alignment/alignment_action = get_alignment_action(user) + if(alignment_action) + config["color"] = alignment_action.alignment_outline_color + config["emit_light"] = should_alignment_spotlight_emit_light(alignment_action) + return config + if(psyker_organ) // alignment color gets priority over psyker. + config["color"] = psyker_spotlight_color + return config + +/datum/action/cooldown/power/resonant_meditate/proc/get_alignment_action(mob/living/user) + if(!user) + return null + var/datum/action/cooldown/power/cultivator/alignment/first_alignment + for(var/datum/action/cooldown/power/cultivator/alignment/alignment_action in user.actions) + if(!first_alignment) + first_alignment = alignment_action + if(alignment_action.active) + return alignment_action + return first_alignment + +/datum/action/cooldown/power/resonant_meditate/proc/should_alignment_spotlight_emit_light(datum/action/cooldown/power/cultivator/alignment/alignment_action) + if(!alignment_action) + return TRUE + var/alignment_color = alignment_action.alignment_outline_color + // Dark colors should not emit a light source. + if(alignment_color && is_color_dark(alignment_color)) + return FALSE + return TRUE // gets the psyker organ and the cultivator component /datum/action/cooldown/power/resonant_meditate/proc/update_components() psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) cultivator_dantian = owner.GetComponent(/datum/component/cultivator_dantian) - //TODO: Cultivator Organ // Returns TRUE if any active Cultivator or Psyker power is active on the target. /datum/action/cooldown/power/resonant_meditate/proc/user_has_active_power(mob/living/user) @@ -89,13 +114,40 @@ Reduces stress for psykers and restores Dantian for cultivators return TRUE return FALSE -// I wish I could just change the color on spotlights but no we have to make it special. -/datum/status_effect/spotlight_light/psyker - id = "psyker_spotlight" - spotlight_color = "#ba2cc9" -/datum/status_effect/spotlight_light/cultivator - id = "cultivator_spotlight" - spotlight_color = "#66c5dd" -/datum/status_effect/spotlight_light/resonant // if you somehow have both +// Meditation spotlight with runtime color/light config. +/datum/status_effect/spotlight_light/meditation + id = "meditation_spotlight" + var/emit_light = TRUE + +/datum/status_effect/spotlight_light/meditation/on_creation(mob/living/new_owner, duration, additional_overlay, custom_spotlight_color, custom_emit_light) + if(!isnull(custom_spotlight_color)) + spotlight_color = custom_spotlight_color + if(!isnull(custom_emit_light)) + emit_light = custom_emit_light + . = ..() + +/datum/status_effect/spotlight_light/meditation/on_apply() + if(emit_light) + return ..() + + beam_from_above_a = new /obj/effect/overlay/spotlight + beam_from_above_a.color = spotlight_color + beam_from_above_a.alpha = 62 + owner.vis_contents += beam_from_above_a + beam_from_above_a.layer = BELOW_MOB_LAYER + + beam_from_above_b = new /obj/effect/overlay/spotlight + beam_from_above_b.color = spotlight_color + beam_from_above_b.alpha = 62 + beam_from_above_b.layer = ABOVE_MOB_LAYER + beam_from_above_b.pixel_y = -2 // Slight vertical offset for an illusion of volume. + owner.vis_contents += beam_from_above_b + + if(additional_overlay) + owner.add_overlay(additional_overlay) + return TRUE + +// Legacy subtype for other powers still referencing this path. +/datum/status_effect/spotlight_light/resonant id = "resonant_spotlight" spotlight_color = "#cf2525" 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 93a5e4eca35757..bd71ca6f781bc0 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 @@ -17,10 +17,16 @@ /datum/power/psyker_root/add(client/client_source) psyker_organ = new /obj/item/organ/resonant/psyker psyker_organ.Insert(power_holder, special = TRUE) - grant_action(/datum/action/cooldown/power/resonant_meditate) + if(power_holder) + var/has_meditate = FALSE + for(var/datum/action/action as anything in power_holder.actions) + if(istype(action, /datum/action/cooldown/power/resonant_meditate)) + has_meditate = TRUE + break + if(!has_meditate) + grant_action(/datum/action/cooldown/power/resonant_meditate) /datum/power/psyker_root/remove(client/client_source) if(psyker_organ) qdel(psyker_organ) - diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm index 4c74ad6c908794..f104ae4dcfe5f8 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm @@ -1,11 +1,9 @@ /datum/power/enigmatist_root - name = "Produce Resonant Chalk" - desc = "Learn how to produce Resonant Chalk with any crayon \ - and a sheet of plasma or Resonant Chalk Remnants. \ - This is mutually exclusive with Spell Preparation." + name = "Coming Soon!" + desc = "Only time will tell." - value = 2 + value = 999 mob_trait = TRAIT_ARCHETYPE_SORCEROUS archetype = POWER_ARCHETYPE_SORCEROUS path = POWER_PATH_ENIGMATIST diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm index a74f8f0d3fe347..6abbed8f24da71 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/lodestone_legends.dm @@ -1,5 +1,5 @@ -/datum/power/enigmatist_spell/lodestone_legends +/*/datum/power/enigmatist_spell/lodestone_legends name = "Lodestone Legends" desc = "Activate with any type of Chalk in hand to be told your GPS \ position. Causes minor damage to the Chalk" @@ -24,4 +24,4 @@ if(!damage_chalk(used_chalk, user, ENIGMATIST_CHALK_MINOR_DAMAGE)) return var/turf/current_turf = get_turf(used_chalk) - to_chat(user, span_notice("Your current coordinates are... [current_turf.x]x, [current_turf.y]y, [current_turf.z]z...")) + to_chat(user, span_notice("Your current coordinates are... [current_turf.x]x, [current_turf.y]y, [current_turf.z]z..."))*/ diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower.dm deleted file mode 100644 index cd3e2b1dfe53b0..00000000000000 --- a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/test_powerthatrequirespower.dm +++ /dev/null @@ -1,8 +0,0 @@ - -/datum/power/enigmatist_spell/powerthatrequirespower - name = "Power That Requires Power" - desc = "I need more POWER. And a better way to debug this, I suppose." - - value = 1 - priority = POWER_PRIORITY_BASIC - required_powers = list(/datum/power/enigmatist_spell/lodestone_legends) diff --git a/tgstation.dme b/tgstation.dme index 83203963f5025c..db423fd84e683a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7541,7 +7541,6 @@ #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\enigmatist\test_powerthatrequirespower.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\_thaumaturge_action.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" From fac338422d92cc5f6fc859ef9eb4d37097360a10 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 14 Mar 2026 16:21:28 +0100 Subject: [PATCH 139/212] Adds another beetlejuice variant; premonition. Emote when the word is mentioned. --- .../powers/resonant/psyker/premonition.dm | 142 ++++++++++++++++++ tgstation.dme | 1 + .../powers/premonition.tsx | 14 ++ 3 files changed, 157 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm create mode 100644 tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/premonition.tsx diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm new file mode 100644 index 00000000000000..68d10305f31721 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm @@ -0,0 +1,142 @@ +/** + Old wife tale of sneezing when your name is mentioned. +**/ +/datum/power/psyker_power/premonition + name = "Premonition" + desc = "You are aware when a particular something is mentioned; a hunch as it were.\ + \n Select a specific word or phrase; anytime someone mentions it (no matter where they are), you will trigger the chosen emote. Has a cooldown of 10 seconds." + security_record_text = "Subject has strange bodily reactions whenever a certain keyphrase is mentioned." + value = 2 + + // Trakcs the component + var/datum/component/beetlejuice/premonition/premonition_component + +// Adds the special beetlejuice component, gets the prefernece components. +/datum/power/psyker_power/premonition/post_add() + if(!power_holder) + return + + // Gets the holder and component + var/mob/living/holder = power_holder + var/datum/component/beetlejuice/premonition/component = holder.GetComponent(/datum/component/beetlejuice/premonition) + if(!component) + component = holder.AddComponent(/datum/component/beetlejuice/premonition) + + premonition_component = component + + // Sets the word of the day. + var/keyword = holder.client?.prefs?.read_preference(/datum/preference/text/premonition_keyword) + if(!keyword) + var/datum/preference/text/premonition_keyword/pref_entry = GLOB.preference_entries[/datum/preference/text/premonition_keyword] + keyword = pref_entry?.create_default_value() || "Premonition" + + component.keyword = keyword + component.update_regex() + + // Sets the emote key. + var/emote_choice = holder.client?.prefs?.read_preference(/datum/preference/choiced/premonition_emote) + var/datum/preference/choiced/premonition_emote/pref_entry = GLOB.preference_entries[/datum/preference/choiced/premonition_emote] + component.emote_key = pref_entry?.validate_premonition_emote_choice(emote_choice) || "sneeze" + . = ..() + +/datum/power/psyker_power/premonition/remove() + . = ..() + if(premonition_component) + QDEL_NULL(premonition_component) + +// Custom beetlejuice component for Premonition. +/datum/component/beetlejuice/premonition + min_count = 1 + cooldown = 10 SECONDS + var/emote_key = "sneeze" + +// When the phrase is mentioned. +/datum/component/beetlejuice/premonition/apport(atom/target) + var/atom/movable/triggered = parent + if(!ismob(triggered)) + return + var/mob/living/living_triggered = triggered + if(HAS_TRAIT(living_triggered, TRAIT_RESONANCE_SILENCED)) + return + if(!emote_key) + return + living_triggered.emote(emote_key, intentional = FALSE) + active = FALSE + addtimer(VARSET_CALLBACK(src, active, TRUE), cooldown) + +// Preference choice for Premonition keyword selection. +/datum/preference/text/premonition_keyword + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "premonition_keyword" + savefile_identifier = PREFERENCE_CHARACTER + can_randomize = FALSE + maximum_value_length = 32 + +/datum/preference/text/premonition_keyword/create_default_value() + return "Premonition" + +/datum/preference/text/premonition_keyword/is_valid(value) + if(!istext(value)) + return FALSE + if(length(value) < 1 || length(value) >= maximum_value_length) + return FALSE + // Allow only ASCII letters, numbers, and spaces. + var/quoted = REGEX_QUOTE(value) + var/static/regex/allowed_regex = regex("^" + ascii2text(91) + "A-Za-z0-9 " + ascii2text(93) + "+$") + allowed_regex.next = 1 + return !!allowed_regex.Find(quoted) + +/datum/preference/text/premonition_keyword/deserialize(input, datum/preferences/preferences) + var/value = ..() + if(!is_valid(value)) + return null + return value + +/datum/preference/text/premonition_keyword/apply_to_human(mob/living/carbon/human/target, value) + return + +// Preference choice for Premonition emote selection. +/datum/preference/choiced/premonition_emote + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "premonition_emote" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/premonition_emote/create_default_value() + return validate_premonition_emote_choice("sneeze") + +/datum/preference/choiced/premonition_emote/init_possible_values() + return get_premonition_emote_choices() + +/datum/preference/choiced/premonition_emote/is_accessible(datum/preferences/preferences) + if (!..(preferences)) + return FALSE + return TRUE + +/datum/preference/choiced/premonition_emote/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/preference/choiced/premonition_emote/proc/get_premonition_emote_choices() + var/list/choices = list() + for(var/key in GLOB.emote_list) + for(var/datum/emote/emote_action in GLOB.emote_list[key]) + if(emote_action.key == key) + choices += key + break + if(!length(choices)) + return list("sneeze") + return sort_list(choices) + +/datum/preference/choiced/premonition_emote/proc/validate_premonition_emote_choice(value) + if(!istext(value)) + value = null + var/list/choices = get_premonition_emote_choices() + if(value && (value in choices)) + return value + return choices[1] + +/datum/power_constant_data/premonition + associated_typepath = /datum/power/psyker_power/premonition + customization_options = list( + /datum/preference/text/premonition_keyword, + /datum/preference/choiced/premonition_emote + ) diff --git a/tgstation.dme b/tgstation.dme index db423fd84e683a..7adf1abdd5f906 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7519,6 +7519,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\levitate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\manipulate.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\premonition.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\scrying.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\_psyker_event.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/premonition.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/premonition.tsx new file mode 100644 index 00000000000000..f35255bc052db4 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/dopplershift_preferences/powers/premonition.tsx @@ -0,0 +1,14 @@ +import { Feature, FeatureShortTextInput, type FeatureChoiced } from '../../base'; +import { FeatureDropdownInput } from '../../dropdowns'; + +export const premonition_keyword: Feature = { + name: 'Premonition Keyword', + description: 'Phrase that triggers your premonition.', + component: FeatureShortTextInput, +}; + +export const premonition_emote: FeatureChoiced = { + name: 'Premonition Emote', + description: 'Emote triggered by your premonition.', + component: FeatureDropdownInput, +}; From 8f07f1ae425cf03e143ea7bfcbdbeb85b02ed056 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 15 Mar 2026 06:39:07 +0100 Subject: [PATCH 140/212] Removes debug logging from powers. --- .../code/powers/resonant/psyker/premonition.dm | 4 ++-- .../modular_powers/code/powers_prefs_middleware.dm | 1 - modular_doppler/modular_powers/code/powers_subsystem.dm | 8 -------- 3 files changed, 2 insertions(+), 11 deletions(-) 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 68d10305f31721..dca95a13e237c7 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/premonition.dm @@ -28,7 +28,7 @@ var/keyword = holder.client?.prefs?.read_preference(/datum/preference/text/premonition_keyword) if(!keyword) var/datum/preference/text/premonition_keyword/pref_entry = GLOB.preference_entries[/datum/preference/text/premonition_keyword] - keyword = pref_entry?.create_default_value() || "Premonition" + keyword = pref_entry?.create_default_value() || "Beetlejuice" component.keyword = keyword component.update_regex() @@ -73,7 +73,7 @@ maximum_value_length = 32 /datum/preference/text/premonition_keyword/create_default_value() - return "Premonition" + return "Beetlejuice" /datum/preference/text/premonition_keyword/is_valid(value) if(!istext(value)) diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index c49cee9534dee8..b5b121efd141b7 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -217,7 +217,6 @@ // Make sure we don't select an incompatible power. var/datum/power/incompatible_power_type = get_incompatible_power(power_type) - message_admins("giver_power BLACKLIST -
incompatible_power_type: [incompatible_power_type]") if(incompatible_power_type) to_chat(user, span_boldwarning("[power_name] is incompatible with [incompatible_power_type.name]!")) return FALSE diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index ebb62adefbd44a..b02ffa8e8f754f 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -108,22 +108,14 @@ PROCESSING_SUBSYSTEM_DEF(powers) continue LAZYADDASSOCLIST(powers_by_priority, power_type.priority, power_type) - message_admins("assign_powers FIRST -
powers_by_priority: [powers_by_priority]
bad_power: [bad_power]") - if(bad_power) applied_client.prefs.save_character() - message_admins("assign_powers SECOND -
power_priorities: [power_priorities]") - for(var/power_priority in power_priorities) var/list/priority_powers = powers_by_priority[power_priority] - message_admins("assign_powers THIRD(LOOP) -
power_priority: [power_priority]
priority_powers: [priority_powers]") if(isnull(priority_powers)) continue - for(var/whatever in priority_powers) - message_admins("assign_powers 3-4(LOOP) -
whatever: [whatever]") for(var/datum/power/power_type as anything in priority_powers) - message_admins("assign_powers FOURTH(LOOP) -
power_type: [power_type]
priority_powers: [priority_powers]") if(!user.add_archetype_power(power_type, override_client = applied_client)) continue SSblackbox.record_feedback("tally", "powers_taken", 1, "[power_type.name]") From 1fae1b3f81a41c0f2c29298424697ce47d78e383 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 15 Mar 2026 09:12:25 +0100 Subject: [PATCH 141/212] Fixes savefiles getting hypernuked. --- .../_savefile_migration/code/_preferences_savefile.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modular_doppler/_savefile_migration/code/_preferences_savefile.dm b/modular_doppler/_savefile_migration/code/_preferences_savefile.dm index 49d1185b7c0771..a302af65e5d591 100644 --- a/modular_doppler/_savefile_migration/code/_preferences_savefile.dm +++ b/modular_doppler/_savefile_migration/code/_preferences_savefile.dm @@ -29,8 +29,8 @@ save_data["languages"] = languages save_data["alt_job_titles"] = alt_job_titles save_data["all_powers"] = all_powers - // load_character will sanitize any bad data, so assume up-to-date. - save_data["version"] = DOPPLER_SAVEFILE_VERSION_MAX + // Track Doppler-specific savefile version separately from core prefs. + save_data["doppler_version"] = DOPPLER_SAVEFILE_VERSION_MAX #undef DOPPLER_SAVEFILE_VERSION_MAX #undef VERSION_NEW_POWERS From 6606f5e630f6082eb2fc111489a606c7ad2cdffe Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sun, 15 Mar 2026 19:49:21 +0100 Subject: [PATCH 142/212] Adds mirage. Lets you create illusions. --- .../code/powers/resonant/psyker/mirage.dm | 256 ++++++++++++++++++ tgstation.dme | 1 + 2 files changed, 257 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm new file mode 100644 index 00000000000000..1b50a4113069dd --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm @@ -0,0 +1,256 @@ +// Mirage mode constants +#define MIRAGE_MODE_STATIONARY 1 +#define MIRAGE_MODE_AGGRESSIVE 2 +#define MIRAGE_MODE_FLEE 3 + +/* + Create duplicates of yourself with varying AI behaviors. +*/ +/datum/power/psyker_power/mirage + name = "Mirage" + desc = "Creates an illusory copy of yourself for 20 seconds; it has one health and draws aggression from creatures, but doesn't deal damage and can be walked through.\ + \n Right click with the power selected to change its behavior between stationary, aggressive and flee. Creatures immune to mental and resonant effects disbelieve the illusion, making them see-through and pass-through. \ + \n Creating the illusion creates a moderate amount of stress." + security_record_text = "Subject can create illusory duplicates of themselves." + security_threat = POWER_THREAT_MAJOR + value = 5 + required_powers = list(/datum/power/psyker_root) + action_path = /datum/action/cooldown/power/psyker/mirage + +/datum/action/cooldown/power/psyker/mirage + name = "Mirage" + desc = "Creates an illusory copy of yourself for 20 seconds; it has one health and draws aggression from creatures, but doesn't deal damage and can be walked through.\ + \n Right click with the power selected to change its behavior between stationary, aggressive and flee. Creatures immune to mental and resonant effects disbelieve the illusion, making them see-through and pass-through." + button_icon = 'icons/mob/actions/actions_minor_antag.dmi' + button_icon_state = "chrono_phase" + click_to_activate = TRUE + unset_after_click = FALSE + + // Active mirage instances + var/list/active_mirages = list() + // Mirage behavior mode + var/mode = MIRAGE_MODE_STATIONARY + // Stress cost + var/stress_cost = PSYKER_STRESS_MODERATE * 1.5 + +// WE get the right click behavior to cycle behavior. +/datum/action/cooldown/power/psyker/mirage/InterceptClickOn(mob/living/clicker, params, atom/target) + if(clicker != owner) + return FALSE + + var/list/mods = params2list(params) + if(LAZYACCESS(mods, RIGHT_CLICK)) + cycle_mode() + return TRUE + + return ..() + +/datum/action/cooldown/power/psyker/mirage/use_action(mob/living/user, atom/target) + . = ..() + if(!owner) + return FALSE + + cleanup_mirages() + + var/turf/spawn_turf = get_turf(target) || get_turf(owner) + if(!spawn_turf) + return FALSE + + // Creates a new instance of the mirrage + var/mob/living/simple_animal/hostile/illusion/mirage/resonant/new_mirage = new(spawn_turf) + new_mirage.Copy_Parent(owner, 20 SECONDS, 1, 0) + new_mirage.set_action_ref(src) + new_mirage.apply_mode(mode) + new_mirage.match_owner_speed(owner) + active_mirages += new_mirage + + // Causes it to act immediately. + new_mirage.FindTarget() + + modify_stress(stress_cost) + playsound(new_mirage, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + + return TRUE + +// Changes behavior of the sapawned illusion. +/datum/action/cooldown/power/psyker/mirage/proc/cycle_mode() + if(mode == MIRAGE_MODE_STATIONARY) + mode = MIRAGE_MODE_AGGRESSIVE + owner?.balloon_alert(owner, "set to Aggressive") + else if(mode == MIRAGE_MODE_AGGRESSIVE) + mode = MIRAGE_MODE_FLEE + owner?.balloon_alert(owner, "set to Flee") + else + mode = MIRAGE_MODE_STATIONARY + owner?.balloon_alert(owner, "set to Stationary") + +/datum/action/cooldown/power/psyker/mirage/Remove(mob/removed_from) + . = ..() + for(var/mob/living/simple_animal/hostile/illusion/mirage/resonant/mirage as anything in active_mirages) + if(!QDELETED(mirage)) + qdel(mirage) + active_mirages.Cut() + +/datum/action/cooldown/power/psyker/mirage/proc/cleanup_mirages() + for(var/mob/living/simple_animal/hostile/illusion/mirage/resonant/mirage as anything in active_mirages.Copy()) + if(QDELETED(mirage)) + active_mirages -= mirage + + +/* + Mirage mob: simple animal used for aggro, but with per-viewer masking. +*/ +/mob/living/simple_animal/hostile/illusion/mirage/resonant + var/datum/weakref/action_ref + var/last_mode = MIRAGE_MODE_STATIONARY + var/alt_appearance_key + density = TRUE + melee_damage_lower = 0 + melee_damage_upper = 0 + obj_damage = 0 + environment_smash = ENVIRONMENT_SMASH_NONE + attack_verb_continuous = "attacks" + attack_verb_simple = "attack" + +/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/set_action_ref(datum/action/cooldown/power/psyker/mirage/action) + action_ref = WEAKREF(action) + if(!alt_appearance_key) + alt_appearance_key = "mirage_alpha_[REF(src)]" + var/image/appearance_image = image(loc = src) + appearance_image.appearance = appearance + appearance_image.dir = dir + add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/mirage_alpha, alt_appearance_key, appearance_image, action, action?.owner) + RegisterSignal(src, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_mirage_dir_change)) + RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_mirage_dispel)) + +/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/match_owner_speed(mob/living/owner) + if(!owner) + return + if(!isnum(owner.cached_multiplicative_slowdown)) + return + var/desired = max(0.1, owner.cached_multiplicative_slowdown) + move_to_delay = desired + set_varspeed(desired) + +// Applies the selection AI mode. Have your illusions act as you please :D +/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/apply_mode(new_mode) + last_mode = new_mode + + switch(new_mode) + if(MIRAGE_MODE_STATIONARY) + stop_automated_movement = TRUE + toggle_ai(AI_IDLE) + if(MIRAGE_MODE_AGGRESSIVE) + stop_automated_movement = FALSE + retreat_distance = 0 + minimum_distance = 0 + toggle_ai(AI_ON) + if(MIRAGE_MODE_FLEE) + stop_automated_movement = FALSE + retreat_distance = 10 + minimum_distance = 10 + toggle_ai(AI_ON) + +/mob/living/simple_animal/hostile/illusion/mirage/resonant/Destroy() + if(alt_appearance_key) + remove_alt_appearance(alt_appearance_key) + alt_appearance_key = null + UnregisterSignal(src, COMSIG_ATOM_DIR_CHANGE) + UnregisterSignal(src, COMSIG_ATOM_DISPEL) + action_ref = null + return ..() + +/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/on_mirage_dispel(datum/source, atom/dispeller) + SIGNAL_HANDLER + qdel(src) + return DISPEL_RESULT_DISPELLED + +// We need to tell the alt appearance variant to turn. +/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/on_mirage_dir_change(datum/source, old_dir, new_dir) + SIGNAL_HANDLER + var/image/appearance_image = hud_list?[alt_appearance_key] + if(appearance_image) + appearance_image.dir = new_dir + +// If you have disbelieved the illusion (immune to mental) you can just walk through them. +/mob/living/simple_animal/hostile/illusion/mirage/resonant/CanAllowThrough(atom/movable/mover, border_dir) + if(should_ignore_target(mover)) + return TRUE + return ..() + +// We don't aggro our owner, +/mob/living/simple_animal/hostile/illusion/mirage/resonant/CanAttack(atom/the_target) + if(should_ignore_target(the_target)) + return FALSE + return ..() + +// Basically we check if they're our owner, are affected by mental or are an illusion of the same mob. +/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/should_ignore_target(atom/target) + var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve() + if(!action || !ismob(target) || !isliving(target)) + return FALSE + var/mob/living/living_target = target + var/mob/living/owner = action.owner + if(owner && living_target == owner) // owner + return TRUE + if(!action.can_affect_mental(living_target)) // magic immune + return TRUE + if(istype(living_target, /mob/living/simple_animal/hostile/illusion)) // ilusion of the same mob + var/mob/living/simple_animal/hostile/illusion/illusion_target = living_target + if(illusion_target.parent_mob_ref?.resolve() == owner) + return TRUE + return FALSE + +// We basically do a fake attack to sell the 'illusion'. We don't want it to actually deal damage, or people will have hissyfit arguments that these are 'harmful' and should be 'illegal' +/mob/living/simple_animal/hostile/illusion/mirage/resonant/AttackingTarget(atom/attacked_target) + if(!isliving(attacked_target)) + return FALSE + + var/mob/living/living_target = attacked_target + do_attack_animation(living_target, ATTACK_EFFECT_PUNCH) + + var/verb_continuous = attack_verb_continuous || "attacks" + var/verb_simple = attack_verb_simple || "attack" + + visible_message( + span_danger("[src] [verb_continuous] [living_target]!"), + span_userdanger("[src] [verb_continuous] you!"), + null, + COMBAT_MESSAGE_RANGE, + src + ) + to_chat(src, span_danger("You [verb_simple] [living_target]!")) + + if(attacked_sound) + playsound(loc, attacked_sound, 25, TRUE, -1) + + return TRUE + + +// Alternate appearance for mirage: semi-transparent for owner and mental-immune viewers. +/datum/atom_hud/alternate_appearance/basic/mirage_alpha + var/datum/weakref/action_ref + var/datum/weakref/owner_ref + var/alpha_override = 80 + +/datum/atom_hud/alternate_appearance/basic/mirage_alpha/New(key, image/appearance_image, datum/action/cooldown/power/psyker/mirage/action, mob/living/owner, options = AA_TARGET_SEE_APPEARANCE) + action_ref = WEAKREF(action) + owner_ref = WEAKREF(owner) + if(appearance_image) + appearance_image.alpha = alpha_override + appearance_image.override = TRUE + . = ..(key, appearance_image, options) + +// Who is ALLOWED to see us for who we truly are? +/datum/atom_hud/alternate_appearance/basic/mirage_alpha/mobShouldSee(mob/viewer) + var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve() + if(!action || !ismob(viewer) || !isliving(viewer)) + return FALSE + var/mob/living/owner = owner_ref?.resolve() + if(owner && viewer == owner) + return TRUE + return !action.can_affect_mental(viewer) + +#undef MIRAGE_MODE_STATIONARY +#undef MIRAGE_MODE_AGGRESSIVE +#undef MIRAGE_MODE_FLEE diff --git a/tgstation.dme b/tgstation.dme index 7adf1abdd5f906..c3d53cd709f005 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7519,6 +7519,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\_psyker_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\levitate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\manipulate.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\mirage.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\premonition.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\scrying.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm" From 500a5cebbc46b92d98f36885424f82b159611c37 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 17 Mar 2026 16:11:27 +0100 Subject: [PATCH 143/212] Tweaks affinity by giving it to a load of items; added ward mind to do properly protect da brain, overloading as a psyker now also dispels you. --- code/game/objects/items.dm | 7 + .../resonant/aberrant/radiosynthesis.dm | 2 +- .../powers/resonant/psyker/_psyker_organ.dm | 3 +- .../code/powers/resonant/psyker/ward_mind.dm | 116 +++++++++ .../thaumaturge/_thaumaturge_action.dm | 7 +- .../affinity/thaumaturge_affinity.dm | 240 +++++++++++++++++- tgstation.dme | 1 + 7 files changed, 362 insertions(+), 14 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/ward_mind.dm diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 75ea737dcd1d55..bca7017e3394a4 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -72,6 +72,8 @@ var/list/bodyshape_icon_files // Used for the affinity system in the Powers system, by Thaumaturge. var/affinity = 0 + // Item gets affinity from being worn; useful for items that can be worn but arent obj/item/clothing + var/affinity_worn_override /// DOPPLER SHIFT ADDITION END /* !!!!!!!!!!!!!!! IMPORTANT !!!!!!!!!!!!!! @@ -494,6 +496,11 @@ else if (siemens_coefficient <= 0.5) .["partially insulated"] = "It is made from a poor insulator that will dampen (but not fully block) electric shocks passing through it." + // DOPPLER EDIT START: Thaumaturges can examine items for affinity stat + if(affinity && (HAS_TRAIT(user, TRAIT_ARCHETYPE_SORCEROUS) || isobserver(user))) + .["affinity [affinity]"] = "Provides affinity [affinity] for thaumaturgic powers." + // DOPPLER EDIT END + /obj/item/examine_descriptor(mob/user) return "item" diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm index 425cbc67410b6a..094c7f614b48ae 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/radiosynthesis.dm @@ -34,6 +34,6 @@ // Heal toxins if we didn't heal any other damage, but never remove the last point (keeps irradiation). var/tox_loss = power_holder.getToxLoss() - if(tox_loss > 1 && heal_amt > tox_loss) // We don't want to heal all of a person's radiation, just as to preserve their radioactiv + if(tox_loss > 1 && heal_amt < tox_loss) // We don't want to heal all of a person's radiation, just as to preserve their radioactiv var/tox_heal = min(heal_amt, tox_loss - 1) power_holder.adjustToxLoss(-tox_heal) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index e1878f359f1686..2d5ce47a2e460d 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -51,12 +51,13 @@ stress_to_recover = max(stress_to_recover, 0) // Apply recovery, don't let it send stress into the negatives. - stress = max(stress - stress_to_recover * seconds_per_tick, 0) + stress = max(stress - (stress_to_recover * seconds_per_tick), 0) // Check if we do stress backlash after stress reduction. if(stress >= (stress_threshold * 2)) // Catastrophic event. stress_backlash(PSYKER_EVENT_TIER_CATASTROPHIC) + dispel(owner, src) // ends most effects stress = 0 // No CD, just a hard reset and the consequences of your actions. CDstressMild = 0 CDstressSevere = 0 diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/ward_mind.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/ward_mind.dm new file mode 100644 index 00000000000000..99625996765b73 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/ward_mind.dm @@ -0,0 +1,116 @@ +/datum/power/psyker_power/ward_mind + name = "Ward Mind" + desc = "Temporarily strengthens your mind to block out mental assaults. You become immune to various abilities that use the mental trait as well as resonance-based detection, such as a large variety of Psyker powers but also a handful of other interactions.\ + \n You gain Stress passively while active, and a moderate amount is gained whenever you block a mental ability." + security_record_text = "Subject can ward their mind against mental assault and scrying." + value = 5 + required_powers = list(/datum/power/psyker_root) + action_path = /datum/action/cooldown/power/psyker/ward_mind + +/datum/action/cooldown/power/psyker/ward_mind + name = "Ward Mind" + desc = "Temporarily strengthens your mind to block out mental assaults. You become immune to various abilities that use the mental trait as well as resonance-based detection, such as a large variety of Psyker powers but also a handful of other interactions.\ + \n You gain Stress passively while active, and a moderate amount is gained whenever you block a mental ability." + button_icon = 'icons/mob/actions/actions_elites.dmi' + button_icon_state = "magic_box" + cooldown_time = 15 SECONDS + + // The status effect on the caster. + var/datum/status_effect/power/ward_mind/active_effect + +/datum/action/cooldown/power/psyker/ward_mind/Remove(mob/removed_from) + . = ..() + if(active_effect) + qdel(active_effect) + active_effect = null + active = FALSE + +/datum/action/cooldown/power/psyker/ward_mind/use_action(mob/living/user, atom/target) + if(active_effect) + qdel(active_effect) + active_effect = null + active = FALSE + to_chat(user, span_notice("You let your mental guard down.")) + else + active_effect = user.apply_status_effect(/datum/status_effect/power/ward_mind, src) + active = TRUE + to_chat(user, span_notice("Your mind wards against intrusion.")) + build_all_button_icons(UPDATE_BUTTON_STATUS) + return TRUE + +/datum/status_effect/power/ward_mind + id = "ward_mind" + alert_type = /atom/movable/screen/alert/status_effect/ward_mind + duration = STATUS_EFFECT_PERMANENT + tick_interval = 1 SECONDS + processing_speed = STATUS_EFFECT_FAST_PROCESS + + // Stress per blocked antimagic charge. + var/stress_per_charge = PSYKER_STRESS_MODERATE + // Per-second upkeep while active. + var/stress_per_second = PSYKER_STRESS_TRIVIAL * 1.5 + + var/datum/action/cooldown/power/psyker/ward_mind/source_action + +/atom/movable/screen/alert/status_effect/ward_mind + name = "Ward Mind" + desc = "You are immune to resonance-based detection and mental effects; but you passively generate stress." + icon = 'icons/mob/actions/actions_elites.dmi' + icon_state = "magic_box" + + +/datum/status_effect/power/ward_mind/on_creation(mob/living/new_owner, datum/action/cooldown/power/psyker/ward_mind/passed_action) + . = ..() + source_action = passed_action + +/datum/status_effect/power/ward_mind/on_apply() + if(!owner) + return FALSE + ADD_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, REF(src)) + RegisterSignal(owner, COMSIG_MOB_RECEIVE_MAGIC, PROC_REF(on_receive_magic), override = TRUE) + RegisterSignal(owner, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + if(source_action) + source_action.active = TRUE + source_action.active_effect = src + source_action.build_all_button_icons(UPDATE_BUTTON_STATUS) + return TRUE + +/datum/status_effect/power/ward_mind/on_remove() + if(owner) + UnregisterSignal(owner, COMSIG_MOB_RECEIVE_MAGIC) + UnregisterSignal(owner, COMSIG_ATOM_DISPEL) + REMOVE_TRAIT(owner, TRAIT_ANTIRESONANCE_SCRYING, REF(src)) + if(source_action) + source_action.active = FALSE + source_action.active_effect = null + source_action.build_all_button_icons(UPDATE_BUTTON_STATUS) + return + +/datum/status_effect/power/ward_mind/proc/on_receive_magic(mob/living/carbon/source, casted_magic_flags, charge_cost, list/antimagic_sources) + SIGNAL_HANDLER + if(!(casted_magic_flags & MAGIC_RESISTANCE_MIND)) + return NONE + var/obj/item/organ/resonant/psyker/psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) + if(!psyker_organ) + return NONE + adjust_stress_from_block(psyker_organ, charge_cost) + if(psyker_organ.stress >= psyker_organ.stress_threshold) + return NONE + antimagic_sources += owner + return COMPONENT_MAGIC_BLOCKED + +/datum/status_effect/power/ward_mind/proc/adjust_stress_from_block(obj/item/organ/resonant/psyker/psyker_organ, charge_cost) + if(!isnum(charge_cost) || charge_cost <= 0) + return + psyker_organ.modify_stress(charge_cost * stress_per_charge) + +/datum/status_effect/power/ward_mind/proc/on_dispel(mob/owner, atom/dispeller) + SIGNAL_HANDLER + qdel(src) + return DISPEL_RESULT_DISPELLED + +/datum/status_effect/power/ward_mind/tick(seconds_between_ticks) + if(!source_action || QDELETED(source_action)) + qdel(src) + return + source_action.modify_stress(stress_per_second * seconds_between_ticks) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_action.dm index 30f54a093b855f..e8a3cccb37bf80 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 @@ -67,12 +67,11 @@ for(var/obj/item/equipped_item as anything in equipped_items) if(!equipped_item) continue - if(!istype(equipped_item, /obj/item/clothing)) + if(!istype(equipped_item, /obj/item/clothing) || equipped_item.affinity_worn_override) continue - var/obj/item/clothing/equipped_clothing = equipped_item - if(equipped_clothing.affinity > highest_affinity) - highest_affinity = equipped_clothing.affinity + if(equipped_item.affinity > highest_affinity) + highest_affinity = equipped_item.affinity // Checks if you're holding items with affinity. for(var/obj/item/held_item as anything in user.held_items) 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 7670e1f2f2842c..9b367e4643c96c 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 @@ -2,14 +2,238 @@ We have to apply this retroactively to existing items, which is what this file is for. If you make something new, include it as a var instead. */ -/* Rule of thumb on what belongs in what category: -Tier 1 Affinity are things vaguely magical. A jester's hat, a cape, in essence any form of slightly mystical drip falls in this category. This is all you need to get access to most non-flashy, non-combat spells. -Tier 2 Affinity are things that are usually more obvious but not cumbersome. This category is primarily populated by any Spell Focus item that can fit in a backpack (pondering your orbs and the likes), or clearly magical items that aren't pronounced on the sprite (such as amulets). -Tier 3 Affinity is where you really start dressing magically. The job-specific thaumaturge robes, for example Security's, have this tier of affinity. Usually these robes provide some degree of utility, such as armor. This includes body and head slots. Spell Focuses, such as wands, that can't fit in the pocket but can fit in the belt/suit slot also go here. Most spells will cap out at this requirement. -Tier 4 Affinity is basically the full wizard dress. Any wizard robes without significant side-effects, such as the costume wizard robes, satisfy this condition. -Tier 5 Affinity is specially reserved for Spell Focus staves, which can only be worn on the back or held in hand. In turn, holding it gets you great power. Just make sure not to lose it. -Whilst Tier 5 is the cap for normal player content, Antagonist and other rare equipment can exceed these affinity tiers. Steal an actual Wizard's hat and you may just get your hands on a Tier 7 item. +/* +A lot of Affinity asignments are vibe-based depending on looks, visibility and rarirty, but the rule of thumb I tend to use is; +- T1: If it goes in a weird slot (neck, mask, undersuit, shoes, gloves) OR if it has some mystical qualities (e.g bedsheet as a cape) and isn't traditionally part of a classical caster archetype (Bard, Druid, Cleric, Wizard) it goes here. +- T2: Magical headwear with bonus stats (armor). Handheld affinity items that fit in pockets+. T1 equipment that covers a lot of the sprite (capes, full-head masks, etc.) +- T3: Magical headwear with NO bonus stats, Magical Bodywear with bonus stats. Rare magic-looking items in weird slots. Handheld affinity items that don't fit in pockets but do fit in the backpack. +- T4: Magical bodywear with NO bonus stats. Handheld affinity items that don't fit in pocket or backpack but allow suit slots/belt slot. +- T5: Handheld affinity items that don't fit anywhere (but the backpack). Most antag robes. +- T6+: Go with your gut based on rarity, looks and antag. Wiz robes are usually T7. */ -/obj/item/clothing/suit/wizrobe +/* + Tier 1 +*/ + // its not as pointy but itlll do +/obj/item/clothing/head/costume/paper_hat + affinity = 1 + +// the various neck-slot capes that cover too little of the sprite to pass. or are the poncho; because I cant recall a single magic-man wearing a poncho. +/obj/item/clothing/neck/face_scarf + affinity = 1 +/obj/item/clothing/neck/mantle + affinity = 1 +/obj/item/clothing/neck/doppler_mantle + affinity = 1 +/obj/item/clothing/neck/basic_poncho + affinity = 1 +/obj/item/clothing/neck/ranger_poncho + affinity = 1 +/obj/item/clothing/neck/patterned_poncho + affinity = 1 + +// we all loved to larp with bedsheet capes when we were younger +/obj/item/bedsheet + affinity = 1 + affinity_worn_override = TRUE + +// there's an argument to be made for plague doctors being mystical. +/obj/item/clothing/mask/gas/plaguedoctor + affinity = 1 + +// Animal masks are like a classic ritual in a lot of folklore so I am giving some leeway here. Small are T1, big are T2. +/obj/item/clothing/mask/animal/small + affinity = 1 + +// There's enough anime of magic maids to justify this. +/obj/item/clothing/under/rank/civilian/janitor/maid + affinity = 1 +/obj/item/clothing/under/costume/maid + affinity = 1 +/obj/item/clothing/accessory/maidapron + affinity = 1 + +/* + Tier 2: +*/ +// Capes are about as caster as it gets and cover enough of the sprite to justify t2. +/obj/item/clothing/neck/wide_cape + affinity = 2 +/obj/item/clothing/neck/robe_cape + affinity = 2 +/obj/item/clothing/neck/long_cape + affinity = 2 + + +/obj/item/staff // the base item is small + affinity = 2 + +// Animal masks that arent small +/obj/item/clothing/mask/animal + affinity = 2 +/* + Tier 3: +*/ +// Jester hat +/obj/item/clothing/head/costume/jester + affinity = 3 + +// Clown Mitre +/obj/item/clothing/head/chaplain/clownmitre + affinity = 4 + +// Nun hood +/obj/item/clothing/head/chaplain/habit_veil + affinity = 3 + +// Shrine maiden wig +/obj/item/clothing/head/costume/shrine_wig + affinity = 3 + +// Gohei; this is apparently the asian equivelant of a staff. Regardless they lose points cause they fit in the backpack. +/obj/item/gohei + affinity = 3 + +// Narsie cult looks sufficiently magical, but they don't get the antag pass because you can get these from Lavaland and are already very robust. +/obj/item/clothing/suit/hooded/cultrobes + affinity = 3 + +// Rare enough cloak-slot dropped by Lavaland Elites. +/obj/item/clothing/neck/cloak/herald_cloak + affinity = 3 + +// Heretic focues arent too pronounced but theyre antag items so they get preferential treatment +/obj/item/clothing/neck/heretic_focus + affinity = 3 + +// You can teleport with it but given its megafauna loot we can be a bit lax +/obj/item/hierophant_club + affinity = 3 + +/* + Tier 4 +*/ +// Fits the criteria for wands but since its lavaland loot it gets a +1 +/obj/item/lava_staff + affinity = 4 + +// Carp suit (magicarp) +/obj/item/clothing/suit/hooded/carp_costume + affinity = 4 + +// Cueball hat. It sparks and makes your head a big white orb. +/obj/item/clothing/head/costume/cueball + affinity = 4 + +// Owl Wings (pretty druidy) +/obj/item/clothing/suit/toggle/owlwings // (includes griffon wings + affinity = 4 + +// Dracula is pretty wizardy +/obj/item/clothing/suit/costume/dracula + affinity = 4 + +// Costumes that are basically wizard drip. +/obj/item/clothing/suit/costume/imperium_monk + affinity = 4 +/obj/item/clothing/suit/hooded/mysticrobe + affinity = 4 + +// Cleric/priest robes with no defenses. +/obj/item/clothing/suit/chaplainsuit/whiterobe + affinity = 4 +/obj/item/clothing/suit/chaplainsuit/habit + affinity = 4 +/obj/item/clothing/suit/chaplainsuit/clownpriest + affinity = 4 + +// I dont know what a touhou or a shrine maiden is but its magical apparently. +/obj/item/clothing/suit/costume/shrine_maiden + affinity = 4 + +// Banshees are valid. +/obj/item/clothing/suit/costume/whitedress + affinity = 4 +/obj/item/clothing/under/dress/wedding_dress + affinity = 4 + +// We should add frost powers tbh. +/obj/item/clothing/suit/costume/drfreeze_coat + affinity = 4 + +// Heretic void cloak. The hood actually gives T5 to reflect you can cast only with the hood up. +/obj/item/clothing/suit/hooded/cultrobes/void + affinity = 4 + +// The heretic book. Bonus points for being antag and heretic spell focus. +/obj/item/codex_cicatrix + affinity = 4 + +/* + Tier 4: Wizrobes specifically. +*/ + +// Wizrobes (Fakes) +/obj/item/clothing/suit/wizrobe/fake + affinity = 4 + +/obj/item/clothing/suit/wizrobe/marisa/fake + affinity = 4 + +/obj/item/clothing/suit/wizrobe/tape/fake + affinity = 4 + +// Wizrobe hats (Fakes) +/obj/item/clothing/head/wizard/fake affinity = 4 + +/obj/item/clothing/head/costume/witchwig + +/obj/item/clothing/head/collectable/wizard + affinity = 4 + +/obj/item/clothing/head/wizard/marisa/fake + affinity = 4 + +/obj/item/clothing/head/wizard/tape/fake + affinity = 4 + +/obj/item/clothing/head/wizard/chanterelle + affinity = 4 + +/* + Tier 5+ +*/ + +// Chaplain nullrod staves +/obj/item/nullrod/staff + affinity = 5 + +// Haunted blade, the heretic sword that gives you cool shit. +/obj/item/melee/cultblade/haunted + affinity = 5 + +// Heretic cloak with hood up: This is invisible, but also heretics can cast spells with it so this is fine. +/obj/item/clothing/head/hooded/cult_hoodie/void + affinity = 5 + +// Eldrich heretic robes with hood up. +/obj/item/clothing/head/hooded/cult_hoodie/eldritch + affinity = 5 + +// Slightly better staves, as a treat. +/obj/item/storm_staff + affinity = 6 +/obj/item/rod_of_asclepius + affinity = 6 + +// Real Wizrobes (antag only) +/obj/item/clothing/head/wizard + affinity = 7 + +/obj/item/clothing/suit/wizrobe + affinity = 7 + +/obj/item/clothing/suit/wizrobe/paper // this ones a bit special since its space loot but rare space loot. + affinity = 6 diff --git a/tgstation.dme b/tgstation.dme index c3d53cd709f005..4d2beb4574cc52 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7523,6 +7523,7 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\premonition.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\scrying.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\ward_mind.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\_psyker_event.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\brain_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\cardiac_arrest.dm" From 8db3b7236236db8846ce714b9cb5d114fd330a10 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 17 Mar 2026 19:13:58 +0100 Subject: [PATCH 144/212] Rehashes dispel to be better (thanks Ephe), adds reality anchors, fixes manipulate not working on various strange computers (modular computers still dont work but fuck it I don't care). --- .../powers/resonant/psyker/_psyker_organ.dm | 2 +- .../code/powers/resonant/psyker/levitate.dm | 2 +- .../code/powers/resonant/psyker/manipulate.dm | 49 +++++++- .../code/powers/resonant/silence_trauma.dm | 2 +- .../powers/sorcerous/theologist/purify.dm | 2 +- .../modular_powers/code/powers_antimagic.dm | 48 ++++---- .../modular_powers/code/reality_anchor.dm | 111 ++++++++++++++++++ .../code/security/resonant_cuffs.dm | 2 +- tgstation.dme | 1 + 9 files changed, 184 insertions(+), 35 deletions(-) create mode 100644 modular_doppler/modular_powers/code/reality_anchor.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index 2d5ce47a2e460d..15eaf9deda9770 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -57,7 +57,7 @@ // Check if we do stress backlash after stress reduction. if(stress >= (stress_threshold * 2)) // Catastrophic event. stress_backlash(PSYKER_EVENT_TIER_CATASTROPHIC) - dispel(owner, src) // ends most effects + owner.dispel(src) // ends most effects stress = 0 // No CD, just a hard reset and the consequences of your actions. CDstressMild = 0 CDstressSevere = 0 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 01cc349a43be69..84e7b237a8db2d 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -53,7 +53,7 @@ return //Faceplant if you get KO'd if(HAS_TRAIT(owner, TRAIT_INCAPACITATED)) - dispel() + on_dispel(owner, src) // Passive stress cost if(active) var/mob/living/carbon/human/psyker = owner 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 551c851f24b56f..95b4692fc38a1c 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/manipulate.dm @@ -9,13 +9,26 @@ /datum/power/psyker_power/manipulate name = "Manipulate" - desc = "Allows you to interact with machinery and various other structures within line of sight as if it were next to you." + desc = "Allows you to interact with machinery and various other structures within line of sight as if it were next to you. Having UIs open from a distance using this power causes stress build-up." security_record_text = "Subject can psychically interact with objects from a distance." security_threat = POWER_THREAT_MAJOR value = 2 + power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES action_path = /datum/action/cooldown/power/psyker/manipulate - mob_trait = TRAIT_NO_UI_DISTANCE - required_powers = list(/datum/power/psyker_power/telekinesis) //given this lets you grab items from certain things from a distance this is basically a fluff requirement to explain why you can grab objects from a distance. + required_powers = list(/datum/power/psyker_power/telekinesis) //given this lets you grab items from a distance this is basically a fluff requirement to explain why you can grab objects from a distance. + +// Normally the golden rule is to let your action handle everything in powers; but in this case we need to actually make it so that we only have TRAIT_NO_UI_DISTANCE while we have a TK'd interface. +/datum/power/psyker_power/manipulate/process(seconds_per_tick) + if(!power_holder) + return + var/datum/action/cooldown/power/psyker/manipulate/manipulate_action = action_path + var/ui_count = manipulate_action ? length(manipulate_action.ui_filters) : 0 + if(ui_count) + ADD_TRAIT(power_holder, TRAIT_NO_UI_DISTANCE, src) + manipulate_action.modify_stress((PSYKER_STRESS_TRIVIAL / 2) * seconds_per_tick * ui_count) // ticks up 0.5 stress per second per ui open. + else + REMOVE_TRAIT(power_holder, TRAIT_NO_UI_DISTANCE, src) + /datum/action/cooldown/power/psyker/manipulate name = "Manipulate" @@ -64,25 +77,36 @@ target.attack_hand(user) // interact with UI if present and not blacklisted. - if((target.interaction_flags_atom & INTERACT_ATOM_UI_INTERACT) && !is_type_in_typecache(target, ui_blacklist)) + var/allow_ui_interact = (target.interaction_flags_atom & INTERACT_ATOM_UI_INTERACT) && !is_type_in_typecache(target, ui_blacklist) + if(allow_ui_interact) + ADD_TRAIT(user, TRAIT_NO_UI_DISTANCE, origin_power) // we give it early so that the we count as being 'valid' before we reach the process. target.ui_interact(user) // We save the ui so we can add a filter to show it is being interacted with. var/datum/tgui/ui = SStgui.get_open_ui(user, target) + // Some UIs (usually older computers) have different UI logic; in this case we fallback to looking at all the open UIs and trying to find it by comparing the source object. + if(!ui) + for(var/datum/tgui/candidate in user.tgui_open_uis) + if(!candidate?.src_object) + continue + if(candidate.src_object == target || candidate.src_object.ui_host(user) == target) + ui = candidate + break if(ui) var/filter_id = "manipulate_glow" - target.add_filter(filter_id, 1, list(type = "outline", color = "#ff66cc", size = 2)) + target.add_filter(filter_id, 1, list(type = "outline", color = "#ff66cc", size = 2)) var/filter = target.get_filter(filter_id) if(filter) animate(filter, alpha = 110, time = 1.5 SECONDS, loop = -1) animate(alpha = 40, time = 2.5 SECONDS) ui_filters[ui] = list(target, filter_id) + RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) RegisterSignal(ui, COMSIG_QDELETING, PROC_REF(on_ui_closed)) REMOVE_TRAIT(user, TRAIT_REMOTE_INTERACT, src) right_click = FALSE - modify_stress(PSYKER_STRESS_TRIVIAL) + modify_stress(PSYKER_STRESS_TRIVIAL * 2) return TRUE /datum/action/cooldown/power/psyker/manipulate/proc/on_ui_closed(datum/tgui/ui) @@ -93,3 +117,16 @@ var/filter_id = entry[2] target?.remove_filter(filter_id) ui_filters -= ui + UnregisterSignal(target, COMSIG_ATOM_DISPEL) + +// Closes any open UIs on a manipulated object. +/datum/action/cooldown/power/psyker/manipulate/proc/on_dispel(atom/source, atom/dispeller) + SIGNAL_HANDLER + var/list/uis_to_close = list() + for(var/datum/tgui/ui as anything in ui_filters) + var/list/entry = ui_filters[ui] + if(entry && entry[1] == source) + uis_to_close += ui + + for(var/datum/tgui/ui as anything in uis_to_close) + ui?.close() diff --git a/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm index cd2284ea0af406..1499dba5e1014c 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/silence_trauma.dm @@ -6,7 +6,7 @@ lose_text = span_notice("You begin to feel your Resonant Powers returning.") /datum/brain_trauma/magic/resonance_silenced/on_gain() - dispel(owner, src) + owner.dispel(src) ADD_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAUMA_TRAIT) . = ..() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm index d3b862c22f8c0e..8ea3a893325869 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm @@ -44,7 +44,7 @@ var/success = FALSE // General dispel on target - if(dispel(target, user)) + if(target.dispel(user)) success = TRUE // Remove poison from a creature's bloodstream or an object's reagents. diff --git a/modular_doppler/modular_powers/code/powers_antimagic.dm b/modular_doppler/modular_powers/code/powers_antimagic.dm index b3709ec8a526e6..d11142eab08940 100644 --- a/modular_doppler/modular_powers/code/powers_antimagic.dm +++ b/modular_doppler/modular_powers/code/powers_antimagic.dm @@ -46,29 +46,29 @@ Dispel proc handler */ -/proc/dispel(atom/target, atom/dispeller, dispel_flags = 0) - if(!target) - return FALSE - - var/signal_result = SEND_SIGNAL(target, COMSIG_ATOM_DISPEL, dispeller) - var/was_dispersed = (signal_result & DISPEL_RESULT_DISPELLED) - - // Only cascade if explicitly requested AND target is a mob - if((dispel_flags & DISPEL_CASCADE_CARRIED) && ismob(target)) - var/mob/living/target_mob = target - - for(var/obj/item/held_item in target_mob.held_items) - if(dispel(held_item, dispeller)) - was_dispersed = TRUE - - for(var/obj/item/worn_item in target_mob.get_equipped_items()) - if(dispel(worn_item, dispeller)) - was_dispersed = TRUE - +/atom/proc/dispel(atom/dispeller, dispel_flags = 0) + var/signal_result = handle_dispel(dispeller, dispel_flags) // SFX that a dispel occurred. - if(was_dispersed) - playsound(target, 'sound/effects/magic/smoke.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) - return was_dispersed + if(signal_result) + playsound(src, 'sound/effects/magic/smoke.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + return signal_result + +/atom/proc/handle_dispel(atom/dispeller, dispel_flags = 0) + var/signal_result = SEND_SIGNAL(src, COMSIG_ATOM_DISPEL, dispeller) + return signal_result + +/mob/living/handle_dispel(atom/dispeller, dispel_flags = 0) + var/signal_result = SEND_SIGNAL(src, COMSIG_ATOM_DISPEL, dispeller) + // Only cascade if explicitly requested. + if(dispel_flags & DISPEL_CASCADE_CARRIED) + for(var/obj/item/held_item in held_items) + if(held_item.dispel(dispeller)) + signal_result = TRUE + + for(var/obj/item/worn_item in get_equipped_items()) + if(worn_item.dispel(dispeller)) + signal_result = TRUE + return signal_result /* Adds dispel on hit for the null rod. @@ -101,7 +101,7 @@ Dispel proc handler /datum/element/resonant_dispel_hit/proc/dispel_on_hit(datum/source, atom/attacker, atom/damage_target, hit_zone, throw_hit) SIGNAL_HANDLER - dispel(damage_target, attacker, cascade_dispels ? DISPEL_CASCADE_CARRIED : null) + damage_target.dispel(attacker, cascade_dispels ? DISPEL_CASCADE_CARRIED : null) /* Very simple wiz spell to test dispel functionality, plus for admeme purposes. @@ -131,5 +131,5 @@ Dispel proc handler to_chat(owner, span_warning("Your dispel failed to work!")) return FALSE - dispel(cast_on, owner, DISPEL_CASCADE_CARRIED) + cast_on.dispel(owner, DISPEL_CASCADE_CARRIED) return TRUE diff --git a/modular_doppler/modular_powers/code/reality_anchor.dm b/modular_doppler/modular_powers/code/reality_anchor.dm new file mode 100644 index 00000000000000..5ca35319c342ff --- /dev/null +++ b/modular_doppler/modular_powers/code/reality_anchor.dm @@ -0,0 +1,111 @@ +/obj/structure/reality_anchor + name = "reassembled reality anchor" + desc = "The fragments of a broken down reality anchor, reassembled. Crude machinery is managing to keep it docile; but when enabled, it will start enforcing normality back in a large area around it." + icon = 'icons/obj/antags/cult/structures.dmi' + icon_state = "pylon_off" + anchored = TRUE + density = TRUE + + // Is it on/off + var/active = FALSE + + // Pulse interval + var/pulse_interval = 6 SECONDS + var/next_pulse_time = 0 + + // Range in turfs + var/pulse_range = 6 + + // on and off icon states. + var/on_icon_state = "pylon" + var/off_icon_state = "pylon_off" + +/obj/structure/reality_anchor/Destroy() + STOP_PROCESSING(SSobj, src) + . = ..() + +// Turns the thing on or off after the do_after. +/obj/structure/reality_anchor/attack_hand(mob/user, list/modifiers) + . = ..() + if(.) + return + var/action_word = active ? "deactivate" : "activate" + user.visible_message( + span_warning("[user] begins to [action_word] the reality anchor..."), + span_warning("You begin to [action_word] the reality anchor...") + ) + if(!do_after(user, 3 SECONDS, target = src)) + return + user.visible_message( + span_warning("[user] finishes [action_word]ing the reality anchor."), + span_warning("You finish [action_word]ing the reality anchor.") + ) + toggle_anchor(user) + +// Switches it on or off. +/obj/structure/reality_anchor/proc/toggle_anchor(mob/user) + active = !active + if(active) + icon_state = on_icon_state + pulse() + next_pulse_time = world.time + pulse_interval + START_PROCESSING(SSobj, src) + return + + icon_state = off_icon_state + STOP_PROCESSING(SSobj, src) + +// Countdown til dispel pulse. +/obj/structure/reality_anchor/process(seconds_per_tick) + if(!active) + return + if(world.time < next_pulse_time) + return + pulse() + next_pulse_time = world.time + pulse_interval + +// Dispel pulse. +/obj/structure/reality_anchor/proc/pulse() + var/turf/center = get_turf(src) + if(!center) + return + var/obj/effect/temp_visual/circle_wave/reality_anchor/pulse_fx = new(center) + pulse_fx.amount_to_scale = pulse_range + 1 // falls short without the +1 + // We get EVERYTHING in range and dispel it. This shouldn't be too much of a lag-machine (hopefully) + for(var/atom/movable/target in range(pulse_range, center)) + if(ismob(target)) + var/mob/living/living_target = target + living_target.dispel(src, DISPEL_CASCADE_CARRIED) + living_target.apply_status_effect(/datum/status_effect/power/reality_anchor_silenced) + else if(isobj(target)) + target.dispel(src) + +// Status effect responsible for silencing. +/datum/status_effect/power/reality_anchor_silenced + id = "reality_anchor_silenced" + status_type = STATUS_EFFECT_REFRESH + alert_type = /atom/movable/screen/alert/status_effect/reality_anchor_silenced + show_duration = TRUE + duration = 10 SECONDS + tick_interval = STATUS_EFFECT_NO_TICK + +/datum/status_effect/power/reality_anchor_silenced/on_apply() + ADD_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAIT_STATUS_EFFECT(id)) + return TRUE + +/datum/status_effect/power/reality_anchor_silenced/on_remove() + REMOVE_TRAIT(owner, TRAIT_RESONANCE_SILENCED, TRAIT_STATUS_EFFECT(id)) + return + +/atom/movable/screen/alert/status_effect/reality_anchor_silenced + name = "Silenced" + desc = "Resonant powers are periodically dispelled and supressed around the reality anchor!" + icon = 'icons/obj/antags/cult/structures.dmi' + icon_state = "pylon" + +// The effect from reality anchors +/obj/effect/temp_visual/circle_wave/reality_anchor + color = COLOR_SILVER + max_alpha = 20 + duration = 0.5 SECONDS + amount_to_scale = 6 diff --git a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm index 888562147f84a2..f9a48e4da69dc8 100644 --- a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm +++ b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm @@ -16,7 +16,7 @@ . = ..() if(slot == ITEM_SLOT_HANDCUFFED) to_chat(user, span_warning("A shudder goes down your spine; [name] seem to suppress resonant powers!")) - dispel(user, src) + user.dispel(src) ADD_TRAIT(user, TRAIT_RESONANCE_SILENCED, src) RegisterSignal(src, COMSIG_ITEM_PRE_UNEQUIP, PROC_REF(on_uncuff)) // Surely there is an unequip proc I am just missing? diff --git a/tgstation.dme b/tgstation.dme index 4d2beb4574cc52..c826858025271f 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7430,6 +7430,7 @@ #include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm" #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" +#include "modular_doppler\modular_powers\code\reality_anchor.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" From 377f666f6d2a007180d87171c95f4b0a7b4e281c Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 17 Mar 2026 19:40:14 +0100 Subject: [PATCH 145/212] Fixed some bugs with resonant cuffs --- .../code/{ => security}/reality_anchor.dm | 4 +-- .../code/security/resonant_cuffs.dm | 29 ++++++++++++------- .../code/tg_vendors/sectech.dm | 1 + tgstation.dme | 2 +- 4 files changed, 23 insertions(+), 13 deletions(-) rename modular_doppler/modular_powers/code/{ => security}/reality_anchor.dm (98%) diff --git a/modular_doppler/modular_powers/code/reality_anchor.dm b/modular_doppler/modular_powers/code/security/reality_anchor.dm similarity index 98% rename from modular_doppler/modular_powers/code/reality_anchor.dm rename to modular_doppler/modular_powers/code/security/reality_anchor.dm index 5ca35319c342ff..62c51da8313985 100644 --- a/modular_doppler/modular_powers/code/reality_anchor.dm +++ b/modular_doppler/modular_powers/code/security/reality_anchor.dm @@ -3,7 +3,6 @@ desc = "The fragments of a broken down reality anchor, reassembled. Crude machinery is managing to keep it docile; but when enabled, it will start enforcing normality back in a large area around it." icon = 'icons/obj/antags/cult/structures.dmi' icon_state = "pylon_off" - anchored = TRUE density = TRUE // Is it on/off @@ -46,12 +45,13 @@ /obj/structure/reality_anchor/proc/toggle_anchor(mob/user) active = !active if(active) + anchored = TRUE icon_state = on_icon_state pulse() next_pulse_time = world.time + pulse_interval START_PROCESSING(SSobj, src) return - + anchored = FALSE icon_state = off_icon_state STOP_PROCESSING(SSobj, src) diff --git a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm index f9a48e4da69dc8..ee756c927a3df7 100644 --- a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm +++ b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm @@ -1,13 +1,16 @@ // Antiresonant cuffs. They're like normal cuffs but slightly worse and put a dampener on resonant folk. /obj/item/restraints/handcuffs/antiresonant name = "resonant suppressant handcuffs" - desc = "Handcuffs laced with leaded brass on the interior, with a plentitude of runes and a bit of circuitry sticking out. Capable of suppressing resonant powers. How R&D came up with this one is a miracle in itself." + desc = "Handcuffs laced with a smooth, dark material similar to vulcanice, harvested from a reality anchor. Capable of suppressing resonant powers." icon_state = "handcuffAlien" color = "#ee3d3d" // til we get a proper sprite for these things. breakouttime = 50 SECONDS handcuff_time = 4.5 SECONDS custom_price = PAYCHECK_COMMAND * 0.6 + // we save the mob so we don't end up orphaning the silence remover + var/mob/living/cuffed_mob + /obj/item/restraints/handcuffs/antiresonant/attempt_to_cuff(mob/living/carbon/victim, mob/living/user) . = ..() playsound(victim, 'sound/effects/magic/magic_block.ogg', 75, TRUE, -2) @@ -18,14 +21,20 @@ to_chat(user, span_warning("A shudder goes down your spine; [name] seem to suppress resonant powers!")) user.dispel(src) ADD_TRAIT(user, TRAIT_RESONANCE_SILENCED, src) - RegisterSignal(src, COMSIG_ITEM_PRE_UNEQUIP, PROC_REF(on_uncuff)) // Surely there is an unequip proc I am just missing? + cuffed_mob = user + RegisterSignal(src, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(on_uncuff)) // why do we just not have an uncuff proc for cuffs I dont understand + +/obj/item/restraints/handcuffs/antiresonant/proc/on_uncuff(datum/source, force, atom/newloc, no_move, invdrop, silent) + SIGNAL_HANDLER + if(cuffed_mob) + REMOVE_TRAIT(cuffed_mob, TRAIT_RESONANCE_SILENCED, src) + cuffed_mob = null + UnregisterSignal(src, COMSIG_ITEM_POST_UNEQUIP) -/obj/item/restraints/handcuffs/antiresonant/proc/on_uncuff(datum/source) - REMOVE_TRAIT(usr, TRAIT_RESONANCE_SILENCED, src) - UnregisterSignal(src, COMSIG_ITEM_PRE_UNEQUIP) +/obj/item/restraints/handcuffs/antiresonant/Destroy(force) + if(cuffed_mob) + REMOVE_TRAIT(cuffed_mob, TRAIT_RESONANCE_SILENCED, src) + cuffed_mob = null + return ..() -// Adds the antiresonant cuffs to the sec vend. -/obj/machinery/vending/security - products_doppler = list( - /obj/item/restraints/handcuffs/antiresonant = 6, - ) +// Vendor entry lives in modular_vending/code/tg_vendors/sectech.dm diff --git a/modular_doppler/modular_vending/code/tg_vendors/sectech.dm b/modular_doppler/modular_vending/code/tg_vendors/sectech.dm index 183b21a32afb62..80180368c068e3 100644 --- a/modular_doppler/modular_vending/code/tg_vendors/sectech.dm +++ b/modular_doppler/modular_vending/code/tg_vendors/sectech.dm @@ -16,6 +16,7 @@ /obj/item/food/donut/jelly/berry = 1, /obj/item/food/donut/jelly/apple = 1, /obj/item/food/donut/jelly/choco = 1, + /obj/item/restraints/handcuffs/antiresonant = 6, ) premium_doppler = list( /obj/item/gun/ballistic/automatic/schiebenmaschine = 30, diff --git a/tgstation.dme b/tgstation.dme index c826858025271f..83aead2f53f8a9 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7430,7 +7430,6 @@ #include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm" #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" -#include "modular_doppler\modular_powers\code\reality_anchor.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" @@ -7571,6 +7570,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\purify.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\theologist\smiting_strike_upgrades.dm" +#include "modular_doppler\modular_powers\code\security\reality_anchor.dm" #include "modular_doppler\modular_powers\code\security\resonant_cuffs.dm" #include "modular_doppler\modular_quirks\atypical_tastes\atypical_tastes.dm" #include "modular_doppler\modular_quirks\atypical_tastes\code\preferences.dm" From d7824564a9e5baf9442cc1025c04ca5b5513be9d Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 18 Mar 2026 11:19:43 +0100 Subject: [PATCH 146/212] Added the abilit to apply powers in player panel. Fixed requires_any not working on powers, fixes power_migration orphaning the old powers save_data. Resonant Cuffs Flavor. --- code/modules/admin/topic.dm | 1 + code/modules/admin/verbs/admingame.dm | 2 +- .../code/powers_migration.dm | 6 ++-- .../affinity/thaumaturge_affinity.dm | 23 ++++++------- .../modular_powers/code/powers_subsystem.dm | 32 ++++++++++++++++++- .../code/security/resonant_cuffs.dm | 2 +- 6 files changed, 50 insertions(+), 16 deletions(-) diff --git a/code/modules/admin/topic.dm b/code/modules/admin/topic.dm index 054a507b6a2e46..e763b827f56894 100644 --- a/code/modules/admin/topic.dm +++ b/code/modules/admin/topic.dm @@ -421,6 +421,7 @@ if(!target.client) to_chat(usr, "[target] has no client!", confidential = TRUE) return + SSpowers.assign_powers(target, target.client) // DOPPLER ADDITION - Rolls adding powers into game panel. SSquirks.AssignQuirks(target, target.client) log_admin("[key_name(usr)] applied client quirks to [key_name(target)].") message_admins(span_adminnotice("[key_name_admin(usr)] applied client quirks to [key_name_admin(target)].")) diff --git a/code/modules/admin/verbs/admingame.dm b/code/modules/admin/verbs/admingame.dm index c5f06d8a5d475d..641aecf3ac8084 100644 --- a/code/modules/admin/verbs/admingame.dm +++ b/code/modules/admin/verbs/admingame.dm @@ -141,7 +141,7 @@ ADMIN_VERB_ONLY_CONTEXT_MENU(show_player_panel, R_ADMIN, "Show Player Panel", mo body += "
" if(!isnewplayer(player)) body += "Forcesay | " - body += "Apply Client Quirks | " + body += "Apply Client Quirks & Powers | " // DOPPLER EDIT: Rolls powers into apply client quirks. body += "Thunderdome 1 | " body += "Thunderdome 2 | " body += "Thunderdome Admin | " diff --git a/modular_doppler/_savefile_migration/code/powers_migration.dm b/modular_doppler/_savefile_migration/code/powers_migration.dm index b308ca5d5d7dec..5860a1bd2ccb64 100644 --- a/modular_doppler/_savefile_migration/code/powers_migration.dm +++ b/modular_doppler/_savefile_migration/code/powers_migration.dm @@ -3,5 +3,7 @@ * Removes the old powers from people's savefiles */ /datum/preferences/proc/nuke_old_powers(list/save_data) - save_data?["powers"] = list() - log_game("The nuke was dropped, yay") + if(save_data && ("powers" in save_data)) + save_data -= "powers" + var/ckey_to_log = parent?.ckey || "unknown" + log_game("[ckey_to_log]'s powers were migrated over from the old powers system.") diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_affinity.dm index 9b367e4643c96c..292bb1f416334f 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 @@ -4,7 +4,7 @@ We have to apply this retroactively to existing items, which is what this file i /* A lot of Affinity asignments are vibe-based depending on looks, visibility and rarirty, but the rule of thumb I tend to use is; -- T1: If it goes in a weird slot (neck, mask, undersuit, shoes, gloves) OR if it has some mystical qualities (e.g bedsheet as a cape) and isn't traditionally part of a classical caster archetype (Bard, Druid, Cleric, Wizard) it goes here. +- T1: If it goes in a weird slot (neck, mask, undersuit, shoes, gloves) OR if it has some association with 'magic' or pretending to be magical (e.g short capes) and isn't traditionally part of a classical caster archetype (Bard, Druid, Cleric, Wizard) it goes here. - T2: Magical headwear with bonus stats (armor). Handheld affinity items that fit in pockets+. T1 equipment that covers a lot of the sprite (capes, full-head masks, etc.) - T3: Magical headwear with NO bonus stats, Magical Bodywear with bonus stats. Rare magic-looking items in weird slots. Handheld affinity items that don't fit in pockets but do fit in the backpack. - T4: Magical bodywear with NO bonus stats. Handheld affinity items that don't fit in pocket or backpack but allow suit slots/belt slot. @@ -170,6 +170,10 @@ A lot of Affinity asignments are vibe-based depending on looks, visibility and r /obj/item/codex_cicatrix affinity = 4 +// Did you know the perceptomatrix lets you cast spells? +/obj/item/clothing/head/helmet/perceptomatrix + affinity = 4 + /* Tier 4: Wizrobes specifically. */ @@ -177,28 +181,22 @@ A lot of Affinity asignments are vibe-based depending on looks, visibility and r // Wizrobes (Fakes) /obj/item/clothing/suit/wizrobe/fake affinity = 4 - /obj/item/clothing/suit/wizrobe/marisa/fake affinity = 4 - /obj/item/clothing/suit/wizrobe/tape/fake affinity = 4 // Wizrobe hats (Fakes) /obj/item/clothing/head/wizard/fake affinity = 4 - /obj/item/clothing/head/costume/witchwig - + affinity = 4 /obj/item/clothing/head/collectable/wizard affinity = 4 - /obj/item/clothing/head/wizard/marisa/fake affinity = 4 - /obj/item/clothing/head/wizard/tape/fake affinity = 4 - /obj/item/clothing/head/wizard/chanterelle affinity = 4 @@ -230,10 +228,13 @@ A lot of Affinity asignments are vibe-based depending on looks, visibility and r // Real Wizrobes (antag only) /obj/item/clothing/head/wizard - affinity = 7 - + affinity = 6 /obj/item/clothing/suit/wizrobe affinity = 7 - /obj/item/clothing/suit/wizrobe/paper // this ones a bit special since its space loot but rare space loot. affinity = 6 + +// This is the actual magnum opus of Wizardy; unless a Wizard item is made to delibaretely interact with thaumaturge, there shouldn't be anything exceeding this. +/obj/item/mod/control/pre_equipped/enchanted + affinity = 8 + affinity_worn_override = TRUE diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index b02ffa8e8f754f..1534f34de3ab85 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -204,11 +204,41 @@ PROCESSING_SUBSYSTEM_DEF(powers) if(!length(required)) continue + var/allow_any = power_type.required_allow_any + var/allow_subtypes = power_type.required_allow_subtypes + var/any_satisfied = FALSE + for(var/datum/power/req_type as anything in required) - if(!selected_types[req_type]) + // Exact requirement satisfied + if(selected_types[req_type]) + any_satisfied = TRUE + if(allow_any) // check to end early if any requirements are validated and allow_any is true. + break + continue + + // Optional: allow subtypes + if(allow_subtypes) + var/required_typepath = ispath(req_type) ? req_type : req_type.type + for(var/datum/power/selected_type as anything in selected_types) + if(ispath(selected_type, required_typepath)) + any_satisfied = TRUE + break + + if(any_satisfied) // check to end early if any requirements are validated and allow_any is true. + if(allow_any) + break + continue + + // If we require all, any missing invalidates. + if(!allow_any) LAZYADD(powers_removed, "[power_name]\" requires [req_type], which was not present.") return list() + // If we require one and we don't have any. + if(allow_any && !any_satisfied) + LAZYADD(powers_removed, "[power_name]\" requires any of [required], none were present.") + return list() + // Everything is fine = return as normal if(intermediary_powers.len == powers_to_check.len) return powers_to_check diff --git a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm index ee756c927a3df7..34222cbcdc719e 100644 --- a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm +++ b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm @@ -1,7 +1,7 @@ // Antiresonant cuffs. They're like normal cuffs but slightly worse and put a dampener on resonant folk. /obj/item/restraints/handcuffs/antiresonant name = "resonant suppressant handcuffs" - desc = "Handcuffs laced with a smooth, dark material similar to vulcanice, harvested from a reality anchor. Capable of suppressing resonant powers." + desc = "Handcuffs laced with a smooth, dark material similar to magnetite, harvested from a reality anchor. Capable of suppressing resonant powers." icon_state = "handcuffAlien" color = "#ee3d3d" // til we get a proper sprite for these things. breakouttime = 50 SECONDS From f60c08a3d785f526e56e7417d8ff6290a1469db6 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 18 Mar 2026 12:49:49 +0100 Subject: [PATCH 147/212] Fixes telekinesis to be less cringe.; routes everything properly, cleaned up code and now has a proper dispel. --- .../powers/resonant/psyker/telekinesis.dm | 138 +++++++++++------- 1 file changed, 89 insertions(+), 49 deletions(-) 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 e8c1235a36622a..877c3c54838f82 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telekinesis.dm @@ -1,8 +1,12 @@ -/* This leviathan of spaghetti is based off of the MODsuit modules. -It is a lazy port to the current acitons powers system from the spells system and has a lot wonkiness as a consequence, including not using use_action. -TODO: FIX THAT +/* + Telekinesis. This is one of the earliest made powers and is a port of how the grab module from MODs do it. It's a bit messy as a consequence; even after this was cleaned up later in production. */ +#define TK_CLICK_NONE 0 +#define TK_CLICK_TRIGGER 1 +#define TK_CLICK_MIDDLE 2 +#define TK_CLICK_RIGHT 3 + /datum/power/psyker_power/telekinesis name = "Telekinesis" desc = "Grants the ability to manipulate and move various objects. Generates stress based upon weight on pick-up and throw, as well as passively while holding an object." @@ -41,12 +45,12 @@ TODO: FIX THAT // Mouse tracker overlay (telekinesis-specific) var/atom/movable/screen/fullscreen/cursor_catcher/kinesis/psyker_tk/kinesis_catcher + // Which mouse click is used in use_action + var/tk_click_type = TK_CLICK_NONE +// Auto-clear the grab if we disable the power + a bit of UI feedback. /datum/action/cooldown/power/psyker/telekinesis/Trigger(mob/clicker, trigger_flags, atom/target) . = ..() - // We run this here cause telekinesis doesn't use use_action because we need click intercepts. - ValidateOrgan() - if(grabbed_atom) clear_grab(playsound = FALSE) to_chat(owner, span_notice("You relax your telekinetic powers.")) @@ -54,53 +58,77 @@ TODO: FIX THAT to_chat(owner, span_notice("You focus your telekinetic powers...
Middle-click: Grab/Punt | Right-click: Drop | Move mouse: to drag")) return TRUE +// We need to disseminate which mouse-press is done for our effects. /datum/action/cooldown/power/psyker/telekinesis/InterceptClickOn(mob/living/clicker, params, atom/target) - .=..() - if(clicker != owner) - return FALSE - var/list/mods = params2list(params) - - // Right click: drop if holding. Doesn't need target or range checks. if(LAZYACCESS(mods, RIGHT_CLICK)) - if(grabbed_atom) - clear_grab() - return TRUE - return FALSE - - if(INCAPACITATED_IGNORING(clicker, INCAPABLE_GRAB)) - owner.balloon_alert(clicker, span_warning("Cannot grab target!")) - return FALSE + tk_click_type = TK_CLICK_RIGHT + else if(LAZYACCESS(mods, MIDDLE_CLICK)) + tk_click_type = TK_CLICK_MIDDLE + else + return FALSE // do not consume the click on lefties. - // Middle click: grab if empty, punt if holding - if(LAZYACCESS(mods, MIDDLE_CLICK)) - if(!grabbed_atom) - if(!target) - owner.balloon_alert(clicker, span_warning("No target!")) + . = ..() + if(!.) + tk_click_type = TK_CLICK_NONE + return TRUE // always return true in right and middle clicks. + +/datum/action/cooldown/power/psyker/telekinesis/use_action(mob/living/user, atom/target) + // gets the mouseclick and saves it; reverts for the next. + var/click_type = tk_click_type + tk_click_type = TK_CLICK_NONE + + // Change effects depending on right and middel click. + switch(click_type) + // Drops the item. + if(TK_CLICK_RIGHT) + if(grabbed_atom) + clear_grab() return TRUE + return FALSE - if(!range_check(clicker, target)) - owner.balloon_alert(clicker, span_warning("Too far!")) + // Grabs if empty, or punts if holding. + if(TK_CLICK_MIDDLE) + if(INCAPACITATED_IGNORING(user, INCAPABLE_GRAB)) + owner.balloon_alert(user, span_warning("Cannot grab target!")) + return FALSE + // Attempt to grab if we aren't holding anything. + if(!grabbed_atom) + if(!target) + owner.balloon_alert(user, span_warning("No target!")) + return FALSE + if(!range_check(user, target)) + owner.balloon_alert(user, span_warning("Too far!")) + return FALSE + if(!can_grab(user, target)) + owner.balloon_alert(user, span_warning("Cannot grab target!")) + return FALSE + + grab_atom(target) return TRUE + // Punt if we are holding something. + punt_held(user, target) + return TRUE - if(!can_grab(clicker, target)) - owner.balloon_alert(clicker, span_warning("Cannot grab target!")) - return TRUE + return FALSE - grab_atom(target) - return TRUE +/datum/action/cooldown/power/psyker/telekinesis/Grant(mob/granted_to) + . = ..() + if(resonant) + RegisterSignal(granted_to, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) - // Holding something: punt - punt_held(clicker, target, params) - return TRUE +/datum/action/cooldown/power/psyker/telekinesis/Remove(mob/removed_from) + . = ..() + if(resonant) + UnregisterSignal(removed_from, COMSIG_ATOM_DISPEL) -// You shouldn't get as stressed from picking up a pen as a closet. +// Calculates the stres cost of vairous interactions. /datum/action/cooldown/power/psyker/telekinesis/proc/get_stress_cost_for_atom(atom/target) var/cost - + // You shouldn't get as stressed from picking up a pen as a closet. if(isitem(target)) - var/obj/item/I = target - switch(I.w_class) + var/obj/item/tk_item = target + switch(tk_item.w_class) if(WEIGHT_CLASS_TINY) cost = PSYKER_STRESS_TRIVIAL if(WEIGHT_CLASS_SMALL) @@ -114,6 +142,7 @@ TODO: FIX THAT return cost +// Important note; because we use the action's proccess, we override cooldown processing. /datum/action/cooldown/power/psyker/telekinesis/process(seconds_per_tick) var/mob/living/user = owner if(!grabbed_atom || !user?.client) @@ -138,8 +167,7 @@ TODO: FIX THAT if(!target_turf) return - // Dragging along hte floor - + // Dragging along the floor if(grabbed_atom.loc != target_turf) var/turf/next_turf = get_step_towards(grabbed_atom, target_turf) @@ -156,16 +184,15 @@ TODO: FIX THAT modify_stress(PSYKER_STRESS_TRIVIAL * seconds_per_tick) // As long as you don't do anything fancy and aren't stressed already, you can do this forever. // The fun part, punting shit. -/datum/action/cooldown/power/psyker/telekinesis/proc/punt_held(mob/living/user, atom/target, params) +/datum/action/cooldown/power/psyker/telekinesis/proc/punt_held(mob/living/user, atom/target) if(!grabbed_atom) return // Where are we throwing it? var/turf/throw_turf = target ? get_turf(target) : null - // If target didn't resolve (common on middle click), derive turf from click params via catcher + // If target didn't resolve (common on middle click), derive turf from cursor catcher if(!throw_turf && kinesis_catcher) - kinesis_catcher.mouse_params = params kinesis_catcher.calculate_params() throw_turf = kinesis_catcher.given_turf @@ -195,9 +222,7 @@ TODO: FIX THAT return FALSE if(ismovable(target) && !isturf(target.loc)) return FALSE - if(!can_see(user, target, grab_range)) - return FALSE - return TRUE + return (target in view(grab_range, user)) // Can we ACTUALLY grab it or will it just fizz out? /datum/action/cooldown/power/psyker/telekinesis/proc/can_grab(mob/living/user, atom/target) @@ -238,6 +263,7 @@ TODO: FIX THAT if(grabbed_atom) clear_grab(playsound = FALSE) grabbed_atom = target + active = TRUE // Mob handling like module_kinesis if(isliving(grabbed_atom)) @@ -246,9 +272,9 @@ TODO: FIX THAT ADD_TRAIT(grabbed_atom, TRAIT_NO_FLOATING_ANIM, REF(src)) RegisterSignal(grabbed_atom, COMSIG_MOVABLE_SET_ANCHORED, PROC_REF(on_setanchored)) + RegisterSignal(grabbed_atom, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) playsound(grabbed_atom, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) - kinesis_icon = mutable_appearance( icon = 'icons/effects/effects.dmi', icon_state = "psychic", @@ -275,6 +301,7 @@ TODO: FIX THAT START_PROCESSING(SSfastprocess, src) /datum/action/cooldown/power/psyker/telekinesis/proc/clear_grab(playsound = TRUE) + active = FALSE if(!grabbed_atom) // Still ensure the fullscreen overlay is gone if we somehow desynced if(owner) @@ -293,7 +320,7 @@ TODO: FIX THAT STOP_PROCESSING(SSfastprocess, src) - UnregisterSignal(held, list(COMSIG_MOB_STATCHANGE, COMSIG_MOVABLE_SET_ANCHORED)) + UnregisterSignal(held, list(COMSIG_MOB_STATCHANGE, COMSIG_MOVABLE_SET_ANCHORED, COMSIG_ATOM_DISPEL)) // Remove overlay BEFORE deleting vars if(kinesis_icon) @@ -323,6 +350,14 @@ TODO: FIX THAT if(grabbed_atom_ref.anchored) clear_grab() +// On dispel, drop the thing. +/datum/action/cooldown/power/psyker/telekinesis/proc/on_dispel(atom/source, atom/dispeller) + SIGNAL_HANDLER + if(grabbed_atom) + clear_grab() + return DISPEL_RESULT_DISPELLED + return NONE + /* ------------------------------------------------------------ // Telekinesis-only screen edge @@ -333,3 +368,8 @@ TODO: FIX THAT alpha = 180 color = "#8A2BE2" mouse_opacity = MOUSE_OPACITY_OPAQUE + +#undef TK_CLICK_NONE +#undef TK_CLICK_TRIGGER +#undef TK_CLICK_MIDDLE +#undef TK_CLICK_RIGHT From e1e5fec8fade0f6dda4e40345f8ef0bbafe94033 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 18 Mar 2026 15:27:10 +0100 Subject: [PATCH 148/212] Adds sanguine absorption. Conjure rain is fixed to have its cast time reflect the correct rain color. Fixed some bugs with unsetting the click ability on cast. Added bats to shapechange. --- .../_globalvars/~doppler_globalvars/powers.dm | 1 + .../sorcerous/thaumaturge/conjure_rain.dm | 11 +- .../thaumaturge/sanguine_absorption.dm | 334 ++++++++++++++++++ .../modular_powers/code/powers_action.dm | 4 +- tgstation.dme | 1 + 5 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm diff --git a/code/_globalvars/~doppler_globalvars/powers.dm b/code/_globalvars/~doppler_globalvars/powers.dm index b589a8879e3674..32c75befdc1c0d 100644 --- a/code/_globalvars/~doppler_globalvars/powers.dm +++ b/code/_globalvars/~doppler_globalvars/powers.dm @@ -11,6 +11,7 @@ GLOBAL_LIST_INIT(shapechange_form_types, list( "Snake" = /mob/living/basic/snake, "Cockroach" = /mob/living/basic/cockroach, "Duct Spider" = /mob/living/basic/spider/maintenance, + "Bat" = /mob/living/basic/bat, "Butterfly" = /mob/living/basic/butterfly, )) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm index 3b0caacef77f42..805ecdc09d49f1 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm @@ -48,7 +48,14 @@ var/rain_color var/obj/item/reagent_containers/held_container = user.get_active_held_item() if(istype(held_container) && held_container.reagents?.reagent_list?.len) - rain_color = mix_color_from_reagents(held_container.reagents.reagent_list) + // We need to make sure that the chems are synthesizable so that people aren't surprised that they can't blood rain + var/list/datum/reagent/synth_reagents = list() + for(var/datum/reagent/reagent in held_container.reagents.reagent_list) + if(reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED) + synth_reagents += reagent + // If all succeeds, mix the rain color. + if(length(synth_reagents)) + rain_color = mix_color_from_reagents(synth_reagents) else // no reagent container, default to rain_chem rain_color = initial(rain_chem.color) @@ -79,6 +86,8 @@ var/chem_ratio = base_chem_ratio var/part = drain_amount / synth_volume for(var/datum/reagent/reagent as anything in held_container.reagents.reagent_list) + if(!(reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED)) + continue var/transfer_amount = reagent.volume * part if(transfer_amount > 0) held_container.reagents.trans_to(buffer.reagents, transfer_amount, chem_ratio, target_id = reagent.type, transferred_by = user) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm new file mode 100644 index 00000000000000..d608154becd8e7 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm @@ -0,0 +1,334 @@ +/* + Heal someone using BLOOD. +*/ +/datum/power/thaumaturge/sanguine_absorption + name = "Sanguine Absorption" + desc = "You draw nearby blood into the target. This draws up to 100u of blood from adjacent floor/wall splatters, containers and other mobs (in that order). It then tranfers that blood to the target and converts it to universally accepted blood.\ + \nAny excess blood in the target creature beyond 100% is transformed into healing, at a 10u per 4 damage ratio. This can only heal organic bodyparts and does not heal any damage-types besides Brute or Burn. This also does not affect creatures with non-blood bloodtypes such as Ethereals or Slimepeople. \ + \nRequires Affinity 3. Affinity increases the healing ratio by 0.5 per point" + value = 4 + + action_path = /datum/action/cooldown/power/thaumaturge/sanguine_absorption + required_powers = list(/datum/power/thaumaturge_root) + +/datum/action/cooldown/power/thaumaturge/sanguine_absorption + name = "Sanguine Absorption" + desc = "You draw nearby blood into the target. This draws up to 100u of blood from adjacent floor/wall splatters, containers and other mobs (in that order). It then tranfers that blood to the target and converts it to universally accepted blood.\ + \nAny excess blood in the target creature beyond 100% is transformed into healing, at a 10u per 4 damage ratio. This can only heal organic bodyparts and does not heal any damage-types besides Brute or Burn. This also does not affect creatures with non-blood bloodtypes such as Ethereals or Slimepeople." + button_icon = 'icons/effects/blood.dmi' + button_icon_state = "bubblegumfoot" + + required_affinity = 3 + prep_cost = 4 + target_range = 4 + + use_time = 3 SECONDS + click_to_activate = TRUE + + // Healing ratio per 1u + var/healing_ratio = 0.4 + // How much extra affinity adds to the ratio. + var/affinity_healing_ratio_bonus = 0.05 + + /// How much blood (in units) we try to gather. + var/harvest_goal = 100 + + // The special effect on the target + var/use_time_target_overlay = /obj/effect/temp_visual/sanguine_absorption + // Tracks whether the current cast was dispelled mid-channel. + var/cast_interrupted_by_dispel = FALSE + +/datum/action/cooldown/power/aberrant/cocoon/InterceptClickOn(mob/living/clicker, params, atom/target) + ..() + // Always consume the click to avoid normal click interactions. + return TRUE + +// We do extra validation because we want to make sure containers aren't full and we aren't trying to put blood in a mob that can't hold it. +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/can_use(mob/living/user, atom/target) + . = ..() + if(istype(target, /obj/item/reagent_containers)) + var/obj/item/reagent_containers/container = target + if(!container.reagents || container.reagents.total_volume >= container.reagents.maximum_volume) + user.balloon_alert(user, "container is full!") + return FALSE + return ..() + + if(!isliving(target)) + return FALSE + + var/mob/living/target_mob = target + // ew, electricity/motor oil/plasma/whatever else aliens are composed of + if(!is_valid_blood_target(target_mob)) + user.balloon_alert(user, "no blood to work with!") + return FALSE + if(target_mob.blood_volume <= BLOOD_VOLUME_NORMAL + 10 && !has_valid_blood_sources(get_turf(target_mob), target_mob)) + user.balloon_alert(user, "no blood nearby!") + return FALSE + +// Special cast effects; we want the blood orb to appear above the target.. +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/do_use_time(mob/living/user, atom/target) + cast_interrupted_by_dispel = FALSE + if(user) + RegisterSignal(user, COMSIG_ATOM_DISPEL, PROC_REF(on_cast_dispel)) + if(target) + RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_cast_dispel)) + var/target_use_overlay + if(use_time_target_overlay) + var/atom/overlay_obj = new use_time_target_overlay(null) + target_use_overlay = new /mutable_appearance(overlay_obj) + qdel(overlay_obj) + target.add_overlay(target_use_overlay) + // Spawns an indicator meant to show nearby targets that they are in the danger zone of having their blood donated to a blood drive. + var/target_location = get_turf(target) + for(var/atom/movable/source as anything in get_valid_blood_sources(target_location, null, null)) + new /obj/effect/temp_visual/sanguine_absorption_target(get_turf(source)) + + target.visible_message(span_warning("[user] draws nearby blood into an orb above [target]!")) + playsound(target, 'sound/effects/magic/enter_blood.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + . = ..() + if(user) + UnregisterSignal(user, COMSIG_ATOM_DISPEL) + if(target) + UnregisterSignal(target, COMSIG_ATOM_DISPEL) + if(cast_interrupted_by_dispel) + return FALSE + if(target_use_overlay && !QDELETED(target)) + target.cut_overlay(target_use_overlay) + +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/use_action(mob/living/user, atom/target) +// Filling reagent containers with blood. + if(istype(target, /obj/item/reagent_containers)) + var/obj/item/reagent_containers/container = target + if(!container.reagents) + return FALSE + + // If between the cast time finishing and this happening the container is filled. + var/remaining_capacity = container.reagents.maximum_volume - container.reagents.total_volume + if(remaining_capacity <= 0) + user.balloon_alert(user, "container is full!") + return FALSE + + var/harvest_cap = min(harvest_goal, remaining_capacity) // harvest_goal capped by the spare space in teh cotnainer + var/turf/center = get_turf(container) + if(!center) + return FALSE + + // Go get some blood. + var/harvested = harvest_blood(center, harvest_cap, null, container) + if(harvested <= 0) // you failed. + user.balloon_alert(user, "no blood nearby!") + return FALSE + container.reagents.add_reagent(/datum/reagent/blood, harvested) + + user.visible_message(span_notice("Blood gathers into [target].")) + playsound(target, 'sound/effects/splat.ogg', 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + return TRUE + +// Filling mobs with blood. + if(!isliving(target)) + return FALSE + + var/mob/living/target_mob = target + var/turf/center = get_turf(target_mob) + if(!center) + return FALSE + + // Harvest loop: We try to gather as much as possible from nearby sources, one at a time, until we meet the quota. + var/harvested = harvest_blood(center, harvest_goal, target_mob, null) + + // What a shitty blood drive. + if(harvested <= 0 && target_mob.blood_volume <= BLOOD_VOLUME_NORMAL + 10) // we do +10 just to make sure we have something to work with + user.balloon_alert(user, "no blood nearby!") + return FALSE + + target_mob.blood_volume += harvested + + // We set the healing ratio and attempt to heal the target. + var/ratio = healing_ratio + (isnum(affinity) ? affinity * affinity_healing_ratio_bonus : 0) + if(ratio > 0) + var/excess_blood = max(target_mob.blood_volume - BLOOD_VOLUME_NORMAL, 0) + if(excess_blood > 0 && iscarbon(target_mob)) + var/mob/living/carbon/target_carbon = target_mob + var/total_brute = 0 + var/total_burn = 0 + // Gets all the damage across various bodyparts. + for(var/obj/item/bodypart/part as anything in target_carbon.bodyparts) + if(!(part.bodytype & BODYTYPE_ORGANIC)) + continue + total_brute += part.brute_dam + total_burn += part.burn_dam + var/total_damage = total_brute + total_burn + + // Based on the total damage, we heal based on the excess blood compared to the normal blood volume. + if(total_damage > 0) + var/heal_capacity = excess_blood * ratio // max we can heal + var/heal_amount = min(heal_capacity, total_damage) // how much we will heal total + var/heal_brute = total_damage ? (heal_amount * (total_brute / total_damage)) : 0 // we try to heal all brute damage first + var/heal_burn = heal_amount - heal_brute // then we heal burn damage + var/actual_healed = target_carbon.heal_overall_damage(brute = heal_brute, burn = heal_burn, updating_health = FALSE, required_bodytype = BODYTYPE_ORGANIC) + // update the blood in the target based on the healing used. + if(actual_healed > 0) + var/blood_used = min(excess_blood, actual_healed / ratio) + target_carbon.blood_volume = max(target_carbon.blood_volume - blood_used, BLOOD_VOLUME_NORMAL) + target_carbon.updatehealth() + target.visible_message(span_notice("Blood flows into [target]'s body, reinvigorating them!"), span_notice("You feel energized as the blood mends your body!")) + playsound(target, 'sound/effects/splat.ogg', 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + return TRUE + +// Do you have BLOOD; as in the real deal. +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/is_valid_blood_target(mob/living/target_mob) + if(!target_mob) + return FALSE + return target_mob.get_blood_reagent() == /datum/reagent/blood + +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/on_cast_dispel(datum/source, atom/dispeller) + cast_interrupted_by_dispel = TRUE + to_chat(owner, span_warning("Your [name] is dispelled!")) + +// Checks if there's any valid blood sources in the area. +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/get_valid_blood_sources(turf/center, mob/living/target_mob, obj/item/reagent_containers/ignore_container) + if(!center) + return list() + + var/list/sources = list() + + for(var/obj/effect/decal/cleanable/blood/blood_decal in range(1, center)) + if(blood_decal.dried || blood_decal.bloodiness <= 0) + continue + if(!(blood_decal.decal_reagent == /datum/reagent/blood || blood_decal.reagents?.has_reagent(/datum/reagent/blood))) + continue + sources += blood_decal + + for(var/obj/item/reagent_containers/container in range(1, center)) + if(ignore_container && container == ignore_container) + continue + if(!container.reagents) + continue + if(container.reagents.get_reagent_amount(/datum/reagent/blood) <= 0) + continue + sources += container + + for(var/mob/living/other in range(1, center)) + if(other == target_mob) + continue + if(other.get_blood_reagent() != /datum/reagent/blood) + continue + if(other.blood_volume <= 0) + continue + sources += other + + return sources + +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/has_valid_blood_sources(turf/center, mob/living/target_mob, obj/item/reagent_containers/ignore_container) + return length(get_valid_blood_sources(center, target_mob, ignore_container)) > 0 + +// Attempts to do a blood drive on decals, containers and mobs in descending order. +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/harvest_blood(turf/center, amount_needed, mob/living/target_mob, obj/item/reagent_containers/ignore_container) + if(amount_needed <= 0 || !center) + return 0 + + var/harvested = 0 + harvested += harvest_blood_from_decals(center, amount_needed - harvested) + if(harvested < amount_needed) + harvested += harvest_blood_from_containers(center, amount_needed - harvested, ignore_container) + if(harvested < amount_needed) + harvested += harvest_blood_from_mobs(center, amount_needed - harvested, target_mob) + + return harvested + +// Attempts to harvest decals. +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/harvest_blood_from_decals(turf/center, amount_needed) + if(amount_needed <= 0 || !center) + return 0 + + var/harvested = 0 + for(var/obj/effect/decal/cleanable/blood/blood_decal in range(1, center)) + if(harvested >= amount_needed) + break + if(blood_decal.dried || blood_decal.bloodiness <= 0) + continue + if(!(blood_decal.decal_reagent == /datum/reagent/blood || blood_decal.reagents?.has_reagent(/datum/reagent/blood))) + continue + + var/available_units = blood_decal.bloodiness * BLOOD_TO_UNITS_MULTIPLIER + if(available_units <= 0) + continue + + var/to_take = min(amount_needed - harvested, available_units) + if(to_take >= available_units) // if we would take the max amount, we destroy hte decal in the process. + if(blood_decal.reagents) + blood_decal.reagents.remove_reagent(/datum/reagent/blood, to_take, include_subtypes = TRUE) + qdel(blood_decal) + else // otherwise, we take away the reagent. + var/bloodiness_to_remove = to_take / BLOOD_TO_UNITS_MULTIPLIER + blood_decal.adjust_bloodiness(-bloodiness_to_remove) + if(blood_decal.reagents) + blood_decal.reagents.remove_reagent(/datum/reagent/blood, to_take, include_subtypes = TRUE) + harvested += to_take + + return harvested + +// Attempts to harvest containers +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/harvest_blood_from_containers(turf/center, amount_needed, obj/item/reagent_containers/ignore_container) + if(amount_needed <= 0 || !center) + return 0 + + var/harvested = 0 + for(var/obj/item/reagent_containers/container in range(1, center)) + if(harvested >= amount_needed) + break + if(ignore_container && container == ignore_container) + continue + if(!isturf(container.loc) || !container.reagents) + continue + var/available_units = container.reagents.get_reagent_amount(/datum/reagent/blood) + if(available_units <= 0) + continue + var/to_take = min(amount_needed - harvested, available_units) + container.reagents.remove_reagent(/datum/reagent/blood, to_take, include_subtypes = TRUE) + harvested += to_take + + return harvested + +// Attempts to harvest mobs. +/datum/action/cooldown/power/thaumaturge/sanguine_absorption/proc/harvest_blood_from_mobs(turf/center, amount_needed, mob/living/target_mob) + if(amount_needed <= 0 || !center) + return 0 + + var/harvested = 0 + for(var/mob/living/other in range(1, center)) + if(harvested >= amount_needed) + break + if(other == target_mob) + continue + if(other.can_block_resonance(1)) // Doesn't work if you're immune to resonance magic. + continue + if(other.get_blood_reagent() != /datum/reagent/blood) + continue + if(other.blood_volume <= 0) + continue + + var/to_take = min(amount_needed - harvested, other.blood_volume) + if(to_take <= 0) + continue + to_chat(other, span_userdanger("Blood is drawn from your body by [owner]!")) + other.blood_volume = max(other.blood_volume - to_take, 0) + harvested += to_take + + return harvested + + +// The visual effect of the cast +/obj/effect/temp_visual/sanguine_absorption + name = "blood bubble" + icon = 'icons/obj/weapons/guns/projectiles.dmi' + icon_state = "mini_leaper" + layer = ABOVE_MOB_LAYER + duration = 3 SECONDS + alpha = 200 + + +/obj/effect/temp_visual/sanguine_absorption_target + icon_state = "blessed" + color = "#da2424" + duration = 3 SECONDS diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 54e4b01a1dab29..58eb6584030249 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -110,6 +110,8 @@ use_overlay = new /mutable_appearance(overlay_obj) qdel(overlay_obj) user.add_overlay(use_overlay) + if(click_to_activate && unset_after_click) // unsets the mouse on use time. + unset_click_ability(user) var/success = do_after(user, use_time, target = use_target, timed_action_flags = use_time_flags) if(use_overlay && !QDELETED(user)) user.cut_overlay(use_overlay) @@ -188,7 +190,7 @@ Handles all the logic involved in using a targeted, click-based action. // If the power can't be used, refuse the click and keep intercept state as-is. if(!try_use(clicker, target)) // fixes the overlay from cast time getting stuck. - if(clicker?.click_intercept == src) + if(clicker?.click_intercept == src && unset_after_click) unset_click_ability(clicker, refund_cooldown = TRUE) return FALSE StartCooldown() diff --git a/tgstation.dme b/tgstation.dme index 83aead2f53f8a9..ee7965d955ff1a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7554,6 +7554,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\gale_blast.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\magic_barrage.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\phantasmal_tool.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\sanguine_absorption.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\vitalize_flora.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_affinity.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_spell_focus.dm" From 55d681e226356aa04ae616b1617f88a5654938fb Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 21 Mar 2026 06:57:38 +0100 Subject: [PATCH 149/212] Tewaks to reality anchor, typo, sanguine absorption tweakin'. --- .../thaumaturge/sanguine_absorption.dm | 8 ++-- .../theologist/_theologist_root_twisted.dm | 2 +- .../code/security/reality_anchor.dm | 42 ++++++++++++------ .../icons/items/reality_anchor.dmi | Bin 0 -> 547 bytes 4 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 modular_doppler/modular_powers/icons/items/reality_anchor.dmi diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm index d608154becd8e7..88fcedb39fc8f7 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm @@ -5,8 +5,8 @@ name = "Sanguine Absorption" desc = "You draw nearby blood into the target. This draws up to 100u of blood from adjacent floor/wall splatters, containers and other mobs (in that order). It then tranfers that blood to the target and converts it to universally accepted blood.\ \nAny excess blood in the target creature beyond 100% is transformed into healing, at a 10u per 4 damage ratio. This can only heal organic bodyparts and does not heal any damage-types besides Brute or Burn. This also does not affect creatures with non-blood bloodtypes such as Ethereals or Slimepeople. \ - \nRequires Affinity 3. Affinity increases the healing ratio by 0.5 per point" - value = 4 + \nRequires Affinity 3. Additional affinity increases the healing ratio by 0.5 per point" + value = 5 action_path = /datum/action/cooldown/power/thaumaturge/sanguine_absorption required_powers = list(/datum/power/thaumaturge_root) @@ -19,7 +19,7 @@ button_icon_state = "bubblegumfoot" required_affinity = 3 - prep_cost = 4 + prep_cost = 5 target_range = 4 use_time = 3 SECONDS @@ -144,7 +144,7 @@ target_mob.blood_volume += harvested // We set the healing ratio and attempt to heal the target. - var/ratio = healing_ratio + (isnum(affinity) ? affinity * affinity_healing_ratio_bonus : 0) + var/ratio = healing_ratio + (isnum(affinity) ? max(affinity - required_affinity, 0) * affinity_healing_ratio_bonus : 0) if(ratio > 0) var/excess_blood = max(target_mob.blood_volume - BLOOD_VOLUME_NORMAL, 0) if(excess_blood > 0 && iscarbon(target_mob)) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index fe5e6ecad2b896..d5d33d1c3d3548 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -43,7 +43,7 @@ // Because we have a do_while, it won't get to the usual unset_click_ability() until after the efffect resolves, so we have to run it here. unset_click_ability(owner, FALSE) keep_going = TRUE - owner.visible_message(span_warning("[owner.get_visible_name()] lays a hand on [target.get_visible_name()], twisting their injurioes into other, smaller injuries!"), span_notice("You twist [target.get_visible_name()]'s injuries!")) + owner.visible_message(span_warning("[owner.get_visible_name()] lays a hand on [target.get_visible_name()], twisting their injuries into other, smaller injuries!"), span_notice("You twist [target.get_visible_name()]'s injuries!")) // Listeners for dispelling. RegisterSignal(user, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) diff --git a/modular_doppler/modular_powers/code/security/reality_anchor.dm b/modular_doppler/modular_powers/code/security/reality_anchor.dm index 62c51da8313985..727e21c7cd25bb 100644 --- a/modular_doppler/modular_powers/code/security/reality_anchor.dm +++ b/modular_doppler/modular_powers/code/security/reality_anchor.dm @@ -1,8 +1,8 @@ /obj/structure/reality_anchor - name = "reassembled reality anchor" - desc = "The fragments of a broken down reality anchor, reassembled. Crude machinery is managing to keep it docile; but when enabled, it will start enforcing normality back in a large area around it." - icon = 'icons/obj/antags/cult/structures.dmi' - icon_state = "pylon_off" + name = "miniature reality anchor" + desc = "The chiseled out parts of a broken down reality anchor. Crude machinery is managing to keep it docile; but when enabled, it will start enforcing normality back in a large area around it." + icon = 'modular_doppler/modular_powers/icons/items/reality_anchor.dmi' + icon_state = "reality_anchor" density = TRUE // Is it on/off @@ -15,12 +15,12 @@ // Range in turfs var/pulse_range = 6 - // on and off icon states. - var/on_icon_state = "pylon" - var/off_icon_state = "pylon_off" + // Ripple filter while active. + var/ripple_filter_id = "reality_anchor_ripple" /obj/structure/reality_anchor/Destroy() STOP_PROCESSING(SSobj, src) + apply_ripple_filter(FALSE) . = ..() // Turns the thing on or off after the do_after. @@ -29,6 +29,7 @@ if(.) return var/action_word = active ? "deactivate" : "activate" + var/action_word_past_tense = active ? "deactivating" : "activating" user.visible_message( span_warning("[user] begins to [action_word] the reality anchor..."), span_warning("You begin to [action_word] the reality anchor...") @@ -36,8 +37,8 @@ if(!do_after(user, 3 SECONDS, target = src)) return user.visible_message( - span_warning("[user] finishes [action_word]ing the reality anchor."), - span_warning("You finish [action_word]ing the reality anchor.") + span_warning("[user] finishes [action_word_past_tense] the reality anchor."), + span_warning("You finish [action_word_past_tense] the reality anchor.") ) toggle_anchor(user) @@ -46,13 +47,14 @@ active = !active if(active) anchored = TRUE - icon_state = on_icon_state + apply_ripple_filter(TRUE) + playsound(launched, 'sound/effects/magic/repulse.ogg', 75, TRUE) pulse() next_pulse_time = world.time + pulse_interval START_PROCESSING(SSobj, src) return anchored = FALSE - icon_state = off_icon_state + apply_ripple_filter(FALSE) STOP_PROCESSING(SSobj, src) // Countdown til dispel pulse. @@ -80,6 +82,17 @@ else if(isobj(target)) target.dispel(src) +// Applies a rippling effect. +/obj/structure/reality_anchor/proc/apply_ripple_filter(active_state) + if(active_state) + add_filter(ripple_filter_id, 2, list("type" = "ripple", "flags" = WAVE_BOUNDED, "radius" = 0, "size" = 2)) + var/filter = get_filter(ripple_filter_id) + if(filter) + animate(filter, radius = 0, time = 0.2 SECONDS, size = 2, easing = JUMP_EASING, loop = -1, flags = ANIMATION_PARALLEL) + animate(radius = 32, time = 1.5 SECONDS, size = 0) + return + remove_filter(ripple_filter_id) + // Status effect responsible for silencing. /datum/status_effect/power/reality_anchor_silenced id = "reality_anchor_silenced" @@ -100,8 +113,8 @@ /atom/movable/screen/alert/status_effect/reality_anchor_silenced name = "Silenced" desc = "Resonant powers are periodically dispelled and supressed around the reality anchor!" - icon = 'icons/obj/antags/cult/structures.dmi' - icon_state = "pylon" + icon = 'modular_doppler/modular_powers/icons/items/reality_anchor.dmi' + icon_state = "reality_anchor" // The effect from reality anchors /obj/effect/temp_visual/circle_wave/reality_anchor @@ -109,3 +122,6 @@ max_alpha = 20 duration = 0.5 SECONDS amount_to_scale = 6 + +/obj/structure/reality_anchor/update_overlays() + . = ..() diff --git a/modular_doppler/modular_powers/icons/items/reality_anchor.dmi b/modular_doppler/modular_powers/icons/items/reality_anchor.dmi new file mode 100644 index 0000000000000000000000000000000000000000..fde5ffc3bd95f743db0d74a1b65b6fa4973cbaa7 GIT binary patch literal 547 zcmV+;0^I$HP)YE64*+io9p>FXyAJ>W0We8KK~yM_z0$o-!!Qs4;1mgF8m4x_i4xHt6su`~c?Q!mUdN zw-ewn1x#QQ;7DV@1U@O@<4vhc7=uX&koeXtJaNih-0SsK#hHif8R4=8##~AMh|smT zpFITf8ljJ&^rw&pK^m|rL|6!&8W3@~ED&jc`f;QNnz&API%Q2e(j(M;3kRLDfxPK- zV=7%s4Pr-?ZY-+RMrlw7)!Ezbr3JAcEM!}$FvPZcIk|bXLMfRiaQtvHt@OiY0K?Kj zs`tSDR?Cxx(#_*;Zh`p;1t@KWgIQ@RC`swr0+<52cukIrcMlW_vN{iM;Rh8^+&m1Q l-=B^oB%b_263+wt5nqQcaa8(CAJzZ>002ovPDHLkV1k=r;*tOW literal 0 HcmV?d00001 From e9ec0245f95e8dae49399b96dd9081ef73cffd0c Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 21 Mar 2026 08:25:14 +0100 Subject: [PATCH 150/212] Thaumaturge now has a minimum sleep type. Nullrods and bibles added to thaumaturge affinity (not all of them). Fixes some issue with spell focus belt slots. --- code/__DEFINES/~doppler_defines/powers.dm | 3 ++ .../thaumaturge/_thaumaturge_preperation.dm | 43 +++++++++++++++++-- .../thaumaturge/_thaumaturge_root.dm | 5 +-- .../affinity/thaumaturge_affinity.dm | 20 ++++++++- .../affinity/thaumaturge_spell_focus.dm | 36 +++++++++++++--- .../code/security/reality_anchor.dm | 2 +- 6 files changed, 96 insertions(+), 13 deletions(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 2978e8a9e7dd2a..dda1d982438403 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -103,6 +103,9 @@ // hard cap on refund powers. #define THAUMATURGE_REFUND_MAX 75 +// How long a thaumaturge has to sleep to get their charges. Please make sure that this is BELOW the normal sleep verb's time. +#define THAUMATURGE_SLEEP_TIME 30 SECONDS + /** * SORCEROUS: ENIGMATIST * All defines related to the enigmatist powers. diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm index 3277f5ab77dc23..da14850af6d158 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_preperation.dm @@ -51,9 +51,9 @@ // Do we have queqed changes and is the flag that it passed validation on? if(applied_prepared_charges && recharge_when_sleep) //Do we have the focus on our person? - for(var/obj/item/spell_focus/focus_item in attached_mob.get_all_contents()) - apply_spell_charges() - to_chat(attached_mob, span_notice("Your mind focuses on your spells, and through your dreams, you feel your Thaumaturge powers recharge!")) + if(locate(/obj/item/spell_focus) in attached_mob.get_all_contents()) + // apply the status effect which handles replenishment. + attached_mob.apply_status_effect(/datum/status_effect/power/thaumaturgic_sleep, src) return to_chat(attached_mob, span_warning("You cannot recharge your spells without a Spell Focus on your person!")) @@ -302,3 +302,40 @@ return TRUE return FALSE + +// Status effect used for validating sleep +/datum/status_effect/power/thaumaturgic_sleep + id = "thaumaturgic_sleep" + duration = THAUMATURGE_SLEEP_TIME // required amount of sleepytime + tick_interval = 1 SECONDS + show_duration = TRUE + alert_type = /atom/movable/screen/alert/status_effect/thaumaturgic_sleep + var/ends_early = FALSE + var/datum/component/thaumaturge_preparation/prep_component + +/datum/status_effect/power/thaumaturgic_sleep/on_creation(mob/living/new_owner, datum/component/thaumaturge_preparation/thaum_component) + prep_component = thaum_component + return ..() + +// Ticks every second, checks for focus and if we are asleep +/datum/status_effect/power/thaumaturgic_sleep/tick(seconds_between_ticks) + var/has_focus = locate(/obj/item/spell_focus) in owner.get_all_contents() + + if(!owner.IsSleeping() || !has_focus) + ends_early = TRUE + qdel(src) + +/datum/status_effect/power/thaumaturgic_sleep/on_remove() + // YOU GET NOTHING, YOU LOSE. + if(ends_early || QDELETED(owner)) + return + if(!prep_component) + return + prep_component.apply_spell_charges() + to_chat(owner, span_notice("Your mind focuses on your spells, and through your dreams, you feel your Thaumaturge powers recharge!")) + +/atom/movable/screen/alert/status_effect/thaumaturgic_sleep + name = "Thaumaturgic Sleep" + desc = "You are manifesting your thaumaturgic power through your dreams; if you are asleep with your spell focus when this effect expires, you will recharge your spells. Waking up early yields nothing!" + icon = 'icons/obj/weapons/guns/projectiles.dmi' + icon_state = "ice_1" diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm index 71a56abc565d63..a5763e70c12858 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 @@ -13,7 +13,6 @@ /datum/power/thaumaturge_root/add_unique(client/client_source) var/obj/item/spell_focus/spell_focus = new(get_turf(power_holder)) - spell_focus.name = "[power_holder.real_name]'s spell focus" give_item_to_holder(spell_focus, list(LOCATION_BACKPACK, LOCATION_HANDS)) /datum/power/thaumaturge_root/post_add() @@ -26,8 +25,8 @@ /datum/action/cooldown/power/thaumaturge/thaumaturge_root name = "Spell Preperation" desc = "Adjust the amount of charges your spells have! Requires sleeping with a Spell Focus on your person to apply (except the first time in a round)." - button_icon = 'icons/obj/storage/book.dmi' - button_icon_state = "ithaqua" + button_icon = 'icons/obj/service/library.dmi' + button_icon_state = "bookcharge" // Makes it not interact with the charges system. max_charges = null 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 292bb1f416334f..cb1109ec7f960f 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 @@ -69,6 +69,10 @@ A lot of Affinity asignments are vibe-based depending on looks, visibility and r /obj/item/staff // the base item is small affinity = 2 +// Nullrods come in a lot of shapes and forms; by default we give it affinity 2 unless it fucks with slots and is clearly magical. +/obj/item/nullrod + affinity = 2 + // Animal masks that arent small /obj/item/clothing/mask/animal affinity = 2 @@ -111,6 +115,10 @@ A lot of Affinity asignments are vibe-based depending on looks, visibility and r /obj/item/hierophant_club affinity = 3 +// Its the bible! Given we allow tomes, I'm sure some people will want to larp a cleric or otherwise have some magic religion. Print more bibles! +/obj/item/book/bible + affinity = 3 + /* Tier 4 */ @@ -204,9 +212,19 @@ A lot of Affinity asignments are vibe-based depending on looks, visibility and r Tier 5+ */ -// Chaplain nullrod staves +// Nullrods that are bulky and clearly magical /obj/item/nullrod/staff affinity = 5 +/obj/item/nullrod/vibro/spellblade + affinity = 5 +/obj/item/vorpalscythe + affinity = 5 +/obj/item/nullrod/claymore/darkblade // its a cult sword and it glows thats good enough + affinity = 5 +/obj/item/nullrod/pitchfork + affinity = 5 +/obj/item/nullrod/pride_hammer + affinity = 5 // Haunted blade, the heretic sword that gives you cool shit. /obj/item/melee/cultblade/haunted diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm index 3eac5dd6f09dbb..5412364efbbb5c 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm @@ -7,8 +7,10 @@ w_class = WEIGHT_CLASS_TINY obj_flags = UNIQUE_RENAME affinity = 2 // check thaumaturge_affinity.dm if you ever wonder what deserves what affinity + /// If FALSE, suppress belt sprite entirely (prevents missing belt sprites). + var/shows_on_belt = FALSE /// Short description of what this item is capable of, for radial menu uses. - var/menu_description = "An orb of energy. Fits in pockets. Can be worn on the belt. Very convenient and not visible in your hands, but doesn't do much more than that." + var/menu_description = "An orb of energy. Fits in pockets. Can be worn on the belt. Very convenient, gives affinity 2 and is not visible in your hands, but doesn't do much more than that." /obj/item/spell_focus/Initialize(mapload) . = ..() @@ -23,6 +25,12 @@ AddComponent(/datum/component/subtype_picker, focuses, CALLBACK(src, PROC_REF(on_spell_focus_picked))) +// Supresses belt sprites if unwanted whilst still allowing the slot to be used. +/obj/item/spell_focus/get_belt_overlay() + if(!shows_on_belt) + return null + return ..() + /obj/item/spell_focus/proc/on_spell_focus_picked(obj/item/spell_focus/new_focus, mob/living/picker) if(!istype(new_focus)) return @@ -34,6 +42,26 @@ if(old_focus.name != initial(old_focus.name)) name = old_focus.name +/obj/item/spell_focus/tome + name = "thaumaturge's tome" + desc = "A tome! What secrets does it hold? Apparently long lines of jargon that only one specific person can understand; some people need to learn how to convey information." + icon = 'icons/obj/service/library.dmi' + icon_state = "bookcharge" + lefthand_file = 'icons/mob/inhands/items/books_lefthand.dmi' + righthand_file = 'icons/mob/inhands/items/books_righthand.dmi' + inhand_icon_state = "kojiki" // they have no inhands but affinity3 needs inhands so we borrow another blue book instead + throw_speed = 1 + throw_range = 5 + slot_flags = ITEM_SLOT_BELT + shows_on_belt = TRUE + w_class = WEIGHT_CLASS_NORMAL + attack_verb_continuous = list("bashes", "whacks", "educates") + attack_verb_simple = list("bash", "whack", "educate") + drop_sound = 'sound/items/handling/book_drop.ogg' + pickup_sound = 'sound/items/handling/book_pickup.ogg' + affinity = 3 + menu_description = "An arcane tome. Fits in your backpack and on your belt, and provides affinity 3; but does not fit in the pockets and is fairly conspicuous." + /obj/item/spell_focus/wand name = "thaumaturge's wand" desc = "A pointy stick, attuned to work with thaumaturgic resonance. Capable of restoring thaumaturgic powers when resting." @@ -45,7 +73,7 @@ slot_flags = ITEM_SLOT_BELT w_class = WEIGHT_CLASS_NORMAL affinity = 3 - menu_description = "A classical magic wand. Fits in your backpack and on your belt, and provides more affinity than the orb; but does not fit in any pockets and is clearly visible when held." + menu_description = "A classical magic wand. Fits in your backpack and on your belt, and provides affinity 3; but does not fit in any pockets and is clearly visible when held." /obj/item/spell_focus/staff name = "thaumaturge's staff" @@ -60,6 +88,4 @@ force = 7 slot_flags = ITEM_SLOT_BACK affinity = 5 - menu_description = "A staff with an orb on the end. Because it is bulky, it can only be stored in the back slot, but offers a very high amount of Affinity in return. As well as being very apt for whacking youngsters." - - + menu_description = "A staff with an orb on the end. Because it is bulky, it can only be stored in the back slot, but offers affinity 5 in return. As well as being very apt for whacking fools that can't comprehend your arcane knowledge." diff --git a/modular_doppler/modular_powers/code/security/reality_anchor.dm b/modular_doppler/modular_powers/code/security/reality_anchor.dm index 727e21c7cd25bb..a0883f1ab2871b 100644 --- a/modular_doppler/modular_powers/code/security/reality_anchor.dm +++ b/modular_doppler/modular_powers/code/security/reality_anchor.dm @@ -48,7 +48,7 @@ if(active) anchored = TRUE apply_ripple_filter(TRUE) - playsound(launched, 'sound/effects/magic/repulse.ogg', 75, TRUE) + playsound(src, 'sound/effects/magic/repulse.ogg', 75, TRUE) pulse() next_pulse_time = world.time + pulse_interval START_PROCESSING(SSobj, src) From 20a8f301a0c522261cab4d7173bd2cf3c2e9b3cc Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 21 Mar 2026 09:47:44 +0100 Subject: [PATCH 151/212] Dual Wielder now has an overlay that indicates if its on or off; smiting strike now respects magic immunity. HeavyLifter+Strider now properly apply skills. Reagent spray cannon now properly sprays 1x3 instead of whatever it was doing and now properly breaks at 0 quality. --- .../powers/mortal/augmented/reagent_cannon.dm | 17 +++++-------- .../code/powers/mortal/expert/heavy_lifter.dm | 5 ++-- .../code/powers/mortal/expert/punt.dm | 2 +- .../code/powers/mortal/expert/strider.dm | 3 ++- .../powers/mortal/warfighter/dual_wielder.dm | 25 +++++++++++++++++-- .../sorcerous/theologist/pious_prayer.dm | 2 +- .../sorcerous/theologist/smiting_strike.dm | 17 ++++++++----- 7 files changed, 47 insertions(+), 24 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm index f8c56a8075c857..c5375ba6c73091 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm @@ -160,19 +160,14 @@ // Allows us to basically toggle between 1x or 3x spray. /obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/spray(atom/A, mob/user) - var/turf/T = get_turf(A) + if(!premium_component?.can_function()) + to_chat(owner, span_warning("Your [name] fails to respond; it seems broken!")) + return FALSE + var/turf/target_turf = get_turf(A) if(focused_mode) - call(src, /obj/item/reagent_containers/spray/proc/spray)(T, user) // only way we can get a 1x1 spray because the chemsprayer is our parent. + call(src, /obj/item/reagent_containers/spray/proc/spray)(target_turf, user) // only way we can get a 1x1 spray because the chemsprayer is our parent and that overrides standard spray rules. return - var/direction = get_dir(src, A) - var/turf/T1 = get_step(T, turn(direction, 90)) - var/turf/T2 = get_step(T, turn(direction, -90)) - var/list/the_targets = list(T, T1, T2) - - for(var/i in 1 to 3) // intialize sprays - if(reagents.total_volume < 1) - return - ..(the_targets[i], user) + ..() // Allows us to switch between focused (1x wide) or unfocused (3x wide) /obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/toggle_stream_mode(mob/user) 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 d8e93951401559..94980927d49633 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 @@ -4,7 +4,7 @@ /datum/power/expert/heavy_lifter name = "Heavy Lifter" - desc = "A strong back does a lot when it comes to carrying closets. You ignore the slowdown from dragging objects and having creatures grabbed/ and/or carried. You also start off as a Journeyman in the Athletics skill. \ + desc = "A strong back does a lot when it comes to carrying closets. You ignore the slowdown from dragging objects and having creatures grabbed and/or carried. You also start off as a Journeyman in the Athletics skill. \ All other slowdowns such as stamina, items, damage, etc. still apply as normal." security_record_text = "Subject possesses a high degree of strength and is capable of hauling objects without being slowed down." value = 5 @@ -13,7 +13,8 @@ // tracks how much was given for removal later. var/xp_given = 0 -/datum/power/expert/heavy_lifter/add() +/datum/power/expert/heavy_lifter/post_add() + ..() // Grab slowdowns all share the same movespeed id. power_holder.add_movespeed_mod_immunities(src, MOVESPEED_ID_MOB_GRAB_STATE) power_holder.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/bulky_drag) diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm index f459beaf728293..f88bebbe3d95ec 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/punt.dm @@ -80,7 +80,7 @@ living_atom.log_message("was punted by an object from [thrower] for [damage] damage.", LOG_VICTIM) thrower.log_message("punted an object at [living_atom] for [damage] damage.", LOG_ATTACK) - if(!thrower || get_dist(thrower, hit_atom) >= 12) //if you hit someone offscreen, which can't be done without legendary. + if(!thrower || get_dist(thrower, hit_atom) >= 12) //if you hit someone offscreen, which can't be done without legendary or backpedaling. thrower.playsound_local(thrower, 'sound/items/weapons/homerun.ogg', 75) to_chat(thrower, span_boldnotice("You can't see it, but you've got a hunch you just hit a fantastic shot.")) else if(hit_atom.uses_integrity) // sorry about the window ma'am 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 9ecd4e89c3f478..44d2900933bb14 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/strider.dm @@ -13,7 +13,8 @@ // how much xp we start with on average. Since the prerequisite skill gives journeyman, we subtract that. var/starting_xp_base = SKILL_EXP_MASTER - SKILL_EXP_JOURNEYMAN -/datum/power/expert/strider/add() +/datum/power/expert/strider/post_add() + ..() power_holder.add_movespeed_mod_immunities(src, /datum/movespeed_modifier/equipment_speedmod) power_holder.mind?.adjust_experience(/datum/skill/athletics, starting_xp_base) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm index 4d883ca734bae5..74b769e9b82c17 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm @@ -23,17 +23,38 @@ name = "Dual Wield" desc = "Toggle dual-wielding. While active, melee attacks immediately follow with an off-hand strike (each strike has a 30% miss chance)." button_icon = 'icons/mob/actions/actions_cult.dmi' - button_icon_state = "equip" + button_icon_state = "dagger" // Chance that we miss a swing var/dual_wield_miss_chance = 30 + /// Overlay for mirrored icon when active. + var/mutable_appearance/dual_wield_overlay /datum/action/cooldown/power/warfighter/dual_wielder/use_action(mob/living/user, atom/target) active = !active user.balloon_alert(user, active ? "dual wield on" : "dual wield off") - build_all_button_icons(UPDATE_BUTTON_STATUS) + build_all_button_icons(UPDATE_BUTTON_OVERLAY | UPDATE_BUTTON_STATUS) return TRUE +// Adds a mirrored overlay to the power button when dual wield is active. +/datum/action/cooldown/power/warfighter/dual_wielder/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force = FALSE) + ..() + + if(!active || !button_icon || !button_icon_state) + if(dual_wield_overlay) + current_button.cut_overlay(dual_wield_overlay) + dual_wield_overlay = null + return + + if(dual_wield_overlay) + current_button.cut_overlay(dual_wield_overlay) + + dual_wield_overlay = mutable_appearance(icon = button_icon, icon_state = button_icon_state) + var/matrix/flip_matrix = matrix() + flip_matrix.Scale(-1, 1) + dual_wield_overlay.transform = flip_matrix + current_button.add_overlay(dual_wield_overlay) + /datum/action/cooldown/power/warfighter/dual_wielder/Grant(mob/granted_to) . = ..() RegisterSignal(granted_to, COMSIG_MOB_ITEM_ATTACK, PROC_REF(on_melee_attack)) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm index e6fa2eb32f55f2..a82c50a4636086 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm @@ -54,7 +54,7 @@ else if(istype(area, /area/station/service/chapel) || prob(check_how_religious(user))) // If you're in the chapel or if fate aligns. if(cap_warning_given) continue - adjust_piety(THEOLOGIST_PIETY_TRIVIAL) + adjust_piety(1) to_chat(user, span_notice("You feel more pious after your prayer.")) else keep_going = FALSE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm index 7420b820596f49..86143a5e4c6d41 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm @@ -152,8 +152,8 @@ if(!isliving(target)) return - on_hit(target, user, get_dir(source, target)) - Detach(source) + if(on_hit(target, user, get_dir(source, target))) + Detach(source) /// triggered after a projectile hits something @@ -162,16 +162,20 @@ if(!isliving(target)) return - on_hit(target, null, angle2dir(Angle)) - Detach(fired_from) + if(on_hit(target, null, angle2dir(Angle))) + Detach(fired_from) // The on hit effect /datum/element/theologist_smite/proc/on_hit(mob/living/target, mob/thrower, throw_dir) //Knockback code if(!ismovable(target) || throw_dir == null) - return + return FALSE if(target.anchored && !throw_anchored) - return + return FALSE + // Magic immune + if(target.can_block_resonance(1)) + // deliberately eats your smite. + return TRUE if(throw_distance < 0) throw_dir = REVERSE_DIR(throw_dir) throw_distance *= -1 @@ -181,6 +185,7 @@ playsound(target, 'sound/effects/magic/magic_block_holy.ogg', 75, TRUE) target.adjustFireLoss(smite_damage) to_chat(target, span_userdanger("You are knocked back by a burning, resonant energy!")) + return TRUE // The on dispel effect /datum/element/theologist_smite/proc/on_dispel(atom/source, atom/dispeller) From 0d0be896071b9692309877d2c4fdee29f1e97fd2 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 21 Mar 2026 10:08:33 +0100 Subject: [PATCH 152/212] Tweaks explosives specialist and grenade sto work with other gernades (signals was missing) --- code/game/objects/items/grenades/_grenade.dm | 4 +-- .../objects/items/grenades/chem_grenade.dm | 1 + .../components/grenade_components.dm | 30 ++++++++++--------- .../warfighter/explosives_specialist.dm | 1 - 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/code/game/objects/items/grenades/_grenade.dm b/code/game/objects/items/grenades/_grenade.dm index 4f8f1cd153e708..4c0ea375406a1d 100644 --- a/code/game/objects/items/grenades/_grenade.dm +++ b/code/game/objects/items/grenades/_grenade.dm @@ -62,8 +62,8 @@ /obj/item/grenade/Initialize(mapload) . = ..() ADD_TRAIT(src, TRAIT_ODD_CUSTOMIZABLE_FOOD_INGREDIENT, type) - AddComponent(/datum/component/grenade_timer_hud) // DOPPLER ADDITION - Display timers for Explosives Specialists (and ghosts) - AddComponent(/datum/component/grenade_timer_registry) // DOPPLER ADDITION - Register grenades for global specialist view + AddComponent(/datum/component/grenade_timer_hud) // DOPPLER ADDITION - Display timers in hand for Explosive Specialist + AddComponent(/datum/component/grenade_timer_ground) // DOPPLER ADDITION - Display timers on ground for Observers & Explosives Specialist RegisterSignal(src, COMSIG_ITEM_USED_AS_INGREDIENT, PROC_REF(on_used_as_ingredient)) /obj/item/grenade/suicide_act(mob/living/carbon/user) diff --git a/code/game/objects/items/grenades/chem_grenade.dm b/code/game/objects/items/grenades/chem_grenade.dm index 98b7edf19ae2f6..56c4a54028d9aa 100644 --- a/code/game/objects/items/grenades/chem_grenade.dm +++ b/code/game/objects/items/grenades/chem_grenade.dm @@ -257,6 +257,7 @@ to_chat(user, span_warning("You prime [src]! [DisplayTimeText(det_time)]!")) active = TRUE + SEND_SIGNAL(src, COMSIG_GRENADE_ARMED, det_time, delayoverride) // DOPPLER ADDITION: Why do we not send this signal?! Used by explosives specialist and a dozen other things. update_icon_state() playsound(src, grenade_arm_sound, volume, grenade_sound_vary) if (landminemode) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm index 6d59f07c511f79..55ebe9770cd7c3 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/components/grenade_components.dm @@ -1,6 +1,8 @@ /** * Shows a live detonation countdown on the grenade's hand HUD icon. * Visible to explosives specialists and to observers watching the holder. + * Spread into two parts: grenade_timer_hud is the inhand timer, grenade_timer_ground is the ground timer. + * The global manager centralizes countdown math; components handle their own visuals. */ /datum/component/grenade_timer_hud var/obj/item/grenade/parent_grenade @@ -137,6 +139,20 @@ current_viewers = list() QDEL_NULL(timer_hud) +/** + * Registers armed grenades with the global timer manager. + */ +/datum/component/grenade_timer_ground +/datum/component/grenade_timer_ground/RegisterWithParent() + RegisterSignal(parent, COMSIG_GRENADE_ARMED, PROC_REF(on_armed)) + +/datum/component/grenade_timer_ground/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_GRENADE_ARMED) + +/datum/component/grenade_timer_ground/proc/on_armed(datum/source, det_time, delayoverride) + SIGNAL_HANDLER + GLOB.grenade_timer_manager.register_grenade(source, det_time, delayoverride) + /** * The part 2 that's respnsible for on the ground timers. @@ -275,17 +291,3 @@ GLOBAL_DATUM_INIT(grenade_timer_manager, /datum/grenade_timer_manager, new) for(var/mob/M in viewer_images) remove_image(M, G) - -/** - * Registers armed grenades with the global timer manager. - */ -/datum/component/grenade_timer_registry -/datum/component/grenade_timer_registry/RegisterWithParent() - RegisterSignal(parent, COMSIG_GRENADE_ARMED, PROC_REF(on_armed)) - -/datum/component/grenade_timer_registry/UnregisterFromParent() - UnregisterSignal(parent, COMSIG_GRENADE_ARMED) - -/datum/component/grenade_timer_registry/proc/on_armed(datum/source, det_time, delayoverride) - SIGNAL_HANDLER - GLOB.grenade_timer_manager.register_grenade(source, det_time, delayoverride) 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 c81417796e4cbc..a30b47071389ed 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 @@ -8,4 +8,3 @@ mob_trait = TRAIT_POWER_EXPLOSIVES_SPECIALIST // See modular_doppler\modular_powers\code\powers\mortal\warfighter\components\grenade_components.dm for how we add the timers -// TODO: Make it work with c4 as well. From ca2c74d2ab9d6073f0401159920cf58cf3f21aa7 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 23 Mar 2026 07:52:34 +0100 Subject: [PATCH 153/212] Adds aim assist (targets the appropriate item on the tile). Fixes precog eyes animations. Fixes reagent cannon being useable while out of uses. Bloodhound shows a different icon for people on dfiferent Zs. Shapechange is more restricted + less buggy. Fixed a qdel loop in theologist shared. --- .../mortal/augmented/precognition_eyes.dm | 5 ++-- .../powers/mortal/augmented/reagent_cannon.dm | 4 +-- .../powers/resonant/aberrant/bloodhound.dm | 4 +++ .../powers/resonant/aberrant/shapechange.dm | 30 ++++++++++++++++++- .../theologist/_theologist_root_shared.dm | 6 +++- .../modular_powers/code/powers_action.dm | 18 +++++++++++ .../code/security/reality_anchor.dm | 4 +-- 7 files changed, 63 insertions(+), 8 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm index 2c8df789c31328..978e2f1423aa58 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm @@ -145,5 +145,6 @@ premium_component?.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) source.visible_message(span_warning("[source] dodges the [proj] with little effort!"), span_danger("You automatically dodge the [proj]!")) - addtimer(TRAIT_CALLBACK_REMOVE(source, TRAIT_UNHITTABLE_BY_PROJECTILES, AUGMENTATION_TRAIT), 0.2 SECONDS) - return PROJECTILE_INTERRUPT_HIT + addtimer(TRAIT_CALLBACK_REMOVE(source, TRAIT_UNHITTABLE_BY_PROJECTILES, AUGMENTATION_TRAIT), 0.1 SECONDS) + source.block_projectile_effects() // does all the vfx + return PROJECTILE_INTERRUPT_HIT_PHASE diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm index c5375ba6c73091..3459ddca3ddc90 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm @@ -160,8 +160,8 @@ // Allows us to basically toggle between 1x or 3x spray. /obj/item/reagent_containers/spray/chemsprayer/reagent_cannon/spray(atom/A, mob/user) - if(!premium_component?.can_function()) - to_chat(owner, span_warning("Your [name] fails to respond; it seems broken!")) + if(!host_implant?.premium_component.can_function()) + to_chat(user, span_warning("Your [name] fails to respond; it seems broken!")) return FALSE var/turf/target_turf = get_turf(A) if(focused_mode) 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 746e4bf96cac02..00e23847d5ed97 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/bloodhound.dm @@ -160,6 +160,10 @@ var/turf/here = get_turf(owner) var/turf/there = get_turf(target) if(!here || !there || here.z != there.z) + if(linked_alert) + linked_alert.icon = 'icons/effects/landmarks_static.dmi' + linked_alert.icon_state = "x" + linked_alert.dir = SOUTH return var/dir_to_target = get_dir(here, there) 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 9a07afa83ec49c..4170cfe7b14b43 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm @@ -97,6 +97,11 @@ // Allow reverting even if starving. if(user.has_status_effect(/datum/status_effect/shapechange_mob/aberrant)) return TRUE + // We shouldn't have any active powers because it will make this power 10x more glitchy. This checks against it. + var/datum/action/cooldown/power/blocking_power = get_blocking_active_power(user) + if(blocking_power) + owner.balloon_alert(user, "active: [blocking_power.name]") + return FALSE if(user.nutrition <= NUTRITION_LEVEL_STARVING) owner.balloon_alert(user, "too hungry!") return FALSE @@ -158,6 +163,9 @@ animate(user, transform = matrix(), time = 0, easing = SINE_EASING) user.transform = old_transform return FALSE + // Restore the original transform after the animation sequence completes. + animate(user, transform = old_transform, time = 0, easing = SINE_EASING) + user.transform = old_transform user.visible_message(span_warning("[user]'s body rearranges itself with a horrible crunching sound!")) playsound(user, 'sound/effects/magic/demon_consume.ogg', 50, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) return TRUE @@ -174,6 +182,8 @@ if(!ispath(shape_type)) return null var/mob/living/new_shape = new shape_type(user.loc) + // Ensure the new form inherits the caster's languages while keeping its existing ones. + new_shape.get_language_holder().copy_languages(user.get_language_holder()) apply_shape_identifier(new_shape) return new_shape @@ -197,11 +207,27 @@ return shape_type return GLOB.shapechange_form_types["Parrot"] +// Returns the first active power action that should block shapechanging. +/datum/action/cooldown/power/aberrant/shapechange/proc/get_blocking_active_power(mob/living/user) + if(!user || !user.powers) + return null + for(var/datum/power/power as anything in user.powers) + var/datum/action/cooldown/power/power_action = power.action_path + if(!istype(power_action)) + continue + if(power_action == src) + continue + if(power_action.active) + return power_action + return null + //Shapechange status effect for aberrant power. We make our own to prevent gibbed RR. /datum/status_effect/shapechange_mob/aberrant id = "shapechange_aberrant" /// The power action that caused the change var/datum/weakref/source_weakref + /// Cached transform of the caster so we can restore non-default scaling (e.g. undersized/oversized). + var/matrix/caster_transform /// Whether the shifted body was gibbed when it died var/last_gibbed = FALSE /// Whether the revert was manually triggered. @@ -220,6 +246,8 @@ var/datum/action/cooldown/power/aberrant/shapechange/source_action = source_weakref.resolve() if(!QDELETED(source_action) && source_action.owner == caster_mob) source_action.Grant(owner) + if(caster_mob) + caster_transform = matrix(caster_mob.transform) return ..() /datum/status_effect/shapechange_mob/aberrant/restore_caster(kill_caster_after) @@ -250,7 +278,7 @@ return // Ensure any transform scaling from the shift animation is cleared. - caster_mob.transform = matrix() + caster_mob.transform = caster_transform ? caster_transform : matrix() // Transfer damage from the shifted body back to the caster. var/damage_mult = manual_revert ? 0.5 : 1 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index 7c5b834902fc4a..af926a36b6aec1 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -92,7 +92,10 @@ // gets rid of the beam if(current_beam) UnregisterSignal(current_beam, COMSIG_QDELETING) - QDEL_NULL(current_beam) + if(!QDELETED(current_beam)) // prevents a qdel loop because clear_link from walking away also deletes it + QDEL_NULL(current_beam) + else + current_beam = null // gets rid of the target's glow if(target_glow) current_target.cut_overlay(target_glow) @@ -115,6 +118,7 @@ */ /datum/action/cooldown/power/theologist/theologist_root/shared/proc/beam_died() SIGNAL_HANDLER + current_beam = null clear_link() /** diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 58eb6584030249..497468e35826a7 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -43,6 +43,8 @@ var/target_range /// If set, clicked target MUST be of this type (or subtype). var/target_type + /// If aim assist is used for click targeting. Disable to disable. + var/aim_assist = TRUE /// Do we check for anti magic on the target when we target them? Basically if your action targets but doesn't do anything directly magical to them immediately (like projectiles), this should be false. var/anti_magic_on_target = TRUE @@ -169,6 +171,10 @@ Handles all the logic involved in using a targeted, click-based action. return FALSE if(!target) return FALSE + if(aim_assist) + var/atom/aim_assist_target = aim_assist(clicker, target, target_type) + if(aim_assist_target) + target = aim_assist_target // Checks if we are allowed to actually target that type. if(target_type && !istype(target, target_type)) @@ -202,6 +208,18 @@ Handles all the logic involved in using a targeted, click-based action. clicker.next_click = world.time + click_cd_override return TRUE +// Optional aim assist for click targeting. Override for custom behavior. +/datum/action/cooldown/power/proc/aim_assist(mob/living/clicker, atom/target, target_type_path) + if(!isturf(target)) + return + + // If we have a specific type we're targeting, we're targeting that instead. + if(target_type_path) + return locate(target_type_path) in target + + // Otherwise, find any human, or if that fails, any living target + return locate(/mob/living/carbon/human) in target || locate(/mob/living) in target + // We override the click abilities to fix an issue with the active_overlay_icon_state not appearing when it should. /datum/action/cooldown/power/set_click_ability(mob/on_who) . = ..() diff --git a/modular_doppler/modular_powers/code/security/reality_anchor.dm b/modular_doppler/modular_powers/code/security/reality_anchor.dm index a0883f1ab2871b..910e8da038818f 100644 --- a/modular_doppler/modular_powers/code/security/reality_anchor.dm +++ b/modular_doppler/modular_powers/code/security/reality_anchor.dm @@ -72,7 +72,7 @@ if(!center) return var/obj/effect/temp_visual/circle_wave/reality_anchor/pulse_fx = new(center) - pulse_fx.amount_to_scale = pulse_range + 1 // falls short without the +1 + pulse_fx.amount_to_scale = pulse_range + 2 // falls short without the +1 // We get EVERYTHING in range and dispel it. This shouldn't be too much of a lag-machine (hopefully) for(var/atom/movable/target in range(pulse_range, center)) if(ismob(target)) @@ -121,7 +121,7 @@ color = COLOR_SILVER max_alpha = 20 duration = 0.5 SECONDS - amount_to_scale = 6 + amount_to_scale = 7 /obj/structure/reality_anchor/update_overlays() . = ..() From 30674393f4a3a030c0522d172288389f9bfff911 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 23 Mar 2026 09:24:22 +0100 Subject: [PATCH 154/212] Cargo orders for reality anchors + thaumaturgic supplies. --- .../code/cargo/reality_anchor.dm | 8 ++ .../code/cargo/thaumaturgic_supplies.dm | 76 +++++++++++++++++++ .../affinity/thaumaturge_spell_focus.dm | 1 - .../code/security/reality_anchor.dm | 1 + tgstation.dme | 2 + 5 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 modular_doppler/modular_powers/code/cargo/reality_anchor.dm create mode 100644 modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm diff --git a/modular_doppler/modular_powers/code/cargo/reality_anchor.dm b/modular_doppler/modular_powers/code/cargo/reality_anchor.dm new file mode 100644 index 00000000000000..ba8a0687eb4f55 --- /dev/null +++ b/modular_doppler/modular_powers/code/cargo/reality_anchor.dm @@ -0,0 +1,8 @@ +/datum/supply_pack/security/reality_anchor + name = "Reality Anchor Crate" + desc = "A miniature reality anchor for suppressing resonant phenomena." + cost = CARGO_CRATE_VALUE * 20 + access_any = list(ACCESS_SECURITY) + contains = list(/obj/structure/reality_anchor) + crate_name = "reality anchor crate" + crate_type = /obj/structure/closet/crate/secure/weapon diff --git a/modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm b/modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm new file mode 100644 index 00000000000000..4fd9e8c0e061e0 --- /dev/null +++ b/modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm @@ -0,0 +1,76 @@ +/datum/supply_pack/costumes_toys/thaumaturgic + name = "Thaumaturgic Crate" + desc = "Contains 3 spell focusi for Thaumaturges to wield; plus 3 additional sets of random (discount) robes and hats to help the proccess." + cost = CARGO_CRATE_VALUE * 5 + contains = list( + /obj/item/spell_focus = 3, + /obj/item/clothing/head/wizard/fake = 3, + /obj/item/clothing/suit/wizrobe/fake = 3, + ) + crate_name = "thaumaturge crate" + crate_type = /obj/structure/closet/crate/wooden + + var/num_hats = 3 + var/num_robes = 3 + var/list/hat_pool = list( + /obj/item/clothing/head/wizard/fake, + /obj/item/clothing/head/costume/witchwig, + /obj/item/clothing/head/collectable/wizard, + /obj/item/clothing/head/wizard/marisa/fake, + /obj/item/clothing/head/wizard/tape/fake, + /obj/item/clothing/head/wizard/chanterelle, + ) + var/list/robe_pool = list( + /obj/item/clothing/suit/wizrobe/fake, + /obj/item/clothing/suit/wizrobe/marisa/fake, + /obj/item/clothing/suit/wizrobe/tape/fake, + ) + + // There's a small chance that we manage to sneak in real wizard robes. + var/real_robe_set_chance = 3 + var/list/real_robe_sets = list( + list( + /obj/item/clothing/suit/wizrobe/magusblue, + /obj/item/clothing/head/wizard/magus, + ), + list( + /obj/item/clothing/head/wizard/magus, + /obj/item/clothing/suit/wizrobe/magusblue, + ), + list( + /obj/item/clothing/head/wizard/black, + /obj/item/clothing/suit/wizrobe/black, + ), + list( + /obj/item/clothing/head/wizard/tape, + /obj/item/clothing/suit/wizrobe/tape, + ), + list( + /obj/item/clothing/head/wizard/santa, + /obj/item/clothing/suit/wizrobe/santa, + ), + list( + /obj/item/clothing/head/wizard, + /obj/item/clothing/suit/wizrobe, + ), + ) +// Fills it with at least 3 spell focuses and a random selection of hats and robes. +/datum/supply_pack/costumes_toys/thaumaturgic/fill(obj/structure/closet/crate/C) + for(var/spawn_index in 1 to 3) + new /obj/item/spell_focus(C) + + // chance for real robes + if(prob(real_robe_set_chance)) + var/list/selected_set = pick(real_robe_sets) + for(var/robe_item_type in selected_set) + new robe_item_type(C) + + var/list/hats = hat_pool.Copy() + for(var/spawn_index in 1 to min(num_hats, length(hats))) + var/hat_type = pick_n_take(hats) + new hat_type(C) + + var/list/robes = robe_pool.Copy() + for(var/spawn_index in 1 to min(num_robes, length(robes))) + var/robe_type = pick_n_take(robes) + new robe_type(C) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm index 5412364efbbb5c..335e47db28652a 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm @@ -34,7 +34,6 @@ /obj/item/spell_focus/proc/on_spell_focus_picked(obj/item/spell_focus/new_focus, mob/living/picker) if(!istype(new_focus)) return - new_focus.on_selected(src, picker) /obj/item/spell_focus/proc/on_selected(obj/item/spell_focus/old_focus, mob/living/picker) diff --git a/modular_doppler/modular_powers/code/security/reality_anchor.dm b/modular_doppler/modular_powers/code/security/reality_anchor.dm index 910e8da038818f..99cd6c499da114 100644 --- a/modular_doppler/modular_powers/code/security/reality_anchor.dm +++ b/modular_doppler/modular_powers/code/security/reality_anchor.dm @@ -4,6 +4,7 @@ icon = 'modular_doppler/modular_powers/icons/items/reality_anchor.dmi' icon_state = "reality_anchor" density = TRUE + max_integrity = 600 // tonky // Is it on/off var/active = FALSE diff --git a/tgstation.dme b/tgstation.dme index ee7965d955ff1a..3738316ce25165 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7430,6 +7430,8 @@ #include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm" #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" +#include "modular_doppler\modular_powers\code\cargo\reality_anchor.dm" +#include "modular_doppler\modular_powers\code\cargo\thaumaturgic_supplies.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_action.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_premium_augment.dm" From 29c0230cc169ae7531b423e94d384b0365a1a829 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 23 Mar 2026 09:36:58 +0100 Subject: [PATCH 155/212] Adds security text to sorcerous paths. --- .../code/powers/sorcerous/thaumaturge/_thaumaturge_root.dm | 2 +- .../code/powers/sorcerous/thaumaturge/blend_for_me.dm | 1 + .../code/powers/sorcerous/thaumaturge/brazen_bindings.dm | 2 ++ .../code/powers/sorcerous/thaumaturge/conjure_rain.dm | 2 ++ .../code/powers/sorcerous/thaumaturge/gale_blast.dm | 2 ++ .../code/powers/sorcerous/thaumaturge/magic_barrage.dm | 2 ++ .../code/powers/sorcerous/thaumaturge/phantasmal_tool.dm | 2 ++ .../code/powers/sorcerous/thaumaturge/sanguine_absorption.dm | 1 + .../code/powers/sorcerous/thaumaturge/vitalize_flora.dm | 1 + .../powers/sorcerous/theologist/_theologist_root_revered.dm | 2 ++ .../code/powers/sorcerous/theologist/_theologist_root_shared.dm | 2 ++ .../powers/sorcerous/theologist/_theologist_root_twisted.dm | 2 ++ .../code/powers/sorcerous/theologist/divine_protection.dm | 2 ++ .../code/powers/sorcerous/theologist/entropic_mending.dm | 1 + .../code/powers/sorcerous/theologist/pious_prayer.dm | 1 + .../modular_powers/code/powers/sorcerous/theologist/purify.dm | 2 ++ .../code/powers/sorcerous/theologist/smiting_strike.dm | 2 ++ .../code/powers/sorcerous/theologist/smiting_strike_upgrades.dm | 2 ++ 18 files changed, 30 insertions(+), 1 deletion(-) 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 a5763e70c12858..83ed9819c1feac 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 @@ -2,7 +2,7 @@ /datum/power/thaumaturge_root name = "Spell Preparation" desc = "Grants you a Spell Focus, an unique item that allows you to charge your Thaumaturge spells while sleeping, and enhance them by holding it. Use the Spell Focus in your hand to change it's form." - + security_record_text = "Subject is capable of performing feats of thaumaturgic magic while in possession of a spell focus." action_path = /datum/action/cooldown/power/thaumaturge/thaumaturge_root value = 3 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm index 6876992b2edc8d..70f268e49d668e 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm @@ -5,6 +5,7 @@ name = "Blend For Me" desc = "Grinds the item in your hand as if it were inserted in a grinder, then conjures a glass to hold it (if you're grinding). Right-hand for grinding, left-hand for juicing. Can be used on people using an aggressive grab to inflict brute damage and bleeding. \ Requires Affinity 1. Affinity gives a chance to not consume charges." + security_record_text = "Subject can magically blend drinks, objects and people with their bare hands." value = 2 action_path = /datum/action/cooldown/power/thaumaturge/blend_for_me diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm index fe709cc8abe29e..7b488a6c468793 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm @@ -5,6 +5,8 @@ /datum/power/thaumaturge/brazen_bindings name = "Brazen Bindings" desc = "Summons a set of manacles made from brass, capable of dispelling and disabling Resonant powers on the bound target. The magic that made them is fragile, causing them to break once someone escapes. Requires Affinity 1. Additional affinity increases the time it takes to break out." + security_record_text = "Subject can conjure anti-resonant manacles out of thin air." + security_threat = POWER_THREAT_MAJOR value = 3 action_path = /datum/action/cooldown/power/thaumaturge/brazen_bindings diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm index 805ecdc09d49f1..dd4ad15b4bf526 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm @@ -4,6 +4,8 @@ desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u water, up to a maximum of 60u spread out across all containers. Mobs are splashed with the same amount and don't count towards this limit. \ \n Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ \n Requires Affinity 3. Higher affinity increases the max amount of spreadable reagents by 20u." + security_record_text = "Subject can conjure rains with varying chemical properties." + security_threat = POWER_THREAT_MAJOR value = 4 action_path = /datum/action/cooldown/power/thaumaturge/conjure_rain 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 1df5373cac9fc9..7168b784854b24 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 @@ -9,6 +9,8 @@ /datum/power/thaumaturge/gale_blast name = "Gale Blast" desc = "Shoots forth a blast of wind. The blast keeps traveling until it hits a solid structure, extinguishing any fires and dragging along any items with it. If it hits a creature, it knocks them back 3 spaces and extinguishes them. Requires Affinity 3. Extra affinity gives a chance to knockback further." + security_record_text = "Subject can create and shoot out strong, violent gusts of wind." + security_threat = POWER_THREAT_MAJOR value = 3 action_path = /datum/action/cooldown/power/thaumaturge/gale_blast 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 5c09f9deec6142..07e28bac7d784c 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 @@ -6,6 +6,8 @@ /datum/power/thaumaturge/magical_barrage name = "Magical Barrage" desc = "Shoots a volley of magic projectiles equal to your Affinity + 2. You can either fire single shots with a short delay between shots, or shoot all your remaining shots in a barrage. Requires Affinity 3." + security_record_text = "Subject can conjure and shoot a volley of magical lasers." + security_threat = POWER_THREAT_MAJOR value = 5 action_path = /datum/action/cooldown/power/thaumaturge/magical_barrage diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm index 075c2a4ef0d3c5..120ddf3275c564 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm @@ -4,6 +4,8 @@ /datum/power/thaumaturge/phantasmal_tool name = "Phantasmal Tool" desc = "Summons a basic tool of your choice in your hand, that disappears after a duration, or if it is dropped/used to attack a person. Requires Affinity 1 to cast. Affinity gives a chance to not consume charges on cast." + security_record_text = "Subject can conjure ephemeral tools out of thin air." + security_threat = POWER_THREAT_MAJOR value = 3 action_path = /datum/action/cooldown/power/thaumaturge/phantasmal_tool diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm index 88fcedb39fc8f7..f249ae99b98f9c 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm @@ -6,6 +6,7 @@ desc = "You draw nearby blood into the target. This draws up to 100u of blood from adjacent floor/wall splatters, containers and other mobs (in that order). It then tranfers that blood to the target and converts it to universally accepted blood.\ \nAny excess blood in the target creature beyond 100% is transformed into healing, at a 10u per 4 damage ratio. This can only heal organic bodyparts and does not heal any damage-types besides Brute or Burn. This also does not affect creatures with non-blood bloodtypes such as Ethereals or Slimepeople. \ \nRequires Affinity 3. Additional affinity increases the healing ratio by 0.5 per point" + security_record_text = "Subject can draw blood from varying sources (including humanoids) and transmute it into universal blood, potentially healing the target." value = 5 action_path = /datum/action/cooldown/power/thaumaturge/sanguine_absorption diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm index 609218d394a967..4d3cad3347b1e5 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm @@ -5,6 +5,7 @@ name = "Vitalize Flora" desc = "Breathes life into the plants around you. This heals any and all plants (including plant creatures), makes them grow if they're still in the growth phase, and speeds up the time until the next harvest. \ Requires Affinity 1. Affinity gives a chance to not consume charges." + security_record_text = "Subject can magically heal and grow plantlife around it." value = 2 action_path = /datum/action/cooldown/power/thaumaturge/vitalize_flora diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index ab966ad6680ca0..ac12abd82fd41f 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -4,6 +4,8 @@ desc = "Nullifies pain and slowly heals the targeted creature over a prolonged period of time. This may be yourself. \ Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again. \ This is mutually exclusive with the other 'A Burden...' powers." + security_record_text = "Subject can magically mend their own wounds and the wounds of others slowly over a long duration." + security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/theologist/theologist_root/revered value = 5 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index af926a36b6aec1..e335eb48fc6994 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -3,6 +3,8 @@ desc = "Channels a beam of energy between you and a target, equalizing damage over a period of time, scaling with severity. \ The beam requires continous line of sight to function, and neither you or your target can be incapacitated. Generates Piety if you are transfering damage to yourself. \ This is mutually exclusive with the other 'A Burden...' powers." + security_record_text = "Subject can transfer the injuries of a target onto themselves, or visa versa." + security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/theologist/theologist_root/shared value = 5 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index d5d33d1c3d3548..54b87f8393df04 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -3,6 +3,8 @@ desc = "Channel chaotic energies into another creature next to you. The target is healed over time in random amounts up to the maximum, then damaged for half that amount in random damage types. \ Gives Piety proportional to the amount of damage twisted. \ This is mutually exclusive with the other 'A Burden...' powers." + security_record_text = "Subject can rapidly transmute the wounds of a target into smaller, insubstantial wounds." + security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/theologist/theologist_root/twisted value = 5 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 ee3d41e011e0ba..6a703c5cdcc60f 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 @@ -5,6 +5,8 @@ /datum/power/theologist/divine_protection name = "Divine Protection" desc = "You gain a block chance (separate from all other block chance) equal to half your piety; reduce Piety by 5 when this triggers." + security_record_text = "Subject tends to unpredictably and miraculously avoid harm." + security_threat = POWER_THREAT_MAJOR value = 4 required_powers = list(/datum/power/theologist_root/) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm index ab181ee3164acc..6b364f55e36e3a 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm @@ -7,6 +7,7 @@ Entropic Mending removes wounds (sometimes) and speeds up the target's metabolis desc = "Entropy's a long road, a few steps further along it will do you more good than harm. Spend 5 Piety to touch another humanoid and attempt to restore it's lingering wounds. \ Moderate wounds will be healed automatically; all other wounds have a random chance to depending on severity. \ Invoking this power will cause temporary, lingering entropic effects on the target; such as increased metabolism, hunger and blood replenishment, at triple pace." + security_record_text = "Subject can accelerate a target's bodily functions (e.g metabolism) to be thrice as fast, and mend lingering wounds." action_path = /datum/action/cooldown/power/theologist/entropic_mending value = 6 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm index a82c50a4636086..a02bcbf25e0fb0 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm @@ -20,6 +20,7 @@ /datum/action/cooldown/power/theologist/pious_prayer name = "Pious Prayer" desc = "Perform a small prayer. If you are in the Chapel, this grants you Piety unless you have 5 or more Piety. Performing prayers elsewhere only has a small chance to grant Piety. Being religious increases the efficiency of this skill." + security_record_text = "Subject fuels their powers with visits to the Chapel." button_icon = 'icons/obj/antags/cult/structures.dmi' button_icon_state = "tomealtar" cooldown_time = 5 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm index 8ea3a893325869..b9f27259ddc214 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/purify.dm @@ -10,6 +10,8 @@ name = "Purify" desc = "Cleanses impurity from objects and creatures in melee range. The chosen target is immediately dispelled and purified of all poisons. \ If the target is an object with a holy equivelant, it turns it into that (e.g water into holy water). Has varying piety costs, but usually defaults to 5." + security_record_text = "Subject can end magical effects on a target, nullify poisons and transmute objects into their holy variants with a touch." + security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/theologist/purify value = 5 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm index 86143a5e4c6d41..9d05b5fa812710 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm @@ -2,6 +2,8 @@ name = "Smiting Strike" desc = "Channel energy into the item you are currently holding. Your next attack that hits with it against a creature deals 15 additional burn damage and sends them flying backwards 4 spaces. \ This knockback cannot stun or damage on impact. Costs 5 Piety to use. This effect ends if the item leaves your hands." + security_record_text = "Subject can bless their own weapons to knock back foes and sear their bodies." + security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/theologist/smiting_strike value = 4 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm index dfa51b25d220bf..d7257bc0bd317b 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike_upgrades.dm @@ -4,6 +4,8 @@ Most of the effects are already baked into the existing power for convenience. /datum/power/theologist/imbue_armaments name = "Imbue Armaments" desc = "Changes Smiting Strike to no longer be removed when it passes hands, and allows you to have an unlimited amount of items blessed. Reduces the smite effect's knockback by 2 and damage by 5." + security_record_text = "Subject can bless the weapons of others to enhance their lethality." + security_threat = POWER_THREAT_MAJOR value = 3 required_powers = list(/datum/power/theologist/smiting_strike) From 27ec75df6df3bb93df2174c69b624d1002c5bb22 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 23 Mar 2026 17:53:07 +0100 Subject: [PATCH 156/212] Tidies up nuke power prefs and catches a few more edge cases I ran into. Adds normal and area telepathy. Removed the silly thing that made it so that mental powers for psyker couldnt be used if you were mental immune. --- .../powers/resonant/psyker/_psyker_action.dm | 34 ++-- .../powers/resonant/psyker/_psyker_root.dm | 12 +- .../code/powers/resonant/psyker/telepathy.dm | 155 ++++++++++++++++++ .../powers/resonant/psyker/telepathy_area.dm | 22 +++ .../sorcerous/theologist/pious_prayer.dm | 2 +- .../modular_powers/code/powers_prefs.dm | 52 +++++- .../code/powers_prefs_middleware.dm | 3 + .../modular_powers/code/powers_subsystem.dm | 1 + tgstation.dme | 2 + 9 files changed, 251 insertions(+), 32 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm 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 5dd6b409144095..211d474a063abb 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 @@ -7,12 +7,12 @@ // The organ that processes most of the Psyker Powers. Mostly all functions here communicate with this. var/obj/item/organ/resonant/psyker/psyker_organ - // Disabled by the trait - var/disabled_by_mental_immunity = TRUE - //If the spell (flavorwise) affects the target's mind. So this should be FALSE for things like telekinesis but TRUE for mind reading. var/mental = TRUE + // charge cost on antimagic powers. If it has a cooldown and is non-spamable then this should be 1; otherwise keep it as is. 0 means the target isn't made aware they get targeted as well. + var/antimagic_charge_cost = 0 + /datum/action/cooldown/power/psyker/New() . = ..() ValidateOrgan() @@ -26,29 +26,27 @@ return TRUE // This doesn't actually add the stress itself; it merely tells the organ to add the stress. -// Why not handle it here? Because why give them an organ if we're not going to use it?! // Validation is handled on the organ side. -/datum/action/cooldown/power/psyker/proc/modify_stress(amount) - psyker_organ.modify_stress(amount) +/datum/action/cooldown/power/psyker/proc/modify_stress(amount, override_cap) + psyker_organ.modify_stress(amount, override_cap) // We added checking for organs on try_use, as well as making sure that if we are wearing a tinfoil cap, we can't just wield our psychic powers. /datum/action/cooldown/power/psyker/try_use(mob/living/user, mob/living/target) if(!ValidateOrgan()) owner.balloon_alert(owner, "No paracausal gland!") return FALSE - if(isliving(target)) - if(mental && !can_affect_mental(target)) - modify_stress(PSYKER_STRESS_MINOR) // This actively stresses you out. - owner.balloon_alert(owner, "Something interveres with your powers!") - return FALSE - if(disabled_by_mental_immunity && target.can_block_magic(MAGIC_RESISTANCE_MIND)) - modify_stress(PSYKER_STRESS_MINOR) // This actively stresses you out. - owner.balloon_alert(owner, "Something interveres with your powers!") - return FALSE + // This checks against mental on the target + if(isliving(target) && mental && !can_affect_mental(target, antimagic_charge_cost)) + modify_stress(PSYKER_STRESS_MINOR) + owner.balloon_alert(owner, "The target's mind is unreachable!") + to_chat(owner, span_boldnotice("The target's mind is unreachable!")) + return FALSE . = .. () // Checks if the target can be affected by mental based psyker stuff, since it has its own litle list of unique immunities. Returns TRUE if the target has nothing that affects mental. -/datum/action/cooldown/power/psyker/proc/can_affect_mental(mob/living/target, charge_cost = 0) +/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)) @@ -61,7 +59,9 @@ // Checks if the target can be affected by specifically psyker's scrying /datum/action/cooldown/power/psyker/proc/can_affect_scrying(mob/living/target, charge_cost = 0) - if(!can_affect_mental(target)) + if(!charge_cost) + charge_cost = antimagic_charge_cost + if(!can_affect_mental(target, charge_cost)) return FALSE if(HAS_TRAIT(target, TRAIT_ANTIRESONANCE_SCRYING)) return FALSE 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 bd71ca6f781bc0..4bb0e28ee5a9bf 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 @@ -1,11 +1,12 @@ - +/* + Doesn't do much besides give you a grumpy organ. I prefer it gave a ribbon or at least some sort of positive, but I suppose the path of a psyker is to suffer. +*/ /datum/power/psyker_root name = "Paracausal Gland" - desc = "An organ found only in the central nervous system of Psykers \ - grown by prolonged exposure to certain types of Resonance. \ - The catalyst for psychic abilities; but beware overexerting it." + desc = "An organ found only in the central nervous system of Psykers, grown by prolonged exposure to certain types of Resonance. \ + \nThe catalyst for psychic abilities; but beware overexerting it." security_record_text = "Subjects has a Paracausal Gland and wields psionic abilities." - value = 2 + value = 1 power_flags = POWER_HUMAN_ONLY mob_trait = TRAIT_ARCHETYPE_RESONANT archetype = POWER_ARCHETYPE_RESONANT @@ -27,6 +28,5 @@ grant_action(/datum/action/cooldown/power/resonant_meditate) /datum/power/psyker_root/remove(client/client_source) - if(psyker_organ) qdel(psyker_organ) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm new file mode 100644 index 00000000000000..b4cce24f40bf2f --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm @@ -0,0 +1,155 @@ + +// Telepathy, basically lifted from the mutation. + +#define TELE_CLICK_NONE 0 +#define TELE_CLICK_LEFT 1 +#define TELE_CLICK_RIGHT 2 +/datum/power/psyker_power/telepathy + name = "Telepathy" + desc = "Allows you to mentally communicate messages to targets. Generates a petit amount of stress." + security_record_text = "Subject can initiate one-way communication with a target telepathically." + value = 1 + required_powers = list(/datum/power/psyker_root) + action_path = /datum/action/cooldown/power/psyker/telepathy + +/datum/action/cooldown/power/psyker/telepathy + name = "Telepathy" + desc = "Allows you to mentally communicate messages to the target." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "telepathy" + click_to_activate = TRUE + target_self = FALSE + target_range = 12 + target_type = /mob/living + + /// The message we send to the target. + var/message + /// The span surrounding the telepathy message + var/telepathy_span = "notice" + /// The bolded span surrounding the telepathy message + var/bold_telepathy_span = "boldnotice" + /// Whether access to area telepathy (right click) is enabled. + var/aoe_enabled = FALSE + /// Which mouse click is used in use_action + var/tele_click_type = 0 + +/datum/action/cooldown/power/psyker/telepathy/InterceptClickOn(mob/living/clicker, params, atom/target) + var/list/mods = params2list(params) + if(LAZYACCESS(mods, RIGHT_CLICK)) + if(!aoe_enabled) + return FALSE + tele_click_type = TELE_CLICK_RIGHT + // We need a valid living target to proceed, so we basically forcefully get any valid target in range. + target = get_aoe_dummy_target(clicker) + else + tele_click_type = TELE_CLICK_LEFT + . = ..() + if(!.) + tele_click_type = TELE_CLICK_NONE + return TRUE // always return true to consume the click + +/datum/action/cooldown/power/psyker/telepathy/use_action(mob/living/user, atom/target) + // Sets teh click type. + var/click_type = tele_click_type + tele_click_type = TELE_CLICK_NONE + if(click_type == TELE_CLICK_RIGHT) + return send_area_thought(user) + + // define mob and set message + var/mob/living/cast_on = target + message = tgui_input_text(user, "What do you wish to whisper to [cast_on]?", "[src]", max_length = MAX_MESSAGE_LEN) + + // if anything happens before we finish typing the message. + if(QDELETED(src) || QDELETED(user) || QDELETED(cast_on)) + return FALSE + + // out of range + if(target_range && get_dist(user, cast_on) > target_range) + user.balloon_alert(user, "they're too far!") + return FALSE + // no message + if(!message) + return FALSE + + send_thought(user, cast_on, message) + return TRUE + +/datum/action/cooldown/power/psyker/telepathy/on_action_success(mob/living/user, atom/target) + modify_stress(PSYKER_STRESS_TRIVIAL) + return ..() + +// Picks a valid mob in view to satisfy target checks for area telepathy; doubles as a check to see if we even have anyone to telepathy to. +/datum/action/cooldown/power/psyker/telepathy/proc/get_aoe_dummy_target(mob/living/user) + var/list/targets = list() + for(var/mob/living/target in view(user)) + if(target == user) + continue + if(mental && !can_affect_mental(target)) + continue + targets += target + + if(!length(targets)) + return null + return pick(targets) + +// Singular transmission +/datum/action/cooldown/power/psyker/telepathy/proc/send_thought(mob/living/caster, mob/living/target, message, disable_feedback = FALSE) + log_directed_talk(caster, target, message, LOG_SAY, name) + + var/formatted_message = "[message]" + target.balloon_alert(target, "you hear a voice") + to_chat(target, "You hear a voice in your head... [formatted_message]") + + if(!disable_feedback) // So that the AoE version doesnt spam your chat log. + to_chat(caster, "You transmit to [target]: [formatted_message]") + send_ghost_message(caster, target, formatted_message) + + +// AoE transmission +/datum/action/cooldown/power/psyker/telepathy/proc/send_area_thought(mob/living/user) + message = tgui_input_text(user, "What do you wish to whisper to everyone in view?", "[src]", max_length = MAX_MESSAGE_LEN) + if(QDELETED(src) || QDELETED(user)) + return FALSE + if(!message) + return FALSE + + // We need to revalidate targeting on each person; you shouldn't be able to whisper to mental or magic immune people + var/list/targets = list() + for(var/mob/living/target in view(user)) + if(target == user) + continue + if(mental && !can_affect_mental(target)) + continue + targets += target + + if(!length(targets)) + user.balloon_alert(user, "no minds in view!") + return FALSE + + var/formatted_message = "[message]" + to_chat(user, "You broadcast to everyone in view: [formatted_message]") + send_ghost_message(user, null, formatted_message, area_broadcast = TRUE) + + // basically goes through send_thought for each target + for(var/mob/living/target as anything in targets) + send_thought(user, target, message, disable_feedback = TRUE) + return TRUE + +// Tells the ghosts that telepathy talk is happening. +/datum/action/cooldown/power/psyker/telepathy/proc/send_ghost_message(mob/living/caster, mob/living/target, formatted_message, area_broadcast = FALSE) + for(var/mob/dead/ghost as anything in GLOB.dead_mob_list) + if(!isobserver(ghost)) + continue + + var/from_link = FOLLOW_LINK(ghost, caster) + var/from_mob_name = "[caster] [src]" + from_mob_name += ":" + var/to_link = "" + var/to_mob_name + if(area_broadcast) + to_mob_name = span_name("area") + else + to_link = FOLLOW_LINK(ghost, target) + to_mob_name = span_name("[target]") + + to_chat(ghost, "[from_link] [from_mob_name] [formatted_message] [to_link] [to_mob_name]") diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm new file mode 100644 index 00000000000000..d901cd915d2ee8 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm @@ -0,0 +1,22 @@ +/datum/power/psyker_power/telepathy_area + name = "Telepathy Area" + desc = "Allows you to right click with your telepathy power to send the message to all creatures currently in view!" + security_record_text = "Subject can initiate one-way communication with all visible targets." + value = 1 + required_powers = list(/datum/power/psyker_power/telepathy) + +/datum/power/psyker_power/telepathy_area/post_add() + . = ..() + var/datum/power/psyker_power/telepathy/telepathy_power = power_holder.get_power(/datum/power/psyker_power/telepathy) + var/datum/action/cooldown/power/psyker/telepathy/telepathy_action = telepathy_power?.action_path + if(telepathy_action) + telepathy_action.aoe_enabled = TRUE + telepathy_action.desc = "Allows you to mentally communicate messages to the target. Left click to send the message to one target, right click to all targets in view." + +/datum/power/psyker_power/telepathy_area/remove() + . = ..() + var/datum/power/psyker_power/telepathy/telepathy_power = power_holder.get_power(/datum/power/psyker_power/telepathy) + var/datum/action/cooldown/power/psyker/telepathy/telepathy_action = telepathy_power?.action_path + if(telepathy_action) + telepathy_action.aoe_enabled = FALSE + telepathy_action.desc = initial(telepathy_action.desc) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm index a02bcbf25e0fb0..61ec8e8a10bb0a 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm @@ -3,6 +3,7 @@ /datum/power/theologist/pious_prayer name = "Pious Prayer" desc = "Focus yourself into prayer. If you are in the Chapel, this grants you Piety unless you have 5 or more Piety. Performing prayers elsewhere only has a small chance to grant Piety. Being religious increases the efficiency of this skill." + security_record_text = "Subject fuels their powers with visits to the Chapel." value = 3 archetype = POWER_ARCHETYPE_SORCEROUS @@ -20,7 +21,6 @@ /datum/action/cooldown/power/theologist/pious_prayer name = "Pious Prayer" desc = "Perform a small prayer. If you are in the Chapel, this grants you Piety unless you have 5 or more Piety. Performing prayers elsewhere only has a small chance to grant Piety. Being religious increases the efficiency of this skill." - security_record_text = "Subject fuels their powers with visits to the Chapel." button_icon = 'icons/obj/antags/cult/structures.dmi' button_icon_state = "tomealtar" cooldown_time = 5 diff --git a/modular_doppler/modular_powers/code/powers_prefs.dm b/modular_doppler/modular_powers/code/powers_prefs.dm index e9e5c52b84dcdf..fb38bcdf155e39 100644 --- a/modular_doppler/modular_powers/code/powers_prefs.dm +++ b/modular_doppler/modular_powers/code/powers_prefs.dm @@ -7,20 +7,56 @@ /// List of all our powers, by name. var/list/all_powers = list() +/// Clears all powers and related augment assignments. +/datum/preferences/proc/nuke_powers_prefs(reasons) + all_powers = list() + + // This is a bit messy with how augmented is implemented but we can't skip these. + if(GLOB.preference_entries[/datum/preference/choiced/augment_left]) + write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_left], AUGMENTED_NO_AUGMENT) + if(GLOB.preference_entries[/datum/preference/choiced/augment_right]) + write_preference(GLOB.preference_entries[/datum/preference/choiced/augment_right], AUGMENTED_NO_AUGMENT) + + // No reason + if(!islist(reasons)) + reasons = isnull(reasons) ? list("unspecified reason") : list(reasons) + + // Have a reason: Logged in the game and told to the user. + if(length(reasons)) + var/list/feedback + LAZYADD(feedback, "Your powers were removed because of the following reasons:") + LAZYADD(feedback, reasons) + if(LAZYLEN(feedback)) + // This doesn't work if the player joins the game with an invalid file. SAD! + to_chat(parent, span_greentext(jointext(feedback, "\n"))) + + var/ckey_to_log = parent?.ckey || "unknown" + log_game("[ckey_to_log]'s powers preferences were nuked: [jointext(reasons, "; ")]") + + save_character() + return TRUE + /datum/preferences/proc/sanitize_powers() var/list/new_powers = SSpowers.filter_invalid_powers(all_powers) var/list/powers_removed = SSpowers.powers_removed - var/list/feedback + var/invalid_reason = null + + for(var/power_name in all_powers) + if(!istext(power_name) || !ispath(SSpowers.powers[power_name])) + invalid_reason = "Invalid power entry: [power_name]" + break // If filter_invalid_powers came back with removed powers, we apply the changes and give feedback + if(invalid_reason) + nuke_powers_prefs(invalid_reason) + return TRUE + if(LAZYLEN(powers_removed) && !length(new_powers)) - all_powers = list() - LAZYADD(feedback, "Your powers were removed because of the following reasons:") - LAZYADD(feedback, powers_removed) - if(LAZYLEN(feedback)) - // This doesn't work if the player joins the game with an invalid file. SAD! - to_chat(parent, span_greentext(feedback.Join("\n"))) + nuke_powers_prefs(powers_removed) return TRUE - return FALSE + if(new_powers.len != all_powers.len) + all_powers = new_powers + return TRUE + return FALSE diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index b5b121efd141b7..7b445560436c8f 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -36,6 +36,9 @@ var/current_points = 0 for(var/power_name in preferences.all_powers) var/datum/power/power_type = SSpowers.powers[power_name] + if(!ispath(power_type)) // Something is here that shouldn't be here. + preferences.nuke_powers_prefs("Invalid power entry detected while building powers UI: [power_name]") + return data current_points += power_type.value for(var/power_name in SSpowers.powers) diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index 1534f34de3ab85..f0db1a50c15623 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -125,6 +125,7 @@ PROCESSING_SUBSYSTEM_DEF(powers) /// If no changes need to be made, will return the same list. /// Expects all power names to be unique, but makes no other expectations. /datum/controller/subsystem/processing/powers/proc/filter_invalid_powers(list/powers_to_check) + powers_removed = list() var/current_balance = 0 var/current_archetype var/list/intermediary_powers = list() diff --git a/tgstation.dme b/tgstation.dme index 3738316ce25165..9faaf031f7c2c2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7525,6 +7525,8 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\premonition.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\scrying.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\telekinesis.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\telepathy.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\telepathy_area.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\ward_mind.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\_psyker_event.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\brain_trauma.dm" From 0d7ca73b10e3e2e2caa29670f6cced4e9f9971ce Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 24 Mar 2026 10:48:22 +0100 Subject: [PATCH 157/212] Augmented arms are less wonky and can now be dictated which arm is given in VV; refurbish is fixed. --- code/__DEFINES/~doppler_defines/powers.dm | 6 +++ .../mortal/augmented/_augmented_power.dm | 52 ++++++++++++++++-- .../mortal/augmented/_premium_augment.dm | 5 +- .../modular_powers/code/powers_subsystem.dm | 54 +------------------ .../modular_powers/code/powers_vv.dm | 29 +++++++++- 5 files changed, 83 insertions(+), 63 deletions(-) diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index dda1d982438403..95ba75080a2da2 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -335,3 +335,9 @@ // Used for the prefs to shorthand tell there's nothing in the right or left arm augment slot. #define AUGMENTED_NO_AUGMENT "None" + +// Arm selection overrides for augmented powers. +#define AUGMENTED_ARM_USE_PREFS 0 +#define AUGMENTED_ARM_LEFT 1 +#define AUGMENTED_ARM_RIGHT 2 +#define AUGMENTED_ARM_BOTH 3 diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm index 25b9a3a503e0e5..dcd779483ab4fb 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_augmented_power.dm @@ -14,6 +14,9 @@ // Should the augment be disabled if they're a prisoner. var/disable_if_prisoner = TRUE + // Override for arm selection (VV/admin or other callers). Defaults to user prefs. + var/arm_override = AUGMENTED_ARM_USE_PREFS + // default text for augments /datum/power/augmented/get_security_record_text() if(security_record_text) @@ -35,13 +38,22 @@ return // All checks passed, time to actually give the item. - // Yes. We do all this. Just to get people's arms. Having two is infinitely more difficult. var/obj/item/organ/implant = new augment() + + // Yes. We do all this. Just to get people's arms. Having two is infinitely more difficult + // In essence we check if the arm is given through VV; if so we skip most pref checking and use arm_override instead. Otherwise we use the prefs as normal. if(implant.zone in GLOB.arm_zones) - var/augment_left = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_left) - var/augment_right = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_right) - var/left_match = augment_matches_pref(augment_left) - var/right_match = augment_matches_pref(augment_right) + var/left_match + var/right_match + if(arm_override == AUGMENTED_ARM_USE_PREFS) // Version that uses prefs + var/augment_left = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_left) + var/augment_right = client_source?.prefs?.read_preference(/datum/preference/choiced/augment_right) + left_match = augment_matches_pref(augment_left) + right_match = augment_matches_pref(augment_right) + else // VV version that uses override. + left_match = (arm_override == AUGMENTED_ARM_LEFT || arm_override == AUGMENTED_ARM_BOTH) + right_match = (arm_override == AUGMENTED_ARM_RIGHT || arm_override == AUGMENTED_ARM_BOTH) + if(left_match && right_match) var/obj/item/organ/left_implant = new augment() left_implant.zone = BODY_ZONE_L_ARM @@ -64,6 +76,36 @@ implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED) return +/// Removes any augments spawned by this power. +/datum/power/augmented/remove() + if(!augment || !power_holder) + return + var/mob/living/carbon/carbon_holder = power_holder + var/obj/item/organ/augment_path = augment + var/zone = initial(augment_path.zone) + + // We don't need to dance with preferences here, just throw out the augment if its on the person. + if(zone in GLOB.arm_zones) + var/obj/item/organ/left_implant = carbon_holder.get_organ_slot(ORGAN_SLOT_LEFT_ARM_AUG) + if(istype(left_implant, augment_path)) + left_implant.Remove(carbon_holder, special = TRUE) + qdel(left_implant) + + var/obj/item/organ/right_implant = carbon_holder.get_organ_slot(ORGAN_SLOT_RIGHT_ARM_AUG) + if(istype(right_implant, augment_path)) + right_implant.Remove(carbon_holder, special = TRUE) + qdel(right_implant) + return + + var/slot = initial(augment_path.slot) + if(!slot) + return + var/obj/item/organ/implant = carbon_holder.get_organ_slot(slot) + if(istype(implant, augment_path)) + implant.Remove(carbon_holder, special = TRUE) + qdel(implant) + return + // Used to get the location zones for augment_location_label /datum/power/augmented/proc/get_augment_location_label() if(!augment) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm index 81e58db54ee3db..894513e44623fd 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm @@ -172,20 +172,19 @@ if(use_amount <= 0 || !stack.use(use_amount)) to_chat(user, span_warning("You need more [stack] to continue.")) return TRUE + needed -= use_amount // Non-stack parts. else typepath = tool.type needed = refurb_parts_remaining[typepath] - // Wrong item if(!needed) to_chat(user, span_warning("[tool] doesn't fit [augment]'s parts.")) return TRUE - + needed -= 1 qdel(tool) // Succesful use interaction - needed -= use_amount if(needed <= 0) refurb_parts_remaining -= typepath else diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index f0db1a50c15623..b61c62140b59a6 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -127,10 +127,8 @@ PROCESSING_SUBSYSTEM_DEF(powers) /datum/controller/subsystem/processing/powers/proc/filter_invalid_powers(list/powers_to_check) powers_removed = list() var/current_balance = 0 - var/current_archetype - var/list/intermediary_powers = list() - var/maximum_balance = MAXIMUM_POWER_POINTS + var/list/intermediary_powers = list() var/list/all_powers = get_powers() // Track distinct paths we accept while filtering this batch @@ -139,25 +137,6 @@ PROCESSING_SUBSYSTEM_DEF(powers) // Track distinct roots we accept. var/list/root_by_path = list() - // TODO: work out how to filter powers missing their requirements. - // This could be higher priorities, but could also be at the same priority level. - // TODO: work out how to filter for going over the balance cap without introducing major issues. - // Like ideally we remove the advanced ones first. - // Maybe we just make a web of requirements at init? - // Maybe we can substitute priorities with this web...? - - // Validate whether directed graph is connected! - // Construct https://www.geeksforgeeks.org/dsa/check-if-a-directed-graph-is-connected-or-not/ - // pick a random power, then do depth first search both ways - // Do the same thing when picking a thing to remove for balance stuff - // Fuck we can have multiple root powers. - - // Okay so, collect root powers on our first go through. - // THEN depth first search from each root power. - // Then copy over all the seen ones that have their requirements. - // FUCK what if there's a power that depends on two root powers. - - // First discard multiple base paths and incompatible powers for(var/power_name in powers_to_check) var/datum/power/power_type = all_powers[power_name] if(!ispath(power_type)) @@ -244,34 +223,3 @@ PROCESSING_SUBSYSTEM_DEF(powers) if(intermediary_powers.len == powers_to_check.len) return powers_to_check return intermediary_powers - - - /** TODO: ALL THE REST OF THIS - - var/value = initial(power_type.value) - if(value > 0) - if (max_positive_quirks >= 0 && positive_quirks.len == max_positive_quirks) - continue - - positive_quirks[quirk_name] = value - - current_balance += value - new_powers += quirk_name - - if (points_enabled && balance > 0) - var/balance_left_to_remove = balance - - for (var/positive_quirk in positive_quirks) - var/value = positive_quirks[positive_quirk] - balance_left_to_remove -= value - new_quirks -= positive_quirk - - if (balance_left_to_remove <= 0) - break - - // It is guaranteed that if no quirks are invalid, you can simply check through `==` - if (new_quirks.len == quirks.len) - return quirks - - return new_quirks - */ diff --git a/modular_doppler/modular_powers/code/powers_vv.dm b/modular_doppler/modular_powers/code/powers_vv.dm index a9125b20083763..000dc494c3273a 100644 --- a/modular_doppler/modular_powers/code/powers_vv.dm +++ b/modular_doppler/modular_powers/code/powers_vv.dm @@ -29,8 +29,29 @@ if(has_power(chosen)) remove_power(chosen) else + // Choice menu for augmented, specifically arms (again) that lets you dictate which arm it goes on. + var/list/power_init_vars + if(ispath(chosen, /datum/power/augmented)) + var/datum/power/augmented/aug_type = chosen + var/obj/item/organ/augment_path = initial(aug_type.augment) + if(augment_path) + var/zone = initial(augment_path.zone) + if(zone in GLOB.arm_zones) + var/arm_choice = input(usr, "Install this augment on which arm?", "Arm Selection") as null|anything in list("Left", "Right", "Both", "Cancel") + if(!arm_choice || arm_choice == "Cancel") + return + var/arm_override = AUGMENTED_ARM_USE_PREFS + switch(arm_choice) + if("Left") + arm_override = AUGMENTED_ARM_LEFT + if("Right") + arm_override = AUGMENTED_ARM_RIGHT + if("Both") + arm_override = AUGMENTED_ARM_BOTH + power_init_vars = list("arm_override" = arm_override) + // Add to sec records + adds power var/include_in_security_records = (alert(usr, "Also include this power in security records?", "Power Mod", "No", "Yes") == "Yes") - add_power(chosen, include_in_security_records = include_in_security_records) + add_power(chosen, include_in_security_records = include_in_security_records, power_init_vars = power_init_vars) // Checks if a power is on the selected target /mob/living/carbon/proc/has_power(powertype) @@ -40,13 +61,17 @@ return FALSE // Adds a power by calling the power subsystem. -/mob/living/carbon/proc/add_power(datum/power/powertype, power_transfer = FALSE, client/override_client, unique = TRUE, include_in_security_records = TRUE) +/mob/living/carbon/proc/add_power(datum/power/powertype, power_transfer = FALSE, client/override_client, unique = TRUE, include_in_security_records = TRUE, list/power_init_vars) if(has_power(powertype)) return FALSE var/pname = initial(powertype.name) if(!SSpowers || !SSpowers.powers[pname]) return FALSE var/datum/power/power = new powertype() + if(islist(power_init_vars)) + for(var/varname in power_init_vars) + if(varname in power.vars) + power.vars[varname] = power_init_vars[varname] power.include_in_security_records = include_in_security_records if(!power.add_to_holder(new_holder = src, power_transfer = power_transfer, client_source = override_client, unique = unique)) qdel(power) From 1bced33335db64e01b4a7f3da9a57b5be83e0ee1 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Tue, 24 Mar 2026 17:36:54 +0100 Subject: [PATCH 158/212] Adds tailsweep. Adds a check to the subsystem to validate the balance. --- .../powers/resonant/aberrant/tail_sweep.dm | 55 +++++++++++++++++++ .../modular_powers/code/powers_subsystem.dm | 6 ++ tgstation.dme | 1 + 3 files changed, 62 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/tail_sweep.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/tail_sweep.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/tail_sweep.dm new file mode 100644 index 00000000000000..37a6ef83ab299b --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/tail_sweep.dm @@ -0,0 +1,55 @@ +/* + Swing your tail! +*/ +/datum/power/aberrant/tailsweep + name = "Tail Sweep" + desc = "Your tail is a weapon in its own right. When activated, damages all creatures adjacent to you for 10 brute and 20 stamina, and knocks them away 2 spaces, potentially into walls.\ + \n Has a short cooldown, consumes hunger. Requires a tail." + security_record_text = "Subject can use their tail to damage and knock back foes in active combat." + security_threat = POWER_THREAT_MAJOR + value = 3 + + required_powers = list(/datum/power/aberrant_root/beastial) + action_path = /datum/action/cooldown/power/aberrant/tailsweep + + // Hunger cost of the power + var/hunger_cost = 10 + +/datum/action/cooldown/power/aberrant/tailsweep + name = "Tail Sweep" + desc = "Your tail is a weapon in its own right. When activated, damages all creatures adjacent to you for 10 brute and 20 stamina, and knocks them away 2 spaces, potentially into walls." + button_icon = 'icons/mob/actions/actions_xeno.dmi' + button_icon_state = "tailsweep" + cooldown_time = 10 SECONDS + +/datum/action/cooldown/power/aberrant/tailsweep/can_use(mob/living/user, atom/target) + if(iscarbon(user)) // we don't check for tails on non-carbons; I figured it should only exist on others for admeme reasons. + var/mob/living/carbon/carbon_user = user + var/obj/item/organ/tail/tail = carbon_user.get_organ_slot(ORGAN_SLOT_EXTERNAL_TAIL) + if(!tail) + owner.balloon_alert(user, "no tail") + return FALSE + if(user.nutrition <= NUTRITION_LEVEL_STARVING) // can't use while starving + owner.balloon_alert(user, "too hungry!") + return FALSE + . = ..() + +/datum/action/cooldown/power/aberrant/tailsweep/use_action(mob/living/user, atom/target) + playsound(get_turf(user), 'sound/effects/magic/tail_swing.ogg', 80, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) + user.visible_message(user, span_danger("[user] swings their tail aggressively in an arc around themselves!")) + user.spin(0.6 SECONDS, 1) + for(var/mob/living/victim in oview(1, user)) + to_chat(victim, span_userdanger("[user] knocks you back with their tail!")) + victim.adjustBruteLoss(10) + victim.adjustStaminaLoss(20) + if(victim.anchored) + continue + var/dir_to_victim = get_dir(user, victim) + var/turf/throw_target = get_ranged_target_turf(victim, dir_to_victim, 2) + if(throw_target) + victim.throw_at(throw_target, 2, 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_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index b61c62140b59a6..71c5b50a8e560f 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -142,6 +142,12 @@ PROCESSING_SUBSYSTEM_DEF(powers) if(!ispath(power_type)) continue + // Checks if the power exceeds the max. + current_balance += power_type.value + if(current_balance > maximum_balance) + LAZYADD(powers_removed, "Power point limit exceeded.") + return list() + // Make sure we only have up to two distinct paths. if(!(power_type.path in unique_paths)) if(length(unique_paths) >= 2) diff --git a/tgstation.dme b/tgstation.dme index 9faaf031f7c2c2..dacae640564dc5 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7494,6 +7494,7 @@ #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\web_crafter\_web_craft_datum.dm" #include "modular_doppler\modular_powers\code\powers\resonant\aberrant\web_crafter\_web_crafter_entries.dm" From d3381c6bdc71305130511f56131ef5a26b8a6d48 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 25 Mar 2026 08:23:24 +0100 Subject: [PATCH 159/212] Adds riftwalker. Teleport between several distinct places. --- code/__DEFINES/~doppler_defines/powers.dm | 10 +- .../aberrant/riftwalker/_riftwalker_datum.dm | 247 ++++++++++++++++++ .../aberrant/riftwalker/riftwalker.dm | 17 ++ tgstation.dme | 2 + 4 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/riftwalker.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index 95ba75080a2da2..c038911cc30b64 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -227,7 +227,7 @@ /** * RESONANT: PSYKER - * All defines related to the enigmatist powers. + * All defines related to the psyker powers. */ // Standard stress threshold value for the Psyker's organ. @@ -263,6 +263,14 @@ // The trait for Psyker's Levitate power. #define TRAIT_PSYKER_LEVITATE_FLIGHT "psyker_levitate_flight" +/** + * RESONANT: ABERRANT + * All defines related to the aberrant powers. + */ + +// Trait that lets you use the riftwalker mechanic. +#define TRAIT_ABERRANT_RIFTWALKER "riftwalker" + /**MORTAL DEFINES * I'm literally just using this to define Breacher Knuckle right now * These things, they take time. diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm new file mode 100644 index 00000000000000..75575380a306a5 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm @@ -0,0 +1,247 @@ +/// Global tracker for Riftwalker rifts. Largely stylized after how Heretic influences work. +GLOBAL_DATUM_INIT(riftwalker_network, /datum/riftwalker_network_tracker, new) + +#define RIFTWALKER_MIN_PAIRS 10 +#define RIFTWALKER_MAX_PAIRS 12 + +/datum/riftwalker_network_tracker + /// List of all active rifts + var/list/obj/effect/riftwalker_rift/rifts = list() + /// Debug: counts attempts to find valid rift turfs during generation + var/debug_attempts = 0 + +/datum/riftwalker_network_tracker/Destroy(force) + if(GLOB.riftwalker_network == src) + stack_trace("[type] was deleted. Riftwalkers may no longer access rifts. This is bad; call the coders!") + message_admins("The [type] was deleted. Riftwalkers may no longer access rifts. This is bad; call the coders!") + QDEL_LIST(rifts) + return ..() + +// Generates the rifts. +/datum/riftwalker_network_tracker/proc/generate_rifts() + if(length(rifts)) + return + var/start_time = world.timeofday + debug_attempts = 0 + + var/pair_count = rand(RIFTWALKER_MIN_PAIRS, RIFTWALKER_MAX_PAIRS) + var/turf/beacon_turf = pick_valid_beacon_turf() + var/next_pair_id = 1 + + // Guarantee at least one pair originates from an active teleport beacon, if possible. Mostly for fluff to suggest the connection between teleportation and the rifts. + if(beacon_turf) + var/obj/effect/riftwalker_rift/beacon_rift = new(beacon_turf) + beacon_rift.pair_id = next_pair_id + var/turf/partner_turf + if(prob(100)) // 25% chance it is adjacent to a teleporter. + var/turf/teleporter_adjacent = pick_adjacent_teleporter_turf() + if(teleporter_adjacent) + partner_turf = teleporter_adjacent + else // normal turf location logic + partner_turf = find_random_rift_turf() + // spawn logic. + if(partner_turf) + var/obj/effect/riftwalker_rift/partner_rift = new(partner_turf) + partner_rift.pair_id = next_pair_id + next_pair_id++ + else + QDEL_NULL(beacon_rift) + + + var/max_iterations = 0 // Just to prevent some form of infinite loop + // Tries creating rift pairs repeatedly up to the pair_count. + while(next_pair_id <= pair_count && max_iterations < 200) + if(!spawn_pair()) + max_iterations++ + continue + next_pair_id++ + + log_game("Riftwalker generate_rifts: [world.timeofday - start_time] ds, attempts=[debug_attempts], rifts=[length(rifts)], pairs=[pair_count], iterations=[max_iterations]") + return + +// Generates a new pair of rifts. +/datum/riftwalker_network_tracker/proc/spawn_pair() + var/turf/first_turf = find_random_rift_turf() + if(!first_turf) + return FALSE + var/turf/second_turf = find_random_rift_turf() + if(!second_turf) + return FALSE + + var/next_pair_id = 1 + for(var/obj/effect/riftwalker_rift/existing_rift as anything in rifts) + next_pair_id = max(next_pair_id, existing_rift.pair_id + 1) + + var/obj/effect/riftwalker_rift/first_rift = new(first_turf) + first_rift.pair_id = next_pair_id + var/obj/effect/riftwalker_rift/second_rift = new(second_turf) + second_rift.pair_id = next_pair_id + return TRUE + +// Main logic that gets the actual turf +/datum/riftwalker_network_tracker/proc/find_random_rift_turf() + var/tries = 0 + while(tries < 50) + debug_attempts++ + var/turf/chosen_location = get_safe_random_station_turf_equal_weight() + if(is_valid_rift_location(chosen_location)) + return chosen_location + tries++ + return null + +// Checks if a space is a valid space for a rift. Basically blocks space and prevents them from being ontop of eachother. +/datum/riftwalker_network_tracker/proc/is_valid_rift_location(turf/target_turf) + if(!isturf(target_turf) || !is_station_level(target_turf.z) || isopenspaceturf(target_turf) || isgroundlessturf(target_turf)) + return FALSE + for(var/obj/thing in target_turf) // don't spawn on dense objects + if(thing.density) + return FALSE + + for(var/obj/effect/riftwalker_rift/existing_rift in range(1, target_turf)) + return FALSE + + return TRUE + +// Specifically gets a turf next to a teleporter. +/datum/riftwalker_network_tracker/proc/pick_adjacent_teleporter_turf() + var/list/turf/candidates = list() + for(var/obj/machinery/teleport/hub/tele as anything in SSmachines.get_machines_by_type_and_subtypes(/obj/machinery/teleport/hub)) // I'm trying this instead of world and seeing how it goes. + if(!is_station_level(tele.z)) + continue + var/turf/tele_turf = get_turf(tele) + if(!tele_turf) + continue + for(var/turf/adjacent_turf as anything in range(1, tele_turf)) + if(adjacent_turf == tele_turf) // not ON the teleporter + continue + if(is_valid_rift_location(adjacent_turf)) + candidates += adjacent_turf + if(!length(candidates)) + return null + return pick(candidates) + +// Specifically gets a turf next to a beacon +/datum/riftwalker_network_tracker/proc/pick_valid_beacon_turf() + for(var/obj/item/beacon/beacon as anything in GLOB.teleportbeacons) + var/turf/beacon_turf = get_turf(beacon) + if(is_station_level(beacon_turf?.z) && is_valid_rift_location(beacon_turf)) + return beacon_turf + return null + +/obj/effect/riftwalker_rift + name = "bluespace rift" + desc = "Bluespace energies connecting two places together; many Bluespace researchers would kill to understand why these rifts form. Some argue that these are left behind by heavy sums of teleportation; but these claims are unfounded." + icon = 'icons/effects/effects.dmi' + icon_state = "bluestream" + anchored = TRUE + invisibility = INVISIBILITY_OBSERVER + /// Which pair this rift belongs to + var/pair_id = 0 + +/obj/effect/riftwalker_rift/Initialize(mapload) + . = ..() + GLOB.riftwalker_network.rifts += src + RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) + apply_wibbly_filters(src) + if(!loc) + return + var/image/rift_image = image(icon = icon, loc = src, icon_state = icon_state, layer = OBJ_LAYER) + rift_image.layer = OBJ_LAYER + rift_image.override = TRUE + apply_wibbly_filters(rift_image) + add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/riftwalker, "riftwalker_rift", rift_image) + +/obj/effect/riftwalker_rift/Destroy() + GLOB.riftwalker_network.rifts -= src + UnregisterSignal(src, COMSIG_ATOM_DISPEL) + return ..() + +/obj/effect/riftwalker_rift/examine(mob/user) + . = ..() + . += span_notice("Only riftwalkers can traverse these rifts.") + +/obj/effect/riftwalker_rift/proc/verify_user_can_see(mob/user) + return HAS_TRAIT(user, TRAIT_ABERRANT_RIFTWALKER) + +// Teleport logic. +/obj/effect/riftwalker_rift/attack_hand(mob/living/user, list/modifiers) + . = ..() + if(!verify_user_can_see(user)) + return TRUE + if(HAS_TRAIT(user, TRAIT_RESONANCE_SILENCED)) + user.balloon_alert(user, "silenced!") + return TRUE + + var/obj/effect/riftwalker_rift/linked_rift = get_paired_rift() + + var/slip_in_message = pick("slides sideways in an odd way, and disappears", "jumps into an unseen dimension",\ + "sticks one leg straight out, wiggles [user.p_their()] foot, and is suddenly gone", "stops, then blinks out of reality", \ + "is pulled into an invisible vortex, vanishing from sight") + var/slip_out_message = pick("silently fades in", "leaps out of thin air","appears", "walks out of an invisible doorway",\ + "slides out of a fold in spacetime") + + to_chat(user, span_notice("You try to align with the bluespace stream...")) + if(!do_after(user, 2 SECONDS, target = src)) + return TRUE + + var/turf/source_turf = get_turf(src) + var/turf/destination_turf = get_turf(linked_rift) || source_turf // you tp to the same space if there's no linked rift. + + new /obj/effect/temp_visual/bluespace_fissure(source_turf) + new /obj/effect/temp_visual/bluespace_fissure(destination_turf) + + user.visible_message(span_warning("[user] [slip_in_message]."), ignored_mobs = user) + + var/atom/movable/pulled = null + if(ismovable(user.pulling)) + pulled = user.pulling + if(ismob(pulled)) + to_chat(pulled, span_notice("You suddenly find yourself in a different location!")) + do_teleport(pulled, destination_turf, no_effects = TRUE) + + if(do_teleport(user, destination_turf, no_effects = TRUE)) + playsound(destination_turf, SFX_PORTAL_ENTER, 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + user.visible_message(span_warning("[user] [slip_out_message]."), span_notice("...and find your way to the other side.")) + if(pulled) + user.start_pulling(pulled) + else\ + user.visible_message(span_warning("[user] [slip_out_message], ending up exactly where they left."), span_notice("...and find yourself where you started?")) + + return TRUE + +/obj/effect/riftwalker_rift/attack_ghost(mob/user) + var/obj/effect/riftwalker_rift/linked_rift = get_paired_rift() + if(!linked_rift) + return ..() + user.abstract_move(get_turf(linked_rift)) + +// On dispel, closes that pair of rifts, and create a new pair somewhere else. +/obj/effect/riftwalker_rift/proc/on_dispel(datum/source, atom/dispeller) + SIGNAL_HANDLER + + var/obj/effect/riftwalker_rift/linked_rift = get_paired_rift() + if(!QDELETED(linked_rift)) + QDEL_NULL(linked_rift) + if(!QDELETED(src)) + QDEL_NULL(src) + + GLOB.riftwalker_network.spawn_pair() // new pair + return DISPEL_RESULT_DISPELLED + +// Gets the sibling rift of a rift. +/obj/effect/riftwalker_rift/proc/get_paired_rift() + if(!pair_id) + return null + for(var/obj/effect/riftwalker_rift/other_rift as anything in GLOB.riftwalker_network.rifts) + if(other_rift != src && other_rift.pair_id == pair_id) + return other_rift + return null + +// Determines if a mob can see it. +/datum/atom_hud/alternate_appearance/basic/riftwalker/mobShouldSee(mob/viewer) + if(!isliving(viewer)) + return FALSE + return HAS_TRAIT(viewer, TRAIT_ABERRANT_RIFTWALKER) + +#undef RIFTWALKER_MIN_PAIRS +#undef RIFTWALKER_MAX_PAIRS diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/riftwalker.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/riftwalker.dm new file mode 100644 index 00000000000000..0c18847e9829be --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/riftwalker.dm @@ -0,0 +1,17 @@ +/* + You can walk through persistent rifts. +*/ +/datum/power/aberrant/riftwalker + name = "Riftwalker" + desc = "You see bluespace gateways unseen to those around you. Each station has several unique pairs of rifts that are connected that you can interact, teleporting you between them. Only you can see and interact with them.\ + \n Interacting with it while dragging someone or something will drag them along. You cannot use these rifts while silenced." + security_record_text = "Subject can see and use special bluespace rifts, teleporting them between two specific points." + security_threat = POWER_THREAT_MAJOR + mob_trait = TRAIT_ABERRANT_RIFTWALKER + value = 5 // even if it gets you into fun places, it is rng dependent and you sometimes just end up with really bad rifts. + required_powers = list(/datum/power/aberrant_root/anomalous) + +// need the mob to be instantiated to generate rifts safely. +/datum/power/aberrant/riftwalker/post_add(client/client_source) + ..() + GLOB.riftwalker_network.generate_rifts() diff --git a/tgstation.dme b/tgstation.dme index dacae640564dc5..18fbe0ec717ba2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7496,6 +7496,8 @@ #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" From e98bd9b373a5226719231af097a5d8803a176de1 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 27 Mar 2026 11:20:07 +0100 Subject: [PATCH 160/212] Tweaks to riftwalker, tailsweep and miasmic conversion. --- .../resonant/aberrant/miasmic_conversion.dm | 12 ++---- .../aberrant/riftwalker/_riftwalker_datum.dm | 4 +- .../powers/resonant/aberrant/tail_sweep.dm | 40 ++++++++++++++----- 3 files changed, 37 insertions(+), 19 deletions(-) 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 5c6a776ea601c8..4ed44b53b8c6f5 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 @@ -1,9 +1,9 @@ /* - You passively convert your brute and burn damage into toxins damage at a 70% rate. + You passively convert your brute and burn damage into toxins damage at a defined ratio. */ /datum/power/aberrant/miasmic_conversion name = "Miasmic Conversion" - desc = "Your body mends itself disturbingly well, but creates toxic backlash in your system. You passively convert 1 brute or burn damage per second to toxins damage, at a 90% ratio. You also passively heal 0.1 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. You also passively heal a tiny amount of toxins damage per second." security_record_text = "Subject extremely rapidly regenerates, but experiences toxic backlash when they do." value = 4 power_flags = POWER_HUMAN_ONLY | POWER_PROCESSES @@ -11,17 +11,13 @@ required_powers = list(/datum/power/aberrant_root/monstrous) // how much we passively heal tox - var/passive_tox_healing = 0.1 - // how much we heal per second + var/passive_tox_healing = 0.05 + // how much we heal/convert per second var/healing = 1 // the ratio at which we convert. var/conversion_rate = 0.90 /datum/power/aberrant/miasmic_conversion/process(seconds_per_tick) - // Does not work if you're in crit - if(power_holder.stat >= SOFT_CRIT) - return - var/heal_amt = healing * seconds_per_tick if(heal_amt <= 0) return diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm index 75575380a306a5..9270c8991d2c8f 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/riftwalker/_riftwalker_datum.dm @@ -33,7 +33,7 @@ GLOBAL_DATUM_INIT(riftwalker_network, /datum/riftwalker_network_tracker, new) var/obj/effect/riftwalker_rift/beacon_rift = new(beacon_turf) beacon_rift.pair_id = next_pair_id var/turf/partner_turf - if(prob(100)) // 25% chance it is adjacent to a teleporter. + if(prob(25)) // 25% chance it is adjacent to a teleporter. var/turf/teleporter_adjacent = pick_adjacent_teleporter_turf() if(teleporter_adjacent) partner_turf = teleporter_adjacent @@ -204,7 +204,7 @@ GLOBAL_DATUM_INIT(riftwalker_network, /datum/riftwalker_network_tracker, new) user.visible_message(span_warning("[user] [slip_out_message]."), span_notice("...and find your way to the other side.")) if(pulled) user.start_pulling(pulled) - else\ + else user.visible_message(span_warning("[user] [slip_out_message], ending up exactly where they left."), span_notice("...and find yourself where you started?")) return TRUE 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 37a6ef83ab299b..f9b0ed8c8f5b50 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 @@ -4,7 +4,7 @@ /datum/power/aberrant/tailsweep name = "Tail Sweep" desc = "Your tail is a weapon in its own right. When activated, damages all creatures adjacent to you for 10 brute and 20 stamina, and knocks them away 2 spaces, potentially into walls.\ - \n Has a short cooldown, consumes hunger. Requires a tail." + \n Has a short cooldown, consumes hunger and the damage is affected by your opponent's chest armor. Requires a tail. If you are a large mob (such as with the Oversized quirk), you gain +1 range." security_record_text = "Subject can use their tail to damage and knock back foes in active combat." security_threat = POWER_THREAT_MAJOR value = 3 @@ -12,15 +12,25 @@ required_powers = list(/datum/power/aberrant_root/beastial) action_path = /datum/action/cooldown/power/aberrant/tailsweep - // Hunger cost of the power - var/hunger_cost = 10 - /datum/action/cooldown/power/aberrant/tailsweep name = "Tail Sweep" desc = "Your tail is a weapon in its own right. When activated, damages all creatures adjacent to you for 10 brute and 20 stamina, and knocks them away 2 spaces, potentially into walls." button_icon = 'icons/mob/actions/actions_xeno.dmi' button_icon_state = "tailsweep" - cooldown_time = 10 SECONDS + cooldown_time = 6 SECONDS + + // Base range. + var/range = 1 + // Throw distance + var/throw_dist = 2 + // Hunger cost of the power + var/hunger_cost = 10 + // How much brute damage it deals + var/damage = 20 + // How much stam damage it deals + var/stam_damage = 30 + // Path of the effect that appears when you get smacked by the tail + var/on_hit_vfx = /obj/effect/temp_visual/dir_setting/tailsweep /datum/action/cooldown/power/aberrant/tailsweep/can_use(mob/living/user, atom/target) if(iscarbon(user)) // we don't check for tails on non-carbons; I figured it should only exist on others for admeme reasons. @@ -38,16 +48,28 @@ playsound(get_turf(user), 'sound/effects/magic/tail_swing.ogg', 80, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) user.visible_message(user, span_danger("[user] swings their tail aggressively in an arc around themselves!")) user.spin(0.6 SECONDS, 1) - for(var/mob/living/victim in oview(1, user)) + // checks if the mob is large; if so +1 to distance. + var/effective_range = range + ((user.mob_size >= MOB_SIZE_LARGE) ? 1 : 0) + for(var/mob/living/victim in oview(effective_range, user)) + // feedback to_chat(victim, span_userdanger("[user] knocks you back with their tail!")) - victim.adjustBruteLoss(10) - victim.adjustStaminaLoss(20) + new on_hit_vfx(get_turf(victim), get_dir(user, victim)) + + // damaging. The reason why we complicate this is because we want it to be affected by body armour. + var/dmg_dealt = victim.apply_damage(damage, BRUTE, BODY_ZONE_CHEST, victim.run_armor_check(BODY_ZONE_CHEST, MELEE)) + var/stam_dmg_dealt = victim.apply_damage(stam_damage, STAMINA, BODY_ZONE_CHEST, victim.run_armor_check(BODY_ZONE_CHEST, MELEE)) + + // logging + victim.log_message("was tail-sweeped by [user] for [dmg_dealt] brute damage and [stam_dmg_dealt] stamina damage.", LOG_VICTIM) + user.log_message("has tail-sweeped [victim] for [dmg_dealt] brute damage and [stam_dmg_dealt] stamina damage.", LOG_ATTACK) + + // throwing if(victim.anchored) continue var/dir_to_victim = get_dir(user, victim) var/turf/throw_target = get_ranged_target_turf(victim, dir_to_victim, 2) if(throw_target) - victim.throw_at(throw_target, 2, 1, thrower = user, force = MOVE_FORCE_STRONG) + 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) From 47117087174ce6ebe8441992f3381778e7d8e048 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 27 Mar 2026 11:49:19 +0100 Subject: [PATCH 161/212] Tweaks some very minor things in moral powers. Dual Wield now uses icon states. --- .../powers/mortal/expert/creature_tamer.dm | 2 +- .../mortal/expert/eye_for_ingredients.dm | 7 +---- .../powers/mortal/warfighter/command_grit.dm | 2 +- .../mortal/warfighter/command_recover.dm | 2 +- .../powers/mortal/warfighter/dual_wielder.dm | 28 ++++-------------- .../icons/powers/actions_icons.dmi | Bin 0 -> 1683 bytes 6 files changed, 10 insertions(+), 31 deletions(-) create mode 100644 modular_doppler/modular_powers/icons/powers/actions_icons.dmi diff --git a/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm b/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm index bdbbbb05b6a7a3..0b4e0b432a83fd 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/expert/creature_tamer.dm @@ -18,7 +18,7 @@ cooldown_time = 5 /datum/action/cooldown/power/expert/creature_tamer/use_action(mob/living/user, mob/living/target) - if (target.stat == DEAD) + if(target.stat == DEAD) user.balloon_alert(user, "they're dead, they won't make for good friends like this!") return FALSE 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 e86e14697223fd..8628f4db7a368c 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 @@ -6,10 +6,5 @@ name = "Eye for Ingredients" desc = "You've interacted with food, drinks and/or chemicals so often, you can see at a glance if something's off with it. You can see the precise reagent contents of all containers by simply examining it." security_record_text = "Subject has a keen eye for spotting substances inside food, drinks and chemicals." + mob_trait = TRAIT_REAGENT_SCANNER value = 3 - -/datum/power/expert/eye_for_ingredients/add() - ADD_TRAIT(power_holder, TRAIT_REAGENT_SCANNER, src) - -/datum/power/expert/eye_for_ingredients/remove() - REMOVE_TRAIT(power_holder, TRAIT_REAGENT_SCANNER, src) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm index db7599136b0b6e..a1734c8319834b 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_grit.dm @@ -21,7 +21,7 @@ action_symbol = "guard" /datum/action/cooldown/power/warfighter/command/grit/use_action(mob/living/user, mob/living/carbon/target) - . = ..() + ..() target.apply_status_effect(/datum/status_effect/power/command_grit, commander_modifier) // Status effect that Burden Revered applies diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm index d4a250b5faf088..08e726c8ed4e5c 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/command_recover.dm @@ -22,7 +22,7 @@ action_symbol = "move" /datum/action/cooldown/power/warfighter/command/recover/use_action(mob/living/user, mob/living/carbon/target) - . = ..() + ..() // Basically the same amounts as shaking up twice multiplied by commander modifiers. target.AdjustStun(-6 SECONDS * (commander_modifier + 1)) target.AdjustKnockdown(-6 SECONDS * (commander_modifier + 1)) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm index 74b769e9b82c17..d9f2299ced1d83 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/dual_wielder.dm @@ -22,9 +22,11 @@ /datum/action/cooldown/power/warfighter/dual_wielder name = "Dual Wield" desc = "Toggle dual-wielding. While active, melee attacks immediately follow with an off-hand strike (each strike has a 30% miss chance)." - button_icon = 'icons/mob/actions/actions_cult.dmi' - button_icon_state = "dagger" + button_icon = 'modular_doppler/modular_powers/icons/powers/actions_icons.dmi' + button_icon_state = "dual_wield" + // starts on + active = TRUE // Chance that we miss a swing var/dual_wield_miss_chance = 30 /// Overlay for mirrored icon when active. @@ -33,28 +35,10 @@ /datum/action/cooldown/power/warfighter/dual_wielder/use_action(mob/living/user, atom/target) active = !active user.balloon_alert(user, active ? "dual wield on" : "dual wield off") - build_all_button_icons(UPDATE_BUTTON_OVERLAY | UPDATE_BUTTON_STATUS) + button_icon_state = (active ? "dual_wield" : "dual_wield_off") + build_all_button_icons(UPDATE_BUTTON_ICON | UPDATE_BUTTON_STATUS) // need this so the icon state updates. return TRUE -// Adds a mirrored overlay to the power button when dual wield is active. -/datum/action/cooldown/power/warfighter/dual_wielder/apply_button_overlay(atom/movable/screen/movable/action_button/current_button, force = FALSE) - ..() - - if(!active || !button_icon || !button_icon_state) - if(dual_wield_overlay) - current_button.cut_overlay(dual_wield_overlay) - dual_wield_overlay = null - return - - if(dual_wield_overlay) - current_button.cut_overlay(dual_wield_overlay) - - dual_wield_overlay = mutable_appearance(icon = button_icon, icon_state = button_icon_state) - var/matrix/flip_matrix = matrix() - flip_matrix.Scale(-1, 1) - dual_wield_overlay.transform = flip_matrix - current_button.add_overlay(dual_wield_overlay) - /datum/action/cooldown/power/warfighter/dual_wielder/Grant(mob/granted_to) . = ..() RegisterSignal(granted_to, COMSIG_MOB_ITEM_ATTACK, PROC_REF(on_melee_attack)) diff --git a/modular_doppler/modular_powers/icons/powers/actions_icons.dmi b/modular_doppler/modular_powers/icons/powers/actions_icons.dmi new file mode 100644 index 0000000000000000000000000000000000000000..47b27b95651aabb556685704b7c224ed0d447da6 GIT binary patch literal 1683 zcmV;E25k9>P)V=-0C=2JR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex7wuvIWN;^NFm z%}mcIfpCgT5=&AQY!#GJN)vP9%QI7RQsVQ|(v-M3Q!Jp@xj55`5_3}_Y%I!g zDOFZ*^>a~h@%7{a0K5e%cN?>;0ssI7(Md!>RA_2L^Bsq=DcjMk>SWxz-v&%cmsmt19&0RC}b{6rc7cGG!jNa2r7_} z48p~@5iTpU?daH6+OB>4Kp-0D`t~*xJs;b1p5HnDp65Kz`&MdiZ|DDPntwZif7?c)*#luhO3KaKpE6pr1fEoKk*D1&D|+@=yeeF$avhiq226^A+;uLc z{FHUTn&;|7$+RhCTE_xlG8i~AFc>xN$iN^bg8_g{>sU&rO<~P*bupg;YWXP>K(Eyz zNg4wBbgVWT1HT;OHj_DO+-)W^1HT-@YO@j0rz1%k^xD`PRV_ay0@N+36vZ=VQdBe< zyZJiwu*c1tjoYG5^EVr}Nrydd0PJQnMMaY-o;j1cC6%hXAQpbo1*orHB8n#$0&vgt z=>Ryi3fDBU>NT!uWE@)MFKZM}E~LJCNmLPH4axvWNHWhj(BIE&7c zCt0=e0|1P6Cr6JQ;grW4^O~nTUV2>J6i%H6K<(PMXn1)wr4PnL)pwrK?uo{LbIqgLm%_1Z4H?f5QT@jCb!3_`7>J|CLNIMY9!i>y2Z~E>wrp-rHj2>l_L|o?cJ^#&``*3k zuYrGN%%2~;Z{ozXH{N}pfHcJU3l{o@Rd@!ql4-Ny@_KP-6+ZdmGco`f4ezzJC8cU=V<&-Mf=kbUgfghmGHL>10LJrxDW(SXoymB3HcRIma=0=_0@O zdI(4o&EI{OvclEy?>3sraAwnW=^PrPnav-4q&gqfr$Ecr24M~QF&QlQ`ujOegwOZv zkw&Zdk@%;H;Opcp0z@N`A_ zuv=kR0epRZOvuY&Zpl0f@+a}|Lo>$KE>-_VCQ(b2U%RwYJp9m%u?6{)m|HTB33)mA z`uc|T9aeyGP6xa8A7b66Edb~>5}{!1|IWl~iSk3i5PHq^j$gNF3%m9oVw}@4tnaWh zV9uO5=II4@dO9z5la{8S)1~o6)977%lEJTfszL;VAwr=5w`V)}x#P5@y}kW!Z${#N dAMjtXKLBYl>gyaxvWEZw002ovPDHLkV1n6_F Date: Fri, 27 Mar 2026 13:36:33 +0100 Subject: [PATCH 162/212] Master rebase parity changes. --- .../code/powers/mortal/warfighter/quick_draw.dm | 2 +- .../powers/resonant/aberrant/web_crafter/binding_webs.dm | 5 ++--- .../code/powers/sorcerous/thaumaturge/brazen_bindings.dm | 2 +- .../modular_powers/code/security/resonant_cuffs.dm | 6 ++---- 4 files changed, 6 insertions(+), 9 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm index ea1d929c231797..933e68e18becc4 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/warfighter/quick_draw.dm @@ -153,7 +153,7 @@ return null var/list/equipped_items = user.get_equipped_items(INCLUDE_POCKETS | INCLUDE_HELD | INCLUDE_ACCESSORIES) - var/list/gear_items = user.get_all_gear(accessories = TRUE, recursive = TRUE) + var/list/gear_items = user.get_all_gear(recursive = TRUE) for(var/obj/item/candidate_item in gear_items) if(!istype(candidate_item, bonded_type)) 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 d145dc408e7a63..d5ae6bbb06ece4 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 @@ -35,7 +35,6 @@ name = "web ties" desc = "Sticky strings meant for binding pesky hands. Be careful not to get yourself stuck!" breakouttime = 60 SECONDS // sticky = better - trashtype = null /// Tracks if this was actually used as cuffs so we can delete on uncuff only. var/was_cuffed = FALSE @@ -54,8 +53,8 @@ RegisterSignal(src, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(on_uncuffed)) // why do we not have an uncuff proc on cuffs hello?!?!?! -/obj/item/restraints/handcuffs/cable/zipties/web/proc/on_uncuffed(datum/source, force, atom/newloc, no_move, invdrop, silent) - SIGNAL_HANDLER +/obj/item/restraints/handcuffs/cable/zipties/web/on_uncuffed(datum/source, mob/living/wearer) + ..() if(was_cuffed) qdel(src) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm index 7b488a6c468793..f7abeb15e425d9 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm @@ -43,6 +43,6 @@ handcuff_time = 6 SECONDS color = null // only til we get a proper sprite for the base cuffs, which are currrently colored red. -/obj/item/restraints/handcuffs/antiresonant/brazen/on_uncuff(datum/source, mob/equipper, slot) +/obj/item/restraints/handcuffs/antiresonant/brazen/on_uncuffed(datum/source, mob/living/wearer) . = ..() qdel(src) diff --git a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm index 34222cbcdc719e..d292d7405e7cc2 100644 --- a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm +++ b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm @@ -22,14 +22,12 @@ user.dispel(src) ADD_TRAIT(user, TRAIT_RESONANCE_SILENCED, src) cuffed_mob = user - RegisterSignal(src, COMSIG_ITEM_POST_UNEQUIP, PROC_REF(on_uncuff)) // why do we just not have an uncuff proc for cuffs I dont understand -/obj/item/restraints/handcuffs/antiresonant/proc/on_uncuff(datum/source, force, atom/newloc, no_move, invdrop, silent) - SIGNAL_HANDLER +/obj/item/restraints/handcuffs/antiresonant/on_uncuffed(datum/source, mob/living/wearer) + ..() if(cuffed_mob) REMOVE_TRAIT(cuffed_mob, TRAIT_RESONANCE_SILENCED, src) cuffed_mob = null - UnregisterSignal(src, COMSIG_ITEM_POST_UNEQUIP) /obj/item/restraints/handcuffs/antiresonant/Destroy(force) if(cuffed_mob) From 537c80eb6f26e5751ba3ec9f2d97e1b53fd5448e Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 27 Mar 2026 15:04:43 +0100 Subject: [PATCH 163/212] Adds energy dash, which is a dash for cultivator with a lot of aura. Cultivator now uses Energy instead of Dantian. --- code/__DEFINES/~doppler_defines/powers.dm | 24 ++-- .../resonant/cultivator/_cultivator_action.dm | 48 +++---- .../cultivator/_cultivator_alignment.dm | 6 +- ...vator_dantian.dm => _cultivator_energy.dm} | 60 ++++---- .../resonant/cultivator/_cultivator_root.dm | 4 +- .../resonant/cultivator/astraltouched_root.dm | 2 +- .../powers/resonant/cultivator/energy_dash.dm | 130 ++++++++++++++++++ .../resonant/cultivator/flamesoul_root.dm | 2 +- .../powers/resonant/cultivator/many_stars.dm | 24 ++-- .../cultivator/set_fire_to_dry_hay.dm | 6 +- .../resonant/cultivator/shadowwalker_root.dm | 2 +- .../travel_under_the_veil_of_night.dm | 65 +++++---- .../cultivator/vanish_unseen_into_shadow.dm | 2 +- .../code/powers/resonant/meditate.dm | 16 +-- .../sorcerous/theologist/_theologist_piety.dm | 4 +- .../modular_powers/code/powers_action.dm | 2 +- tgstation.dme | 3 +- 17 files changed, 265 insertions(+), 135 deletions(-) rename modular_doppler/modular_powers/code/powers/resonant/cultivator/{_cultivator_dantian.dm => _cultivator_energy.dm} (59%) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm diff --git a/code/__DEFINES/~doppler_defines/powers.dm b/code/__DEFINES/~doppler_defines/powers.dm index c038911cc30b64..9a4d57b2b386b4 100644 --- a/code/__DEFINES/~doppler_defines/powers.dm +++ b/code/__DEFINES/~doppler_defines/powers.dm @@ -181,11 +181,11 @@ * All defines related to the cultivator powers. */ -// Maximum amount of Dantian we can have. -#define CULTIVATOR_DANTIAN_MAX 1000 +// Maximum amount of Energy we can have. +#define CULTIVATOR_ENERGY_MAX 1000 -// How much dantian we get from meditation every 2.5 seconds -#define CULTIVATOR_DANTIAN_MEDITATION_POWER 5 +// How much energy we get from meditation every 2.5 seconds +#define CULTIVATOR_ENERGY_MEDITATION_POWER 5 // UI location of the Cultivator element #define CULTIVATOR_UI_SCREEN_LOC "WEST,CENTER-2:15" @@ -193,9 +193,9 @@ // Bonus damage on strikes done while in alignment. Balancing notes: punches have a base 20% miss chance, and this does not stack with martial arts. #define CULTIVATOR_ALIGNMENT_DAMAGE_BONUS 15 -// The max amount of Dantian we give from aura farming per second +// The max amount of Energy we give from aura farming per second #define CULTIVATOR_MAX_CULTIVATION_BONUS 3 -// The min amount of Dantian we give from aura farming per second +// The min amount of Energy we give from aura farming per second #define CULTIVATOR_MIN_CULTIVATION_BONUS 0 // How much does activating the alignment cost @@ -204,12 +204,12 @@ // How much does sustaining the alignment cost #define CULTIVATOR_ALIGNMENT_UPKEEP_COST 3 -// Standard Dantian cost defines for Cultivators. -#define CULTIVATOR_DANTIAN_TRIVIAL (CULTIVATOR_DANTIAN_MAX / 100) -#define CULTIVATOR_DANTIAN_MINOR (CULTIVATOR_DANTIAN_MAX / 10) -#define CULTIVATOR_DANTIAN_MODERATE (CULTIVATOR_DANTIAN_MAX / 5) -#define CULTIVATOR_DANTIAN_MAJOR (CULTIVATOR_DANTIAN_MAX / 2) -#define CULTIVATOR_DANTIAN_CRUSHING (CULTIVATOR_DANTIAN_MAX) +// Standard Energy cost defines for Cultivators. +#define CULTIVATOR_ENERGY_TRIVIAL (CULTIVATOR_ENERGY_MAX / 100) +#define CULTIVATOR_ENERGY_MINOR (CULTIVATOR_ENERGY_MAX / 10) +#define CULTIVATOR_ENERGY_MODERATE (CULTIVATOR_ENERGY_MAX / 5) +#define CULTIVATOR_ENERGY_MAJOR (CULTIVATOR_ENERGY_MAX / 2) +#define CULTIVATOR_ENERGY_CRUSHING (CULTIVATOR_ENERGY_MAX) // Defines SPECIFICALLY for auro farming amounts #define CULTIVATOR_AURA_FARM_TRIVIAL (CULTIVATOR_MAX_CULTIVATION_BONUS / 100) 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 46d48bce50043e..4997b3eb584832 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 @@ -5,58 +5,58 @@ button_icon = 'icons/mob/actions/backgrounds.dmi' // The organ that processes most of the Psyker Powers. Mostly all functions here communicate with this. - var/datum/component/cultivator_dantian/dantian_component + var/datum/component/cultivator_energy/energy_component - // The UI used for dantian.alist - var/atom/movable/screen/cultivator_dantian/cultivator_ui + // The UI used for energy.alist + var/atom/movable/screen/cultivator_energy/cultivator_ui - // Cost in Dantian to use + // Cost in Energy to use var/cost // Bypasses the cost check while active. On_action_success still subtracts it as normal. var/bypass_cost - // Does this power get called by _cultivator_dantian.dm when we check for aura farming? Used for potential future powers that allow you to aura farm in other ways. + // Does this power get called by _cultivator_energy.dm when we check for aura farming? Used for potential future powers that allow you to aura farm in other ways. var/contributes_to_aura_farming = FALSE /datum/action/cooldown/power/cultivator/Grant(mob/grant_to) . = ..() - ValidateDantianComponent() + ValidateEnergyComponent() return . -// Feng Shui / Aura farming mechanics; get stuff in the environment, increase dantian based on it -// The func should be responsible for checking all the environmental stuff, calculating it and then returning it to the dantian system. +// Feng Shui / Aura farming mechanics; get stuff in the environment, increase energy based on it +// The func should be responsible for checking all the environmental stuff, calculating it and then returning it to the energy system. /datum/action/cooldown/power/cultivator/proc/aura_farm() return 0 -// Since Cultivator has multiple roots and a persistent resource system, we use a component for handling Dantian -/datum/action/cooldown/power/cultivator/proc/ValidateDantianComponent() +// Since Cultivator has multiple roots and a persistent resource system, we use a component for handling Energy +/datum/action/cooldown/power/cultivator/proc/ValidateEnergyComponent() if(owner) // Prevents runtiming on start var/mob/living/carrier = owner - dantian_component = carrier.GetComponent(/datum/component/cultivator_dantian) - if(!dantian_component) + energy_component = carrier.GetComponent(/datum/component/cultivator_energy) + if(!energy_component) return FALSE return TRUE -// Validation handled in the dantian component. -/datum/action/cooldown/power/cultivator/proc/adjust_dantian(amount, override_cap) - dantian_component.adjust_dantian(amount, override_cap) +// Validation handled in the energy component. +/datum/action/cooldown/power/cultivator/proc/adjust_energy(amount, override_cap) + energy_component.adjust_energy(amount, override_cap) -//Easy access to dantian -/datum/action/cooldown/power/cultivator/proc/get_dantian() - return dantian_component.dantian +//Easy access to energy +/datum/action/cooldown/power/cultivator/proc/get_energy() + return energy_component.energy -// We check to see if our dantian component is actually there, because usually things will go bad if they don't. +// We check to see if our energy component is actually there, because usually things will go bad if they don't. /datum/action/cooldown/power/cultivator/try_use(mob/living/user, mob/living/target) - if(!ValidateDantianComponent()) - owner.balloon_alert(owner, "Yell at the coders; you're missing your dantian system!") + if(!ValidateEnergyComponent()) + owner.balloon_alert(owner, "Yell at the coders; you're missing your energy system!") return FALSE - if(dantian_component.dantian < cost && !bypass_cost) - user.balloon_alert(user, "needs [cost] dantian!") + if(energy_component.energy < cost && !bypass_cost) + user.balloon_alert(user, "needs [cost] energy!") return FALSE . = .. () // Make sure the cost gets deducted after using the power (we already checked if we can afford it) /datum/action/cooldown/power/cultivator/on_action_success(mob/living/user, atom/target) if(cost) - adjust_dantian(-cost) + adjust_energy(-cost) return diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm index 18a441e844f29c..637b558bbbf4f3 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_alignment.dm @@ -119,13 +119,13 @@ SEND_SIGNAL(user, COMSIG_CULTIVATOR_ALIGNMENT_DISABLED, src) return TRUE -// Dispel handler: drains Dantian if alignment is active. +// Dispel handler: drains Energy if alignment is active. /datum/action/cooldown/power/cultivator/alignment/proc/on_dispel(mob/owner, atom/dispeller) SIGNAL_HANDLER if(!active) return NONE - if(ValidateDantianComponent()) - adjust_dantian(-CULTIVATOR_DANTIAN_MODERATE) + if(ValidateEnergyComponent()) + adjust_energy(-CULTIVATOR_ENERGY_MODERATE) return DISPEL_RESULT_DISPELLED // Deactivating the power doesn't cost anything so we skip the cost component. diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_energy.dm similarity index 59% rename from modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm rename to modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_energy.dm index 3427243dde2ea4..47f4459750feb1 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_dantian.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_energy.dm @@ -1,24 +1,24 @@ -/// Helper to format the text that gets thrown onto the dantian hud element. -#define FORMAT_DANTIAN_TEXT(charges) MAPTEXT("
[floor(charges)]
") +/// Helper to format the text that gets thrown onto the energy hud element. +#define FORMAT_ENERGY_TEXT(charges) MAPTEXT("
[floor(charges)]
") -/datum/component/cultivator_dantian +/datum/component/cultivator_energy dupe_mode = COMPONENT_DUPE_UNIQUE // The mob we’re attached to is always `parent`. var/mob/living/attached_mob - // Current Dantian & the cap itself. - var/dantian = 0 - var/max_dantian = CULTIVATOR_DANTIAN_MAX + // Current Energy & the cap itself. + var/energy = 0 + var/max_energy = CULTIVATOR_ENERGY_MAX // The UI itself - var/atom/movable/screen/cultivator_dantian/cultivator_ui + var/atom/movable/screen/cultivator_energy/cultivator_ui // min and max values for aurafarming var/aura_min = CULTIVATOR_MIN_CULTIVATION_BONUS var/aura_max = CULTIVATOR_MAX_CULTIVATION_BONUS -/datum/component/cultivator_dantian/Initialize() +/datum/component/cultivator_energy/Initialize() . = ..() if(!isliving(parent)) return COMPONENT_INCOMPATIBLE @@ -26,20 +26,20 @@ RegisterWithParent() START_PROCESSING(SSfastprocess, src) -/datum/component/cultivator_dantian/RegisterWithParent() +/datum/component/cultivator_energy/RegisterWithParent() . = ..() if(attached_mob.hud_used) - install_dantian_hud(parent) + install_energy_hud(parent) else RegisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created)) -/datum/component/cultivator_dantian/UnregisterFromParent() +/datum/component/cultivator_energy/UnregisterFromParent() // UnregisterSignal(attached_mob, list(COMSIG_..., COMSIG_...)) . = ..() if(attached_mob) // prevents runtiming when adding/removing duplicate components UnregisterSignal(attached_mob, COMSIG_MOB_HUD_CREATED) -/datum/component/cultivator_dantian/Destroy() +/datum/component/cultivator_energy/Destroy() UnregisterFromParent() STOP_PROCESSING(SSfastprocess, src) @@ -54,17 +54,17 @@ attached_mob = null return ..() -// Processing is responsible for most of the aura farming / 'passive dantian gain'. -/datum/component/cultivator_dantian/process(seconds_per_tick) +// Processing is responsible for most of the aura farming / 'passive energy gain'. +/datum/component/cultivator_energy/process(seconds_per_tick) if(!attached_mob) return // Handles upkeep for alignment powers. for(var/datum/action/cooldown/power/cultivator/alignment/power in attached_mob.actions) if(power.active) - adjust_dantian(-(power.alignment_upkeep_cost * seconds_per_tick)) - if(dantian <= 0) // disable if we're out of dantian - to_chat(attached_mob, span_boldwarning("You've ran out of Dantian!")) + adjust_energy(-(power.alignment_upkeep_cost * seconds_per_tick)) + if(energy <= 0) // disable if we're out of energy + to_chat(attached_mob, span_boldwarning("You've ran out of Energy!")) power.disable_alignment(attached_mob) // Aura farming code below @@ -79,41 +79,41 @@ total = clamp(total, aura_min, aura_max) total *= seconds_per_tick // I love spess game time-based maths - adjust_dantian(total) + adjust_energy(total) -/datum/component/cultivator_dantian/proc/on_hud_created(datum/source) +/datum/component/cultivator_energy/proc/on_hud_created(datum/source) SIGNAL_HANDLER var/mob/living/living_holder = attached_mob if(!living_holder || !living_holder.hud_used) return - install_dantian_hud(living_holder) + install_energy_hud(living_holder) -/datum/component/cultivator_dantian/proc/install_dantian_hud(mob/living/living_holder) +/datum/component/cultivator_energy/proc/install_energy_hud(mob/living/living_holder) if(cultivator_ui) // already installed return var/datum/hud/hud_used = living_holder.hud_used - cultivator_ui = new /atom/movable/screen/cultivator_dantian(null, hud_used) + cultivator_ui = new /atom/movable/screen/cultivator_energy(null, hud_used) hud_used.infodisplay += cultivator_ui // Set initial text so it isn't blank until first adjust. - cultivator_ui.maptext = FORMAT_DANTIAN_TEXT(dantian) + cultivator_ui.maptext = FORMAT_ENERGY_TEXT(energy) hud_used.show_hud(hud_used.hud_version) -/datum/component/cultivator_dantian/proc/adjust_dantian(amount, override_cap) +/datum/component/cultivator_energy/proc/adjust_energy(amount, override_cap) if(!isnum(amount)) return - var/cap_to = isnum(override_cap) ? override_cap : max_dantian - dantian = clamp(dantian + amount, 0, cap_to) + var/cap_to = isnum(override_cap) ? override_cap : max_energy + energy = clamp(energy + amount, 0, cap_to) - cultivator_ui?.maptext = FORMAT_DANTIAN_TEXT(dantian) + cultivator_ui?.maptext = FORMAT_ENERGY_TEXT(energy) -// UI Elements for dantian -/atom/movable/screen/cultivator_dantian - name = "dantian" +// UI Elements for energy +/atom/movable/screen/cultivator_energy + name = "energy" icon = 'icons/hud/blob.dmi' // TODO: Get sprites/UI for this. icon_state = "block" screen_loc = CULTIVATOR_UI_SCREEN_LOC diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/_cultivator_root.dm index 7d882a08b514f9..cdf7abbd251256 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 @@ -13,7 +13,7 @@ if(!power_holder) // So it doesn't runtime at init return // We pass along the piety component to actually handle most of the piety stuff. - power_holder.AddComponent(/datum/component/cultivator_dantian, power_holder) + power_holder.AddComponent(/datum/component/cultivator_energy, power_holder) // Passes along meditation. var/has_meditate = FALSE for(var/datum/action/action as anything in power_holder.actions) @@ -38,5 +38,5 @@ break if(!has_other_root) - var/tobedel = holder.GetComponent(/datum/component/cultivator_dantian) + var/tobedel = holder.GetComponent(/datum/component/cultivator_energy) QDEL_NULL(tobedel) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm index d119de64469ef1..12329c053bda48 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -1,6 +1,6 @@ /datum/power/cultivator_root/astral_touched name = "Astral Touched Alignment" - desc = "You gain your Dantian's aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do extra burn damage.\ + desc = "You gain your Energy's aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do extra burn damage.\ Passively, your cold temprature tolerance is increased by 40C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\ You gain armor IV across your whole body. Has diminishing effects with your worn armor." security_record_text = "Subject is capable of entering a heightened state by observing space, granting them resistance to damage, deadlier punches and the ability to ignore cold tempratures and low pressure." diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm new file mode 100644 index 00000000000000..aca2e020da0fbf --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm @@ -0,0 +1,130 @@ +/datum/power/cultivator/energy_dash + name = "Energy Dash" + desc = "While in Alignment, you can dash forth at extreme speeds. Choose a space that can be reached by walking (even if it requires reasonable detours). You immediately dash there and arrive near-instantly. Costs Dantian to use." + security_record_text = "Subject can dash at extreme speeds while in their heightened state." + security_threat = POWER_THREAT_MAJOR + value = 4 + required_powers = list(/datum/power/cultivator_root) + required_allow_subtypes = TRUE + action_path = /datum/action/cooldown/power/cultivator/energy_dash + +/datum/action/cooldown/power/cultivator/energy_dash + name = "Energy Dash" + desc = "While in Alignment, choose a space to dash to at extreme speeds, so long as you can reach the location by walking. Costs Dantian to use." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "blink" + cost = 25 + target_type = /turf + click_to_activate = TRUE + + // Dash behavior. + var/dash_step_delay = 0.05 SECONDS + // Max amount of spaces we can dash. + var/dash_max_distance = 30 + +// Extra movement gating. +/datum/action/cooldown/power/cultivator/energy_dash/can_use(mob/living/user, atom/target) + . = ..() + if(!.) + return FALSE + // We can't dash if we're already dashing. + if(active) + user.balloon_alert(user, "already dashing!") + return FALSE + // We can't dash if we're on our ass + if(user.IsKnockdown()) + owner.balloon_alert(user, "knocked down!") + return FALSE + // We can't dash if we're immobilized + if(HAS_TRAIT(user, TRAIT_IMMOBILIZED)) + owner.balloon_alert(user, "immobilized!") + return FALSE + // We can't dash if we're legcuffed. + if(iscarbon(user)) + var/mob/living/carbon/carbon_user = user + if(carbon_user.legcuffed) + owner.balloon_alert(user, "legcuffed!") + return FALSE + return TRUE + +// Dash to the clicked location using pathfinding. +/datum/action/cooldown/power/cultivator/energy_dash/use_action(mob/living/user, atom/target) + if(!target) + return FALSE + // check & store alignment + var/datum/action/cooldown/power/cultivator/alignment/alignment_action = get_alignment_action(user) + if(!alignment_action || !alignment_action.active) + user.balloon_alert(user, "alignment required!") + return FALSE + + // Gets our current location & target turf and checks if its a valid space. + var/turf/user_turf = get_turf(user) + var/turf/target_turf = get_turf(target) + if(!user_turf || !target_turf) + return FALSE + if(!is_valid_destination(user, target_turf)) + user.balloon_alert(user, "invalid destination!") + return FALSE + + // Pathfinds the destination + var/list/path = get_path_to(user, target_turf, max_distance = dash_max_distance, mintargetdist = 0, access = user.get_access(), simulated_only = !HAS_TRAIT(user, TRAIT_SPACEWALK), skip_first = TRUE) + if(!length(path)) + user.balloon_alert(user, "no clear path!") + return FALSE + if(path[length(path)] != target_turf) + path += target_turf + + // we start dashing! + active = TRUE + INVOKE_ASYNC(src, PROC_REF(dash_along_path), user, path, alignment_action.alignment_outline_color) + return TRUE + +/datum/action/cooldown/power/cultivator/energy_dash/proc/dash_along_path(mob/living/user, list/path, alignment_color) + ADD_TRAIT(user, TRAIT_IMMOBILIZED, src) // we don't want em moving. + // for loop that creates afterimages, moves us to the next space and repeats til we're at our destination. + for(var/turf/next_turf as anything in path) + if(QDELETED(user) || user.stat >= DEAD) + break + var/dir_to_next = get_dir(user, next_turf) + new /obj/effect/temp_visual/energy_dash_afterimage(user.loc, dir_to_next, alignment_color) + var/atom/old_loc = user.loc + user.Move(next_turf, get_dir(user, next_turf), FALSE, TRUE) + if(old_loc == user.loc) + break + SLEEP_CHECK_DEATH(dash_step_delay, user) + REMOVE_TRAIT(user, TRAIT_IMMOBILIZED, src) + active = FALSE + +// Validates we can land on the destination turf. +/datum/action/cooldown/power/cultivator/energy_dash/proc/is_valid_destination(mob/living/user, turf/target_turf) + if(!target_turf || !isopenturf(target_turf)) + return FALSE + return !target_turf.is_blocked_turf(exclude_mobs = TRUE, source_atom = user) + +// Returns an active cultivator alignment action, or the first one found. +/datum/action/cooldown/power/cultivator/energy_dash/proc/get_alignment_action(mob/living/user) + if(!user) + return null + var/datum/action/cooldown/power/cultivator/alignment/first_alignment + for(var/datum/action/cooldown/power/cultivator/alignment/alignment_action in user.actions) + if(!first_alignment) + first_alignment = alignment_action + if(alignment_action.active) + return alignment_action + return first_alignment + +/obj/effect/temp_visual/energy_dash_afterimage + name = "afterimage" + icon = 'icons/effects/effects.dmi' + icon_state = "blank_white" + duration = 5 + randomdir = FALSE + +// colors the afterimage to match the alignment +/obj/effect/temp_visual/energy_dash_afterimage/Initialize(mapload, dir_override, alignment_color) + . = ..() + if(dir_override) + setDir(dir_override) + if(alignment_color) + add_atom_colour(alignment_color, FIXED_COLOUR_PRIORITY) + animate(src, alpha = 0, time = duration) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm index 2d45dd47cdfe79..a3ba616c37b8f4 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -1,6 +1,6 @@ /datum/power/cultivator_root/flame_soul name = "Flame Soul Alignment" - desc = "You gain your Dantian's aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do extra burn damage.\ + desc = "You gain your Energy's aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do extra burn damage.\ Passively, your high temprature threshold is increased by 60C regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ You gain armor III (with laser VI and fire X) across your whole body. Has diminishing effects with your worn armor." security_record_text = "Subject is capable of entering a heightened state by observing fires, granting them resistance to damage (especially lasers & fire), deadlier punches and the ability to ignore hot tempratures and fire." diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm index ceaf72eb277737..d1e4952efa529c 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm @@ -3,7 +3,7 @@ desc = "An active ability. Activating it sends forth a little star, which stops when it reaches it's destination (or hits an object) passively glowing in an area as a light source for 60 seconds. \ ou can have up to 8 of these active. \ While in alignment, you can right click with this ability to explode all active stars that are not in motion dealing 20 burn damage to all creatures in a 3x3 area centered on it. \ - Exploding the stars consumes Dantian per star. No cooldown." + Exploding the stars consumes Energy per star. No cooldown." security_record_text = "Subject can shoot lights to illuminate an area, which can be detonated while in a heightened state to explode and damage those around it." security_threat = POWER_THREAT_MAJOR value = 5 @@ -15,7 +15,7 @@ desc = "Activating the ability sends forth a little star, which stops when it reaches it's destination (or hits an object) passively glowing in an area as a light source for 5 minutes. \ ou can have up to 8 of these active. \ While in alignment, you can right click with this ability to explode all active stars that are not in motion dealing 20 burn damage to all creatures in a 3x3 area centered on it. \ - Exploding the stars consumes Dantian per star. No cooldown." + Exploding the stars consumes Energy per star. No cooldown." button_icon = 'icons/effects/eldritch.dmi' button_icon_state = "ring_leader_effect" @@ -51,10 +51,10 @@ var/star_explosion_range = 1 // the explosion sound var/star_explosion_sound = 'sound/effects/magic/wandodeath.ogg' - // the dantian cost for exploding the stars - var/star_explosion_cost = CULTIVATOR_DANTIAN_TRIVIAL * 2 - // the dantian cost per star - var/star_explosion_cost_per_star = CULTIVATOR_DANTIAN_TRIVIAL + // the energy cost for exploding the stars + var/star_explosion_cost = CULTIVATOR_ENERGY_TRIVIAL * 2 + // the energy cost per star + var/star_explosion_cost_per_star = CULTIVATOR_ENERGY_TRIVIAL // Cached alignment action for gating effects. var/datum/action/cooldown/power/cultivator/alignment/astral_touched/astral_alignment @@ -121,13 +121,13 @@ return if(!can_use(user, target)) // we need to revalidate can_use since right click normally doesnt have that. return - if(dantian_component.dantian < star_explosion_cost) - user.balloon_alert(user, "needs more dantian!") + if(energy_component.energy < star_explosion_cost) + user.balloon_alert(user, "needs more energy!") if(user) user.log_message("detonated their Many Stars.", LOG_GAME) var/list/stars_to_explode = active_stars.Copy() - adjust_dantian(-star_explosion_cost) // removes the base cost + adjust_energy(-star_explosion_cost) // removes the base cost for(var/obj/effect/many_stars_star/star as anything in stars_to_explode) if(QDELETED(star)) continue @@ -152,9 +152,9 @@ qdel(star) // Removes cost per star; if we end up at 0, explode no more stars and shut off their power. - adjust_dantian(-star_explosion_cost_per_star) - if(dantian_component.dantian <= 0) - user.balloon_alert(user, "no more dantian!") + adjust_energy(-star_explosion_cost_per_star) + if(energy_component.energy <= 0) + user.balloon_alert(user, "no more energy!") if(astral_alignment.active) astral_alignment.disable_alignment(user) break diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm index e14703b931e014..8b9feadf0e0d47 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm @@ -2,7 +2,7 @@ name = "Set Fire to Dry Hay" desc = "You can set fire onto anything you touch. This works similary to a ligher in terms of functionality. \ While in Alignment, you can right click shoot a flameblast that ignite everything in the area where it lands. \ - Using the alignment version consumes Dantian. No cooldown." + Using the alignment version consumes Energy. No cooldown." security_record_text = "Subject can set fire to any object in melee range. While in a heightened state, they can shoot motes of flame to ignite anything hit as well." security_threat = POWER_THREAT_MAJOR value = 5 @@ -13,7 +13,7 @@ name = "Set Fire to Dry Hay" desc = "You can set fire onto anything you touch. This works similary to a lighter in terms of functionality. \ While in Alignment, you can right click to shoot a flameblast that ignite everything in the area where it lands. \ - Using the alignment version consumes Dantian. No cooldown." + Using the alignment version consumes Energy. No cooldown." button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "fireball" @@ -100,7 +100,7 @@ next_projectile_time = world.time + projectile_delay fire_projectile(user, target, /obj/projectile/resonant/fire_to_dry_hay) playsound(user, 'sound/effects/fire_puff.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) - adjust_dantian(-flameblast_cost) + adjust_energy(-flameblast_cost) return TRUE // Applies projectile customization here. diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm index bb432371b34f67..f2b0fe97df0910 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm @@ -1,6 +1,6 @@ /datum/power/cultivator_root/shadow_walker name = "Shadow Walker Alignment" - desc = "You gain your Dantian's aura through dark rooms and environments. Activating it wraps you in an aura of shadow.\ + desc = "You gain your Energy's aura through dark rooms and environments. Activating it wraps you in an aura of shadow.\ You are entirely unrecognizeable in this state and your punches do extra brute damage.\ Passively, you have enhanced darkvision, and gain full on night vision while your alignment is activated.\ You gain armor IV across your whole body. Has diminishing effects with your worn armor." diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm index 3f84e6bc4d5529..5dae79c9d782f0 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm @@ -6,54 +6,61 @@ /datum/power/cultivator/travel_under_the_veil_of_night name = "Travel Under the Veil of Night" - desc = "Whilst your alignment is active, you can spend 2 seconds channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Dantian cost; no cooldown." + desc = "Whilst your alignment is active, you can spend 2 seconds channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Energy cost; no cooldown." security_record_text = "Subject can teleport in darkness while in their heightened state." security_threat = POWER_THREAT_MAJOR - value = 5 + value = 4 required_powers = list(/datum/power/cultivator_root/shadow_walker) action_path = /datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night /datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night name = "Travel Under the Veil of Night" - desc = "Whilst your alignment is active, you can spend 2 seconds channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Dantian cost; no cooldown." + desc = "Whilst your alignment is active, you can spend 2 seconds channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Energy cost; no cooldown." button_icon = 'icons/effects/effects.dmi' button_icon_state = "blank" click_to_activate = TRUE unset_after_click = TRUE - cost = 50 - + cost = 25 + target_type = /turf + use_time = 1 SECONDS // Cached alignment action for gating effects. var/datum/action/cooldown/power/cultivator/alignment/shadow_walker/shadow_walker_alignment +// Handles the channel-time delay and mid-channel validation. +/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/do_use_time(mob/living/user, atom/target) + if(!check_travel_requirements(user, target)) + return FALSE + return ..() + /datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/use_action(mob/living/user, atom/target) - if(!target) + // Revalidate after channeling. + if(!check_travel_requirements(user, target)) return FALSE - // no teleporting out of where-ever-the-nowhere you are var/turf/user_turf = get_turf(user) - if(!user_turf) - return FALSE - // alignment required + darkness - if(!is_shadow_walker_alignment_active(user) || !is_turf_in_darkness(user_turf)) - user.balloon_alert(user, "alignment + darkness required!") - return FALSE - // LOS requirement - if(!(target in view(user.client.view, user))) - user.balloon_alert(user, "out of view!") - return FALSE var/turf/target_turf = get_turf(target) - // is it open & unblocked? - if(!is_valid_destination(target_turf)) - user.balloon_alert(user, "invalid destination!") + if(!user_turf || !target_turf) return FALSE - // Do after. - if(!do_after(user, 2 SECONDS, target = user)) + // Okay so after that giant check of requirements now we actually try to teleport the person. + if(!do_teleport(user, target_turf, no_effects = TRUE)) return FALSE + playsound(target_turf, 'sound/effects/nightmare_poof.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + playsound(user_turf, 'sound/effects/nightmare_reappear.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + // After image + new /obj/effect/temp_visual/blank_echo(user_turf) + return TRUE - // Revalidate after channeling. - // recheck if we still have valid turfs (something may happen to them???) - if(!user_turf || !target_turf) +// Shared validation for use-time, mid-channel, and post-channel checks. +/datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night/proc/check_travel_requirements(mob/living/user, atom/target) + if(!target) + return FALSE + // no teleporting out of where-ever-the-nowhere you are + var/turf/user_turf = get_turf(user) + if(!user_turf) + return FALSE + var/turf/target_turf = get_turf(target) + if(!target_turf) return FALSE // alignment required + darkness if(!is_shadow_walker_alignment_active(user) || !is_turf_in_darkness(user_turf)) @@ -67,14 +74,6 @@ if(!is_valid_destination(target_turf)) user.balloon_alert(user, "invalid destination!") return FALSE - - // Okay so after that giant check of requirements now we actually try to teleport the person. - if(!do_teleport(user, target_turf, no_effects = TRUE)) - return FALSE - playsound(target_turf, 'sound/effects/nightmare_poof.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) - playsound(user_turf, 'sound/effects/nightmare_reappear.ogg', 60, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) - // After image - new /obj/effect/temp_visual/blank_echo(user_turf) return TRUE // Basically is the turf open/blocked? 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 c2d24aaaf28334..21ff87445b7319 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 @@ -76,6 +76,6 @@ /atom/movable/screen/alert/status_effect/vanish_unseen_into_shadow name = "Vanish Unseen Into Shadow" - desc = "You are undetectable and are unaffected by slowdowns." + desc = "You are undetectable through scrying and are unaffected by slowdowns." icon = 'icons/effects/effects.dmi' icon_state = "blank" diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index b3f9f772b14b49..7b3c4246dcadb7 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -1,5 +1,5 @@ /* Since this is used by two different archetypes there will be a bit of snowflaking. -Reduces stress for psykers and restores Dantian for cultivators +Reduces stress for psykers and restores Energy for cultivators */ /datum/action/cooldown/power/resonant_meditate @@ -12,7 +12,7 @@ Reduces stress for psykers and restores Dantian for cultivators // The components responsible for meditation. var/obj/item/organ/resonant/psyker/psyker_organ - var/datum/component/cultivator_dantian/cultivator_dantian + var/datum/component/cultivator_energy/cultivator_energy // used for the do while loop var/keep_going @@ -42,16 +42,16 @@ Reduces stress for psykers and restores Dantian for cultivators to_chat(owner, span_notice("You have active abilities draining your resources!")) keep_going = FALSE break - if(!psyker_organ && !cultivator_dantian) + if(!psyker_organ && !cultivator_energy) to_chat(owner, span_notice("I have nothing to meditate on!")) if(psyker_organ) psyker_organ.modify_stress(-PSYKER_STRESS_MEDITATION_POWER) if(psyker_organ.stress <= 0) to_chat(owner, span_notice("I no longer feel any stress")) - if(cultivator_dantian) - cultivator_dantian.adjust_dantian(CULTIVATOR_DANTIAN_MEDITATION_POWER) - if(cultivator_dantian.dantian >= CULTIVATOR_DANTIAN_MAX) - to_chat(owner, span_notice("My Dantian is fully charged.")) + if(cultivator_energy) + cultivator_energy.adjust_energy(CULTIVATOR_ENERGY_MEDITATION_POWER) + if(cultivator_energy.energy >= CULTIVATOR_ENERGY_MAX) + to_chat(owner, span_notice("My Energy is fully charged.")) else keep_going = FALSE break @@ -100,7 +100,7 @@ Reduces stress for psykers and restores Dantian for cultivators // gets the psyker organ and the cultivator component /datum/action/cooldown/power/resonant_meditate/proc/update_components() psyker_organ = owner.get_organ_slot(ORGAN_SLOT_PSYKER) - cultivator_dantian = owner.GetComponent(/datum/component/cultivator_dantian) + cultivator_energy = owner.GetComponent(/datum/component/cultivator_energy) // Returns TRUE if any active Cultivator or Psyker power is active on the target. /datum/action/cooldown/power/resonant_meditate/proc/user_has_active_power(mob/living/user) 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 978b368e50ebc0..717bb51861f333 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 @@ -68,8 +68,8 @@ var/datum/hud/hud_used = living_holder.hud_used theologist_ui = new /atom/movable/screen/theologist_piety(null, hud_used) - // If the cultivator dantian UI is present, use the alternate screen loc to avoid overlap. - if(living_holder.GetComponent(/datum/component/cultivator_dantian)) + // If the cultivator energy UI is present, use the alternate screen loc to avoid overlap. + if(living_holder.GetComponent(/datum/component/cultivator_energy)) theologist_ui.screen_loc = THEOLOGIST_ALT_UI_SCREEN_LOC hud_used.infodisplay += theologist_ui diff --git a/modular_doppler/modular_powers/code/powers_action.dm b/modular_doppler/modular_powers/code/powers_action.dm index 497468e35826a7..a2940be4f98d30 100644 --- a/modular_doppler/modular_powers/code/powers_action.dm +++ b/modular_doppler/modular_powers/code/powers_action.dm @@ -210,7 +210,7 @@ Handles all the logic involved in using a targeted, click-based action. // Optional aim assist for click targeting. Override for custom behavior. /datum/action/cooldown/power/proc/aim_assist(mob/living/clicker, atom/target, target_type_path) - if(!isturf(target)) + if(!isturf(target) && !istype(target_type, /turf)) // only auto aims if you click turfs; or if the auto-aim type is a turf. return // If we have a specific type we're targeting, we're targeting that instead. diff --git a/tgstation.dme b/tgstation.dme index 2e39984b413f51..5e04e9d923aa99 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7544,10 +7544,11 @@ #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_dantian.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_energy.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_power.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\_cultivator_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\astraltouched_root.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\cultivator\energy_dash.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\flamesoul_root.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\fly_like_a_shooting_star.dm" #include "modular_doppler\modular_powers\code\powers\resonant\cultivator\from_friction_comes_flame.dm" From 0c5117f03ad2abf791ce141c89efb42e28e39666 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 27 Mar 2026 21:56:53 +0100 Subject: [PATCH 164/212] Changes energy dash criteria to not care about dense desintations (it'll just bump into it). Fixes an operator. --- .../code/powers/resonant/cultivator/energy_dash.dm | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm index aca2e020da0fbf..a7231424fa1161 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/energy_dash.dm @@ -21,6 +21,8 @@ var/dash_step_delay = 0.05 SECONDS // Max amount of spaces we can dash. var/dash_max_distance = 30 + // max amounts of steps you can take with the dash + var/dash_max_steps = 50 // Extra movement gating. /datum/action/cooldown/power/cultivator/energy_dash/can_use(mob/living/user, atom/target) @@ -81,8 +83,11 @@ /datum/action/cooldown/power/cultivator/energy_dash/proc/dash_along_path(mob/living/user, list/path, alignment_color) ADD_TRAIT(user, TRAIT_IMMOBILIZED, src) // we don't want em moving. + var/steps = 0 // for loop that creates afterimages, moves us to the next space and repeats til we're at our destination. for(var/turf/next_turf as anything in path) + if(steps >= dash_max_steps) + break if(QDELETED(user) || user.stat >= DEAD) break var/dir_to_next = get_dir(user, next_turf) @@ -91,6 +96,7 @@ user.Move(next_turf, get_dir(user, next_turf), FALSE, TRUE) if(old_loc == user.loc) break + steps++ SLEEP_CHECK_DEATH(dash_step_delay, user) REMOVE_TRAIT(user, TRAIT_IMMOBILIZED, src) active = FALSE @@ -99,7 +105,7 @@ /datum/action/cooldown/power/cultivator/energy_dash/proc/is_valid_destination(mob/living/user, turf/target_turf) if(!target_turf || !isopenturf(target_turf)) return FALSE - return !target_turf.is_blocked_turf(exclude_mobs = TRUE, source_atom = user) + return TRUE // Returns an active cultivator alignment action, or the first one found. /datum/action/cooldown/power/cultivator/energy_dash/proc/get_alignment_action(mob/living/user) From 822372b129e8a049e7f1ef33f87cddd6f48a69e1 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 28 Mar 2026 12:58:37 +0100 Subject: [PATCH 165/212] Adds written descriptions to the various sections in Powers. Adjusted the descriptions of Cutlviator & Thaumaturge powers to use better wording and use linebreaks. --- .../resonant/cultivator/astraltouched_root.dm | 6 ++-- .../resonant/cultivator/flamesoul_root.dm | 6 ++-- .../resonant/cultivator/shadowwalker_root.dm | 8 ++--- .../sorcerous/thaumaturge/blend_for_me.dm | 2 +- .../sorcerous/thaumaturge/brazen_bindings.dm | 3 +- .../sorcerous/thaumaturge/conjure_rain.dm | 6 ++-- .../sorcerous/thaumaturge/gale_blast.dm | 3 +- .../sorcerous/thaumaturge/magic_barrage.dm | 3 +- .../sorcerous/thaumaturge/phantasmal_tool.dm | 3 +- .../sorcerous/thaumaturge/vitalize_flora.dm | 2 +- .../interfaces/PreferencesMenu/Mortal.tsx | 27 ++++++++++++-- .../interfaces/PreferencesMenu/Resonant.tsx | 36 +++++++++++++++++-- .../interfaces/PreferencesMenu/Sorcerous.tsx | 29 +++++++++++++-- 13 files changed, 109 insertions(+), 25 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm index 12329c053bda48..86919cb8ec60b2 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/astraltouched_root.dm @@ -1,8 +1,8 @@ /datum/power/cultivator_root/astral_touched name = "Astral Touched Alignment" - desc = "You gain your Energy's aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do extra burn damage.\ - Passively, your cold temprature tolerance is increased by 40C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\ - You gain armor IV across your whole body. Has diminishing effects with your worn armor." + desc = "You gain Energy through Aura by being able to view space (or space adjacent things), proportional to distance. Activating it gives you a radiant, blue aura causing your punches to do extra burn damage.\ + \nPassively, your cold temprature tolerance is increased by 40C; activating the alignment makes you immune to cold and pressure, allowing you to navigate space unharmed (though you still need to breathe).\ + \nYou gain armor IV across your whole body. Has diminishing effects with your worn armor." security_record_text = "Subject is capable of entering a heightened state by observing space, granting them resistance to damage, deadlier punches and the ability to ignore cold tempratures and low pressure." security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/cultivator/alignment/astral_touched diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm index a3ba616c37b8f4..1046e6d7442b2c 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/flamesoul_root.dm @@ -1,8 +1,8 @@ /datum/power/cultivator_root/flame_soul name = "Flame Soul Alignment" - desc = "You gain your Energy's aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do extra burn damage.\ - Passively, your high temprature threshold is increased by 60C regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ - You gain armor III (with laser VI and fire X) across your whole body. Has diminishing effects with your worn armor." + desc = "You gain Energy through Aura by being able to see exposed fires (bonfires, plasma fires, etc.) or if you are on fire yourself. Activating it gives you a burning hot aura, causing your punches to do extra burn damage.\ + \nPassively, your high temprature threshold is increased by 60C regardless of species. Activating the alignment makes you completely immune to fire (but does not extinguish them).\ + \nYou gain armor III (with laser VI and fire X) across your whole body. Has diminishing effects with your worn armor." security_record_text = "Subject is capable of entering a heightened state by observing fires, granting them resistance to damage (especially lasers & fire), deadlier punches and the ability to ignore hot tempratures and fire." security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/cultivator/alignment/flame_soul diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm index f2b0fe97df0910..4813fb2c136a5c 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/shadowwalker_root.dm @@ -1,9 +1,9 @@ /datum/power/cultivator_root/shadow_walker name = "Shadow Walker Alignment" - desc = "You gain your Energy's aura through dark rooms and environments. Activating it wraps you in an aura of shadow.\ - You are entirely unrecognizeable in this state and your punches do extra brute damage.\ - Passively, you have enhanced darkvision, and gain full on night vision while your alignment is activated.\ - You gain armor IV across your whole body. Has diminishing effects with your worn armor." + desc = "You gain Energy through Aura by being in dark rooms and environments. Activating it wraps you in an aura of shadow.\ + \nYou are entirely unrecognizeable in this state and your punches do extra brute damage.\ + \nPassively, you have enhanced darkvision, and gain full on night vision while your alignment is activated.\ + \nYou gain armor IV across your whole body. Has diminishing effects with your worn armor." security_record_text = "Subject can enter a heightened state by observing darkness, granting them resistance to damage, deadlier punches, the abiliy to become unrecognizeable as a dark silhouette and the ability to see perfectly in the dark." security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/cultivator/alignment/shadow_walker diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm index 70f268e49d668e..52f904c066bf08 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/blend_for_me.dm @@ -4,7 +4,7 @@ /datum/power/thaumaturge/blend_for_me name = "Blend For Me" desc = "Grinds the item in your hand as if it were inserted in a grinder, then conjures a glass to hold it (if you're grinding). Right-hand for grinding, left-hand for juicing. Can be used on people using an aggressive grab to inflict brute damage and bleeding. \ - Requires Affinity 1. Affinity gives a chance to not consume charges." + \nRequires Affinity 1. Affinity gives a chance to not consume charges." security_record_text = "Subject can magically blend drinks, objects and people with their bare hands." value = 2 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm index f7abeb15e425d9..227387d6db4122 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm @@ -4,7 +4,8 @@ /datum/power/thaumaturge/brazen_bindings name = "Brazen Bindings" - desc = "Summons a set of manacles made from brass, capable of dispelling and disabling Resonant powers on the bound target. The magic that made them is fragile, causing them to break once someone escapes. Requires Affinity 1. Additional affinity increases the time it takes to break out." + desc = "Summons a set of manacles made from brass, capable of dispelling and disabling Resonant powers on the bound target. The magic that made them is fragile, causing them to break once someone escapes. \ + \nRequires Affinity 1. Additional affinity increases the time it takes to break out." security_record_text = "Subject can conjure anti-resonant manacles out of thin air." security_threat = POWER_THREAT_MAJOR value = 3 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm index dd4ad15b4bf526..ed34dbce354f0a 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm @@ -2,8 +2,8 @@ /datum/power/thaumaturge/conjure_rain name = "Conjure Rain" desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u water, up to a maximum of 60u spread out across all containers. Mobs are splashed with the same amount and don't count towards this limit. \ - \n Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ - \n Requires Affinity 3. Higher affinity increases the max amount of spreadable reagents by 20u." + \nHolding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ + \nRequires Affinity 3. Higher affinity increases the max amount of spreadable reagents by 20u." security_record_text = "Subject can conjure rains with varying chemical properties." security_threat = POWER_THREAT_MAJOR value = 4 @@ -14,7 +14,7 @@ /datum/action/cooldown/power/thaumaturge/conjure_rain name = "Conjure Rain" desc = "Coats a 3x3 area at the chosen location in rain. Everything in the area becomes wet, and any reagent containers are filled with 20u water, up to a maximum of 60u spread out across all containers. Mobs are splashed with the same amount and don't count towards this limit. \ - \n Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ " + Holding a reagent container in hand will consume the chemical and replaces that much of the water with the held reagent (only works with chemicals that can be synthesized). Replacing all the water in a cast will prevent slippery tiles. \ " button_icon = 'icons/effects/weather_effects.dmi' button_icon_state = "rain_low" 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 7168b784854b24..212cee086a60d9 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 @@ -8,7 +8,8 @@ /datum/power/thaumaturge/gale_blast name = "Gale Blast" - desc = "Shoots forth a blast of wind. The blast keeps traveling until it hits a solid structure, extinguishing any fires and dragging along any items with it. If it hits a creature, it knocks them back 3 spaces and extinguishes them. Requires Affinity 3. Extra affinity gives a chance to knockback further." + desc = "Shoots forth a blast of wind. The blast keeps traveling until it hits a solid structure, extinguishing any fires and dragging along any items with it. If it hits a creature, it knocks them back 3 spaces and extinguishes them. \ + \nRequires Affinity 3. Extra affinity gives a chance to knockback further." security_record_text = "Subject can create and shoot out strong, violent gusts of wind." security_threat = POWER_THREAT_MAJOR value = 3 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 07e28bac7d784c..3ca9fe6fc9815d 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 @@ -5,7 +5,8 @@ /datum/power/thaumaturge/magical_barrage name = "Magical Barrage" - desc = "Shoots a volley of magic projectiles equal to your Affinity + 2. You can either fire single shots with a short delay between shots, or shoot all your remaining shots in a barrage. Requires Affinity 3." + desc = "Shoots a volley of magic projectiles equal to your Affinity + 2. You can either fire single shots with a short delay between shots, or shoot all your remaining shots in a barrage. \ + \nRequires Affinity 3." security_record_text = "Subject can conjure and shoot a volley of magical lasers." security_threat = POWER_THREAT_MAJOR value = 5 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm index 120ddf3275c564..7d2c2211019e21 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/phantasmal_tool.dm @@ -3,7 +3,8 @@ */ /datum/power/thaumaturge/phantasmal_tool name = "Phantasmal Tool" - desc = "Summons a basic tool of your choice in your hand, that disappears after a duration, or if it is dropped/used to attack a person. Requires Affinity 1 to cast. Affinity gives a chance to not consume charges on cast." + desc = "Summons a basic tool of your choice in your hand, that disappears after a duration, or if it is dropped/used to attack a person. \ + \nRequires Affinity 1 to cast. Affinity gives a chance to not consume charges on cast." security_record_text = "Subject can conjure ephemeral tools out of thin air." security_threat = POWER_THREAT_MAJOR value = 3 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm index 4d3cad3347b1e5..6c9d6b5e0b5fba 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/vitalize_flora.dm @@ -4,7 +4,7 @@ /datum/power/thaumaturge/vitalize_flora name = "Vitalize Flora" desc = "Breathes life into the plants around you. This heals any and all plants (including plant creatures), makes them grow if they're still in the growth phase, and speeds up the time until the next harvest. \ - Requires Affinity 1. Affinity gives a chance to not consume charges." + \nRequires Affinity 1. Affinity gives a chance to not consume charges." security_record_text = "Subject can magically heal and grow plantlife around it." value = 2 diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx index f7d9fd30d15f80..01853e138dbe88 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/Mortal.tsx @@ -1,8 +1,8 @@ -import { Button, Section, Stack } from 'tgui-core/components'; +import { Box, Button, Collapsible, Section, Stack } from 'tgui-core/components'; import { useBackend } from '../../backend'; import { Powers } from './PowersMenu'; -import { PreferencesMenuData } from './types'; +import type { PreferencesMenuData } from './types'; type MortalPagePowers = { handleCloseMortal: () => void; @@ -10,6 +10,11 @@ type MortalPagePowers = { export const MortalPage = (props: MortalPagePowers) => { const { data } = useBackend(); + const descriptionBlock = (text: string) => ( + + {text} + + ); return ( @@ -41,6 +46,12 @@ export const MortalPage = (props: MortalPagePowers) => {
+ {descriptionBlock( + 'Warfighter, as the name implies, focuses almost exclusively on combat. It is split into three distinct categories, which are not mutually exclusive.\ + \n\nCommander, which applies defensive buffs to targets through verbal or non-verbal command. The efficiency of these powers scales with whether the target is in your department and if you are a leadership role.\ + \n\nEquipment Specialist, which specializes in using specific equipment in better ways. These usually require a specific type of item to get their mileage out of it, but some are more universally applicable than others, such as dual-wielding.\ + \n\nMartial Artist, which powers up your unarmed prowess and grants you better strikes, access to martial arts and tackling.', + )} {data.warfighter.map((val) => ( @@ -50,6 +61,10 @@ export const MortalPage = (props: MortalPagePowers) => {
+ {descriptionBlock( + 'Experts are broad in their capabilities, and often include the many phenomenal things anyone can do with perseverance, experience and a fair degree of luck. There are no broader mechanics in Expert.\ + \n\nMost expert powers provide specialized bonuses that on their own may seem niche, but when presented with their use-case, can help you perform your actions come to fruition. An expert is only as good as their creativity.', + )} {data.expert.map((val) => ( @@ -59,6 +74,14 @@ export const MortalPage = (props: MortalPagePowers) => {
+ {descriptionBlock( + 'The flesh is weak; Augmented lets you tweak and adjust your physical body with specialized augments, granting you capabilities on-par with resonance, in a technological manner.\ + \n\nAugmented grants you augments at round-start, but is is beholden to a fair few restrictions and drawbacks; you can only have one augment per body part, and you are susceptible to EMPs, disabling your augments and possibly having adverse side-effects.\ + \n\nA subcategory of powers exists within Augmented; Premium Augments. These are commercialized and specialized augments made out of propieretary parts, making them unable to be built on the station. \ + These possess a quality meter, which dictates how much mileage you get out of your Premium Augments. The higher the percentage, the stronger their effects. \ + Through robotic surgery, these can be maintained and refurbished, restoring their quality. Once quality reaches 0%, you are required to refurbish it for it to be functional.\ + \nWhether you wish to burn through your augments and make repeat roboticist visits, or try to be more diligent with it, is up to you. Keep in mind as well; your powers can be physically stolen!', + )} {data.augmented.map((val) => ( diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/Resonant.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/Resonant.tsx index 65527fadfd1489..82c92194154821 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/Resonant.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/Resonant.tsx @@ -1,14 +1,19 @@ -import { Button, Section, Stack } from 'tgui-core/components'; +import { Box, Button, Collapsible, Section, Stack } from 'tgui-core/components'; import { useBackend } from '../../backend'; import { Powers } from './PowersMenu'; -import { PreferencesMenuData } from './types'; +import type { PreferencesMenuData } from './types'; type ResonantPowerProps = { handleCloseResonant: () => void; }; export const ResonantPage = (props: ResonantPowerProps) => { const { data } = useBackend(); + const descriptionBlock = (text: string) => ( + + {text} + + ); return ( @@ -40,6 +45,15 @@ export const ResonantPage = (props: ResonantPowerProps) => {
+ {descriptionBlock( + 'The mind grows stronger, and your body twisted to facilitate it, as much as it can handle. Psykers uses classically psychic abilities such as telekenisis and telepathy, mastering the domain over the mind.\ + \n\nMechanically, this manifests in their special mechanic; Stress. You have an unique organ inside you called a Paracusal Gland. This is in-essence the liver of your brain; it is there to handle chemical and physical strain put on your body by your mental powers.\ + Using your powers generates Stress proportional to the impact of your powers. Whilst you are under the Stress Threshold, it passively diminishes over-time, but should you go over it, you start experiencing negative events and your stress will not decay without using\ + the special Meditate action you were given. You are never truly certain of how much Stress you have, only the estimates given by your body violently reacting to the pressure.\ + \n\nExceeding the threshold causes at first mild symptons, such as headaches, jittering and more. Continued overuse expands it to severe symptoms such as bleeding eyes, vomiting and more. Should you continue past this point, you will suffer a\ + catastrophic breakdown, often inflicting permanent, long-lasting injuries on you, and reseting your Stress consequently.\ + \n\nIn exchange for this Stress, almost none of your abilities have cooldowns or other limiting factors; Stress is your sole-limiting resource. Manage it well.', + )} {data.psyker.map((val) => ( @@ -49,6 +63,15 @@ export const ResonantPage = (props: ResonantPowerProps) => {
+ {descriptionBlock( + "Your body is a temple; one that strengthens from aligning it with resonant energies. By associating with specific phenomena, you gain supernatural powers, allowing you resist blows and strike with your fists as if it were a blade.\ + \n\nCultivator builds up a resource called Energy, which is the cost for a variety of their powers. Most prominently it is used to fuel a state called Alignment. Once you enter this heightened state of Alignment, you gain passive effects and heightened damage,\ + turning you into a force to be reckoned with regardless of your current equipment. Many of your powers require Alignment to be active and cost Energy in turn, but have some incredibly powerful effects in turn.\ + \n\nEnergy is build up through two methods; Meditation, and Aura. Meditation can be done at any point, engulfing you in light as you attune with the passive Resonance in the air. This slowly fills your energy, but prevents you from doing anything else.\ + Meanwhile, Aura lets you harvest it passively from an environment with which you align. If your Alignment is Astral Touched, that means your Energy builds from seeing starlight and other space-based phenomena, whilst something such as Flame soul energizes from seeing exposed flames.\ + You can combine these two methods; an Astral-Touched Cultivator energizes quickly while meditating before the stars. Your Energy caps out at 1000, and most Alignments require at least 200 to activate, with a hefty upkeep (you cannot gain Energy while in Alignment).\ + \n\nYou won't be able to enter your heightened state often, but once you do, you will wield great powers. Wisdom is knowing when to wield it.", + )} {data.cultivator.map((val) => ( @@ -58,6 +81,15 @@ export const ResonantPage = (props: ResonantPowerProps) => {
+ {descriptionBlock( + "Aberrant is a collection of the odd, the excentric and the extraordinary. It is home to many categories, of various capabilities that don't belong strongly in any particular path. These three categories are:\ + \n\nBeastial; people who have the trait and qualities of animals. Whether being able to shift into one, or mimmicking their biological traits, they wield these along with their existing biology to enhance their capabilities.\ + Beastial abilities often have a hunger cost and cannot be used while starving.\ + \n\nAberrant; whose traits are not of animals, but of monsters. The ability to regenerate any wounds, to grow blades for arms. The qualities of monsters that are often the tail of rumor and folk-lore. They often resist any and all\ + harm cast upon them; and often are the truly unstopable monsters people think about.\ + \n\nAnomalous; whose very existence is unexplainable through sciences. The ability to end anomalies at a touch, the ability to walk through rifts in realities, or interacting in inexplicable ways with reality, such as healing from radiation poisoning.\ + These oddities work in their own way, and wield their poorly understood powers in their day-to-day work.", + )} {data.aberrant.map((val) => ( diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/Sorcerous.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/Sorcerous.tsx index 5306270a3e62d0..b8cc94dd6e9279 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/Sorcerous.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/Sorcerous.tsx @@ -1,8 +1,8 @@ -import { Button, Section, Stack } from 'tgui-core/components'; +import { Box, Button, Collapsible, Section, Stack } from 'tgui-core/components'; import { useBackend } from '../../backend'; import { Powers } from './PowersMenu'; -import { PreferencesMenuData } from './types'; +import type { PreferencesMenuData } from './types'; type SorcerousPageProps = { handleCloseSorcerous: () => void; @@ -10,6 +10,11 @@ type SorcerousPageProps = { export const SorcerousPage = (props: SorcerousPageProps) => { const { data } = useBackend(); + const descriptionBlock = (text: string) => ( + + {text} + + ); return ( @@ -41,6 +46,18 @@ export const SorcerousPage = (props: SorcerousPageProps) => {
+ {descriptionBlock( + "Magic, wizards, sages. The most classical depiction of magic in folklore and history is based on perception, and people's believe that a person with a pointy-hat can cast a spell. To be a Thaumaturge, you have to act like a Thaumaturge.\ + \nThaumaturgy has two core components; Spell Preperation, and Affinity.\ + \n\nTo start off, your spells are limited not by cooldowns, but by charges. Every point you put in the Thaumaturge power grants you 2 points of Mana. This is used by your Spell Preperation power, which allows you to allocate\ + your Mana to spells to charge them. The cost to gain the Power is the same as to prepare the Charges. Once you set your spells, that are the amount of charges you have. Once you run out of charges, you can't use \ + that power again until you sleep for a certain duration. Not just any sleep will do; you need a catalyst on you to shape your dreams called an Arcane Focus. You start the round with it, and you'd best keep it safe, as without\ + it you won't ever be able to restore your spells.\ + \n\nFuthermore, you have Affinity to both scale and use your powers. Your Arcane Focus has a value called Affinity, which determines the potency of your spells. Some spells require a certain amount of affinity to wield;\ + and you gain it by holding the affinity item. Exceeding the required affinity usually grants additional bonuses with spells, such as higher damage (elaborated per spell). Affinity also exists on other items and clothes; \ + dressing like a Wizard with a wizard costume will grant you Affinity as well. Affinity does not stack; you take the highest source. You can examine items to see how much Affinity they have, if any. Usually anything \ + you'd see on a druid, wizard, bard or other magically inclined person in folklore will grant you Affinity.", + )} {data.thaumaturge.map((val) => ( @@ -50,6 +67,7 @@ export const SorcerousPage = (props: SorcerousPageProps) => {
+ {descriptionBlock('Enigmatist is still in development!')} {data.enigmatist.map((val) => ( @@ -59,6 +77,13 @@ export const SorcerousPage = (props: SorcerousPageProps) => {
+ {descriptionBlock( + 'Whilst Thaumaturgy is rooted in the perception of others on you, Theology is rooted in your perception of self. To act holy and perform miracles is rooted in firm believe and willpower.\ + \nTheologists are spread across several categories, each of which have a base power that heals the wounds of others. In what form and with what method differs per power, but it will always grant you a measure of Piety.\ + \n\nPiety is a measure of your good deeds; it is gained by healing others with your powers, proportional to the healing (as long as it is sentient, healing animals is not pious, alas). These are in turn used to fuel other\ + theologist powers, such as being able to bless weapons, randomly resist blows and other powers specific to your path. It has a maximum of 50.\ + \n\nUniquely, the Chaplain gains additional powers and bonuses with certain powers, and has double the maximum amount of Piety. Theologist powers and not necessairly related to divinity; they are rooted in firm believe themselves, whether in said divinity or their deeds.', + )} {data.theologist.map((val) => ( From 1c7fbc2e66debb7c14a96306736e26dd5713926a Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 28 Mar 2026 14:05:26 +0100 Subject: [PATCH 166/212] Calls antiresonant cuffs eschatite now. Adds it to sec orders. Bunch of other small tweaksies. --- .../code/cargo/antiresonant_cuffs.dm | 9 +++++++++ .../sorcerous/thaumaturge/brazen_bindings.dm | 1 + .../code/security/reality_anchor.dm | 2 +- .../code/security/resonant_cuffs.dm | 10 +++++----- .../modular_powers/icons/items/restraints.dmi | Bin 5023 -> 2571 bytes .../code/tg_vendors/sectech.dm | 2 +- tgstation.dme | 1 + 7 files changed, 18 insertions(+), 7 deletions(-) create mode 100644 modular_doppler/modular_powers/code/cargo/antiresonant_cuffs.dm diff --git a/modular_doppler/modular_powers/code/cargo/antiresonant_cuffs.dm b/modular_doppler/modular_powers/code/cargo/antiresonant_cuffs.dm new file mode 100644 index 00000000000000..a113729baa740f --- /dev/null +++ b/modular_doppler/modular_powers/code/cargo/antiresonant_cuffs.dm @@ -0,0 +1,9 @@ +/datum/supply_pack/security/antiresonant_cuffs + name = "Eschatite Handcuff Crate" + desc = "A crate containing 5 special-crafted handcuffs that suppress resonant powers." + cost = CARGO_CRATE_VALUE * 2 + access_any = list(ACCESS_SECURITY) + contains = list(/obj/item/restraints/handcuffs/antiresonant = 5) + crate_name = "eschatite handcuff crate" + crate_type = /obj/structure/closet/crate/secure/gear + diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm index 227387d6db4122..1ddede7ce58447 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/brazen_bindings.dm @@ -38,6 +38,7 @@ /obj/item/restraints/handcuffs/antiresonant/brazen/ name = "brazen manacles" desc = "Bulky, enchanted and resonant manacles made out of brass and laden with (cheap) gemstones. They're held together using a sliver of resonant power, causing them to break into an unuseable mess once removed." + icon = 'icons/obj/weapons/restraints.dmi' icon_state = "brass_manacles" w_class = WEIGHT_CLASS_NORMAL breakouttime = 30 SECONDS // default for 1affinity. For comparison, zipties are 30seconds and normal cuffs are 1min. diff --git a/modular_doppler/modular_powers/code/security/reality_anchor.dm b/modular_doppler/modular_powers/code/security/reality_anchor.dm index 99cd6c499da114..9328cc794d6d72 100644 --- a/modular_doppler/modular_powers/code/security/reality_anchor.dm +++ b/modular_doppler/modular_powers/code/security/reality_anchor.dm @@ -1,6 +1,6 @@ /obj/structure/reality_anchor name = "miniature reality anchor" - desc = "The chiseled out parts of a broken down reality anchor. Crude machinery is managing to keep it docile; but when enabled, it will start enforcing normality back in a large area around it." + desc = "The chiseled out Eschatite remains of an anchor, smoothed and cobbled together. Crude machinery is managing to keep it docile; but when enabled, it will start enforcing normality back in a large area around it." icon = 'modular_doppler/modular_powers/icons/items/reality_anchor.dmi' icon_state = "reality_anchor" density = TRUE diff --git a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm index d292d7405e7cc2..8b898c5bdf7b89 100644 --- a/modular_doppler/modular_powers/code/security/resonant_cuffs.dm +++ b/modular_doppler/modular_powers/code/security/resonant_cuffs.dm @@ -1,12 +1,12 @@ // Antiresonant cuffs. They're like normal cuffs but slightly worse and put a dampener on resonant folk. /obj/item/restraints/handcuffs/antiresonant - name = "resonant suppressant handcuffs" - desc = "Handcuffs laced with a smooth, dark material similar to magnetite, harvested from a reality anchor. Capable of suppressing resonant powers." - icon_state = "handcuffAlien" - color = "#ee3d3d" // til we get a proper sprite for these things. + name = "eschatite handcuffs" + desc = "Handcuffs laced with a smooth, dark material similar to magnetite called Eschatite, harvested from a reality anchor. Capable of suppressing resonant powers on whoever is made to wear them. Slightly less sturdy than regular handcuffs." + icon = 'modular_doppler/modular_powers/icons/items/restraints.dmi' + icon_state = "anti_resonant_cuffs" breakouttime = 50 SECONDS handcuff_time = 4.5 SECONDS - custom_price = PAYCHECK_COMMAND * 0.6 + custom_price = PAYCHECK_COMMAND // we save the mob so we don't end up orphaning the silence remover var/mob/living/cuffed_mob diff --git a/modular_doppler/modular_powers/icons/items/restraints.dmi b/modular_doppler/modular_powers/icons/items/restraints.dmi index ad7599ebb74d36ea8ded8a923805807ff43479ac..a7add805fd051aa1bc0a2260693ed0099e4d48ce 100644 GIT binary patch delta 2454 zcmV;H32FA9CyNx28Gi!+003nIoHYOd07y_wR7KR()PsYApP!$%x3|m7%crNOetv$9 zjg6q7poD~ko}Qkjrlz*Gwq|B#aBy(c)YL32EF2shW&j*bO-;D~GK>HwI5kQnAvP-r z3pxM*00DGTPE!Ct=GbNc0063bR9JLGWpiV4X>fFDZ*Bkpc#$`Ge~}a?=9Of|7o`^G z=K)#q$)#y&#klk+E4cc(fWry^xO7$?HkjV&000QPNklR2=pV9cczlb z&gTFB^dwJaY}r7AK)aVMbE&#b5G{~pSv0b{?%$Gr7=|B>KaO{I4ST4^?~`!9E&hHu zq#j067a{fd`N|#of8}ojg_1f*J&>vpsmAYzYCPF)Ie@#naiBwn7xUU@0vz~j#p>CKHQtE>mj+~IQg7S5yjs z*Aw`Co^I)iJxD#4%MP;$O&y3ZSO4zwG(0d@QPdOoIe`3lkh|_5?w2U+IBNpPP-#VE zM?un4`2BRfVBd2DQbV$Xa^hZq7Jo!~gnw&WD7OID2*f}MTha{={^E-bp-yD+cN})j=HOt82mD~|A`VkGj`eKCf=Lu zZ!ZE${+-8n+I#V}{5y~Dvv=bI{@u&p*VGa$fB4sjJIB9!`Fms^P#+NacQ1dB>_h4U z694Yy?~#2>eL&#fz5G4052_CcNw<@~NA_X$0fF}L@%P9+tUll+o~rG`>H{|FuY$h^ z_Wt^Sg?z2z?}5F)KA@q$J^Vegch?8{4i_Li{5`Vw;#*2UHT*ra_u?B$bT#}vwD;n3 ze+|6+eb6$>;-dy${vO%K)Cc$mUj820$J7UIXoLe!{vO$f)CX>Q@;Ho-zen~F^?@0S zJZI(O?~#2(ec+}i`FAgWkL&~L1J?!&|L*1Q-AO&bpBwP}yGp$FZhVP?)`&VF3wRA zNH?*5nL}mH+V>CEu7RbLh=6d!e{%j`sYm#;W10xW-Kg#SzfO&i@Cze^B0(aD1(T&i@CzA5e~HI6Z{U|F?esz%TIVZ{wrKf4l6m%Ptpb z%ip(`dj1|gs2V@WuDI3wJ=!Y7ZzaevVEKDAVra7l_2zh(F<>_8aAu+HGSJ&+E}Jlr zsnUhG5dl%cBI*_RE8$Z%GF$zo&O}I*GD><4ej!5A%#g2n!T~M;CGC=4gTD}owqQ`E z)@nY7ZYb$B_yiCkB8V~qf36uMws?F%-Kj9vTH+G2)S*((;>-Md*@ryHZ^!HGCD&nw zyjANNe89giMNuC(PCPiX4wLk`K~LiY{@vO^7H%D`D3ECI0slUW$AyItSfysZ5XNjOmDqBdcoY+9szsB*#T{YidI(v2z?n#xA4#Lyln z^6<(=xIp^ifA0S-yX>;d4;Ox!ZOzXIJqX!NQR7m7)K-M8Pk1+L-ui$Nk$=Cyxm|YG z2M7a8ePW|ACM*HJx;tPq6J49y${m-1DZPr9RO?O%NxmNX2MGZA#jb zpZDQwNrBe4rzwe{Ae{Uw<@B1k2+llx4K7#%Gd#zIz@qXWjw}0-}lkjC;!ub?;q8k zDaFf8@%GvEkM}f@K2D#f)2YHYl~jD+M`7P}+2_yn|8xmXzp}Inq2l{K*gi?}`h0Qu z^qKy7KL2VHK+X4kpnZ4o|NK4Zr}WR?zt3M0{%)k^fBQbrzPmVm{`w9e`QLSo{IB}H z548Ut^y@F@-`f^3;w{3l_9=kb>900}-zoX`D*pcTtJ$Cv{nr+vCHrQBz7X$G0E2xK z@gBx7**6gHaTue0xOfjFneC&+dnC+mA1&TPd94o(Dm-u=i~KX6RoG>hUDom+GlY5d UDAhPB3;+NC07*qoM6N<$f?y#K_5c6? delta 4908 zcmX|Fc|25K*dI)mib2`S5F(6SMU5d#)|5(`!Pv88NfCy*wkQ$FR@uuILw2&vgoNpr zHCr-@2@x4%nQ6?tqxbW^f8BHL{hsGM&-Xd!Ip1?1A1zQ!lQ_%`0)gyiZu77oSx*xl z1N^NSB942G1SU5q=B8<8{|OQw2qfHNVPbSGEcbU_TeXw!k&lBkuyCV|9wi61)|5#WF(icJjhLg2EqYp8+dYch5~RbZ`Df9WXLJyI8la zI!M{ol1?2gKmgd&&yxpuTEz^)DGL-}a}a@N(wKXH7nSepJaYSmPE_TJrr??MmC;38 z1KI+W$wr_*Ck`cCp{5286yRw;UzSr!9Pj7rgS)bUtuHsq2G0b}p6{5KZO87>SC>c2 zhR^8ryC2<4gwI@rM%~+9sll&hskaL-Ha0%gZPpOtRL21nnz~pS%ZB-<)Tfy=^OmdhJ&61j%Flo(1FP_p>hYPpUtf7RO4)bjt~X>A zBsDSL&+h%~8yk85;)&H7&Rf>{$gsbKVosA*gs2&q=5Tf?V#A^mG}AX6_nMb@*JCmD*}u@R@|M`g7P`3{xsHh&`lkwujZZGjFK%vbt_)Uq zNP&=@K*?b1TZ)kdHM{;xG!F<@1s1t{#E(|ty@Eg5KoAad0V(4hc{qY zHaEKv=kLiYN$FTBfajQbj>dOLC3n2Oh<4<0xo?h!Wr9l9aLIL22f}t+Lf7^E#U~Zu z5DaSme1G#8o6o0}^ZC7N-w$e2ffp$JX|b+y-qu*LI0jJJgQ|)z7p$W+PPdlI;!Bv} z_;QEuUKrbD``0Luw@>@NHPrip!d&Wf(^SGyLEWJ(np2!Bd438M7!^8O)9ha@A2R<} z^d&qxl2;~__QKtHD0rzhev3=>XvwPRHp z7uwUNvJc7v1`$}g;0gsm5fy*_gR4nsB075BF~%th|NG)nm*B(|%IJ|53W^BvDPfs% z88(ac7RTiZSe^qG#oijl%6V(@c2LL%js~LncrLsPvimOC$L-w3CR>~q^>J#ivG#X@rh?o zNGeWXetT_ZEvu*tEj(AXkdeg>28Kql)Y9jFrlseKn4>j%n_H!n2utkQYbsb(d!QGz zsk?fXSh}O?KJ+SU>if`JM#-Rk;ej-DC1@Xmdad)`I$0}%?TOzqeks@G=dEVop_oxK z50SJI+fDL;GyY9t#WF>mAhn0zr9c+*KR-RBcz!i@A}u^%p{~WguI$QwXx>YtZ}~3% zXO2NR?ZGiSlac!*1*WKQIZ!0~?%h|QEjO+=a*wlJ3ygUQfB))dnc}~W=~E8xvQN_g z$Mn?-Ai{tkjigQOoMq<&zE2y#58JyXVc2rlT-({v%Op!p+jhQBE}MkJ9)3Z$6?ou4 z92Bs;wHvJ6;Aprki-0e6uv6wfG!A5mnQJ53JbD;Z;eBsXOQvWbKkuKnARFa>(*DoD z;0vxkx%I23RiF>{S4Szpe8%tM)2$Uytkc^rw!e6$>4TD-nE{Noyng`ZB8SAYqB}

y1&gFHro(z89XU8;2UHb{r^1TvJ+wBumBGrLO zTYm%v7#li5QM-R!n?2Ve1PG>r*E%QV8!n~?@+q@!6+{7{37-UY;-xDLz;B)gN)u;Y znPI9=f+nu`SCRKCY8B(KMWGON^2oK{92#o94y|Tm9yy54kVCg&h=dpPi(G`hD=BBo zkpzwAh;m#ax170#B@kLmT#+JN zE^6UQc+v8p$+D#5MxvR6?3D|nLaLR=rt;3+|E#8__GZB106(Dvvva9{9L{4^4vEQW zzH*6o*e1B8P~@~V5;`+ygEjZtlRJ~DC=CZ9?pcn9cxIHjN)nR?t>Bry5J9iCOoO|Z zGQ7q-;ARsDWyH1`z}nj>qZD6gB0ecOB%k+ejJ$1p${rnF+2jX!EkW8ns(>M zh(k>RYSCR}l_tYinp-R=rka@6H~{Ug;3*^~_rY#yX*=*T6D%%&8rIdFQzadQ0kn=^ z_+Cb0wbkLNZPAI#*iS~mDu}RKybV8f=Zrl6opATdvTrQ#svFrS<19y>F=nZ(Z}&+R zS!jE#bSVG*pXzu<-Px%u%3V)qDNv^RX4jB=^bz{dfcYPgsu8i7+IoZ5v5dAix3Lve zLCjYX?3e&GxN$#TV*K)ne|L zp{lKlHNo!$5}kvee^6WhDD7Givn3nSxUf^qYUEplAQ(XvKH#uaCs(!SV0=B)@GJif zdS0!TxxZXFjMT>5QGQV>hI(V&Vf2qoimqv_}sSq zBWDr0yF0tD-|1koZ+pnf_*T3&H@N(}q@&8_N9sugkIf!ei>#1CmR19S^+VF^aT6F0 zNlqOuoT+)+bQcF$>L{rSihv5YZU+1RHX3|rJ$$@=!_lw%T_ZzShYDHB<{8X~Kc3yl z#N>%Cg?mSi;Mz?x`88*BPPENt9PXq(oI|#rjbEQaQq%H}68v_mH&5a-?Dn{BJ`%EP zZ>*4S7hnYuA?V;?Pa>Ri4r;#t!3b~u5SQJ8{NXB)fhcudXgXA%s5zMOvHboG`ZP^ zZdC~?`FPT6TJ3MwEadYkxR>5z4PZI^gCUU#II>T z-!<{z6vxUtZ?n)Z)e;T05PSCJe(MX;7lfXPwDQnk<+EI4zI+~7x+KSqahSuJ>`_-= zA*Nc{f#$5#ooRZ_^!{hx#j>4XhV7axa8!$lmzSu{WMt86nuL@2`I2Kjl6-$B4cI^) z?v2wgO;P&)PGmd{R!{CvP3cu_{T#;_r`L>d^*Q8pFjf_05b(MLpY9;9R-Qs~f@o|~ z9EPBxE2+JxuTDYzLVp91+`<=m%heD7M1`k$T+k0I(FO9ccIMI$jiX}0dK3__RUXM@+t~v(oT_^t0LL9 zvX~?kUqD^~JR3<{zI#)j5IYuhfVkd#R)v7XnpZQ$5)IiZV{r&2cC2#nnrdS7IHi%w za)zX(k6Z{fYAF&mdz)F%dHqJN*7wT16CM>SfsY*_vuv{7vmPIL8dV z*(c6U@1=*w@kkvdMKf31!Y}j>Ey#^CxO~Fb3zOdWngzML@(oqqK<=hYhI3TOetTB{TMmU|j8#m-XItpI*bNC%oK|=xV|+GSl7d>n4sB-v5Zs7)od>y%v+9|H&H0bQBNVg~hLp3;%2dQln2%`Q2ITS$B zECUyfO%jcvx`_50G}-Y*uAZTJn&7a?`-}fU)~{n9ZqYy(YJ`D_ol=j`X7`RfK9hM- zA9}*?qC;T6kKgTY=YG*Zuks*IMGoD2&G-W!s3~Cy4JP&pB%HIv?mCkvMp)j9#z*~= z5Eu5laeyQp`aAOBkD#SlwlyVmF^)C-IdLh#I5`B1^`pVWmm_(brQE8-imS*wW;Kh( zEvp<=o0}-&dZzSXqLNnhJ{uH|81i z@juUl4NS%bm&|G3;RB>@HcNm5o2CDw4CL80ZjNG9S@QWm zGUrAZtJ4}7P!rkpq8;w_wH_2r(dU@*(G%8rSI(XK@){6u2cc-oxHX?2&}{CsY2TgP z0<4?W71hfrz~T3&s;#U;VJi5Lgar$|DR?xy*7QH5Eh{#8NU_`7X~UFJ+uL{Un~bo9 z{bGVRDxKd&1<_V2H1};@-l2^#3c79a0zA<-*x6&gKgvYP@HRFyaB<1^_?2HBjA!SI z4;Ol2Lv12Hu_N=mFF??l=-Ylt`OrGyLvT3@gr$3EI9@t``8Q=yFB_uA06*?a=|M5T85+4 zPxNB;N4|s`1{{jWILp@a2+p=zLj{=k=a5-(b~0|2n&lq;Pba#6$eJtX@P8~3j(n!x WU*yg&_Fpd$$inoJN%aMn*#80Knb@HK diff --git a/modular_doppler/modular_vending/code/tg_vendors/sectech.dm b/modular_doppler/modular_vending/code/tg_vendors/sectech.dm index 80180368c068e3..dbda5b5508be8c 100644 --- a/modular_doppler/modular_vending/code/tg_vendors/sectech.dm +++ b/modular_doppler/modular_vending/code/tg_vendors/sectech.dm @@ -16,9 +16,9 @@ /obj/item/food/donut/jelly/berry = 1, /obj/item/food/donut/jelly/apple = 1, /obj/item/food/donut/jelly/choco = 1, - /obj/item/restraints/handcuffs/antiresonant = 6, ) premium_doppler = list( + /obj/item/restraints/handcuffs/antiresonant = 6, // anti powers cuffs /obj/item/gun/ballistic/automatic/schiebenmaschine = 30, /obj/item/gun/ballistic/avispa_stingball_shooter = 5, /obj/item/knife/combat/survival = 3, diff --git a/tgstation.dme b/tgstation.dme index 5e04e9d923aa99..757a18f31c48d7 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7468,6 +7468,7 @@ #include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm" #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" +#include "modular_doppler\modular_powers\code\cargo\antiresonant_cuffs.dm" #include "modular_doppler\modular_powers\code\cargo\reality_anchor.dm" #include "modular_doppler\modular_powers\code\cargo\thaumaturgic_supplies.dm" #include "modular_doppler\modular_powers\code\powers\mortal\augmented\_augmented_power.dm" From 3ad9e80619a73fcee52a256857b5acb7f2cc4dcf Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 30 Mar 2026 07:52:11 +0200 Subject: [PATCH 167/212] Adds a species blacklist. Blacklists holosynth from shapechange cause its so fucking buggy. Psykers don't need hands anymore. --- modular_doppler/modular_powers/code/_power.dm | 9 ++-- .../powers/resonant/aberrant/shapechange.dm | 1 + .../powers/resonant/psyker/_psyker_action.dm | 3 ++ .../modular_powers/code/powers_prefs.dm | 2 +- .../code/powers_prefs_middleware.dm | 30 ++++++++++++- .../modular_powers/code/powers_subsystem.dm | 44 ++++++++++++++++++- 6 files changed, 81 insertions(+), 8 deletions(-) diff --git a/modular_doppler/modular_powers/code/_power.dm b/modular_doppler/modular_powers/code/_power.dm index 466e83f50aafee..3fab9590676da6 100644 --- a/modular_doppler/modular_powers/code/_power.dm +++ b/modular_doppler/modular_powers/code/_power.dm @@ -13,6 +13,10 @@ var/mob/living/power_holder /// if applicable, apply and remove this mob trait var/mob_trait + /// Species that cannot pick this power. If species_blacklist_is_whitelist is TRUE, only these species can. + var/list/species_blacklist + /// If TRUE, species_blacklist becomes a whitelist. + var/species_blacklist_is_whitelist = FALSE /// Amount of points this trait is worth towards the hardcore character mode. /// Minus points implies a positive power, positive means its hard. /// This is used to pick the powers assigned to a hardcore character. @@ -315,11 +319,6 @@ GLOBAL_LIST_INIT_TYPED(all_power_constant_data, /datum/power_constant_data, gene SIGNAL_HANDLER update_process() -/// If a power is able to be selected for the mob's species -/datum/power/proc/is_species_appropriate(datum/species/mob_species) - if(mob_trait in GLOB.species_prototypes[mob_species].inherent_traits) - return FALSE - return TRUE /** * Handles inserting an item in any of the valid slots provided, then allows for post_add notification. 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 4170cfe7b14b43..035981d2234703 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm @@ -10,6 +10,7 @@ \n If the creature dies or the effect ends, you are reverted to your normal form (prone on the ground), and all damage taken is transfered to your original form (halved if reverting back manually)." security_threat = POWER_THREAT_MAJOR value = 5 + species_blacklist = list(/datum/species/android/holosynth) // there are SO MANY BUGS with holosynths I'd rather just NOT. required_powers = list(/datum/power/aberrant_root/beastial, /datum/power/aberrant_root/monstrous) required_allow_any = TRUE 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 211d474a063abb..fe46ca50198e27 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 @@ -4,6 +4,9 @@ overlay_icon_state = "bg_hive_border" button_icon = 'icons/mob/actions/backgrounds.dmi' + // We're a psychic we don't need hands. + need_hands_free = FALSE + // The organ that processes most of the Psyker Powers. Mostly all functions here communicate with this. var/obj/item/organ/resonant/psyker/psyker_organ diff --git a/modular_doppler/modular_powers/code/powers_prefs.dm b/modular_doppler/modular_powers/code/powers_prefs.dm index fb38bcdf155e39..bc0e34aff2e38e 100644 --- a/modular_doppler/modular_powers/code/powers_prefs.dm +++ b/modular_doppler/modular_powers/code/powers_prefs.dm @@ -38,7 +38,7 @@ /datum/preferences/proc/sanitize_powers() - var/list/new_powers = SSpowers.filter_invalid_powers(all_powers) + var/list/new_powers = SSpowers.filter_invalid_powers(all_powers, parent) var/list/powers_removed = SSpowers.powers_removed var/invalid_reason = null diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index 7b445560436c8f..696fea44b470b8 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -41,10 +41,13 @@ return data current_points += power_type.value + var/datum/species/mob_species = preferences.read_preference(/datum/preference/choiced/species) + for(var/power_name in SSpowers.powers) var/datum/power/power_type = SSpowers.powers[power_name] var/has_given_power = (power_name in preferences.all_powers) + var/species_allowed = is_species_appropriate(power_type, mob_species) // TODO: GRAY OUT powers you: // Don't have the requirements for. @@ -57,7 +60,7 @@ if(get_requiring_power(power_type)) locked_in = TRUE else - if(get_incompatible_power(power_type) || length(get_required_power(power_type)) || would_exceed_path_limit(power_type)) + if(!species_allowed || get_incompatible_power(power_type) || length(get_required_power(power_type)) || would_exceed_path_limit(power_type)) locked_in = TRUE var/state @@ -192,6 +195,12 @@ if(power_name in preferences.all_powers) return FALSE // Already have this power. + // Cehcks against the species blacklist. + var/datum/species/mob_species = preferences.read_preference(/datum/preference/choiced/species) + if(!is_species_appropriate(power_type, mob_species)) + to_chat(user, span_boldwarning("[power_name] is not available to your species!")) + return FALSE + // Make sure we don't exceed 2 distinct paths. if(length(preferences.all_powers)) var/list/unique_paths = list() @@ -240,6 +249,25 @@ preferences.all_powers += power_name return TRUE +/// If a power is able to be selected for the mob's species. +/datum/preference_middleware/powers/proc/is_species_appropriate(datum/power/power_type, datum/species/mob_species) + if(isnull(mob_species)) + return TRUE + // Gets the power from the power_species_restriction global list if its in there. + var/list/species_restrictions = GLOB.powers_species_restrictions[power_type] + if(!islist(species_restrictions)) // not in there? cool skip this step. + return TRUE + var/list/species_blacklist = species_restrictions["list"] + var/is_whitelist = species_restrictions["whitelist"] + if(!islist(species_blacklist) || !species_blacklist.len) + return TRUE + var/is_listed = (mob_species in species_blacklist) + // whitelist inverts + if(is_whitelist) + return is_listed + // if its in there, yes/no. + return !is_listed + // A lot of validation specifically for augmented, given they're very snowflakey in their restrictions. /datum/preference_middleware/powers/proc/validate_augment(datum/power/power_type, power_name, mob/user) if(!ispath(power_type, /datum/power/augmented)) diff --git a/modular_doppler/modular_powers/code/powers_subsystem.dm b/modular_doppler/modular_powers/code/powers_subsystem.dm index 71c5b50a8e560f..389fba223e2d1a 100644 --- a/modular_doppler/modular_powers/code/powers_subsystem.dm +++ b/modular_doppler/modular_powers/code/powers_subsystem.dm @@ -8,6 +8,8 @@ GLOBAL_LIST_INIT(powers_requirements_list, generate_powers_requirements_list()) GLOBAL_LIST_INIT(powers_inverse_requirements_list, generate_powers_inverse_requirements_list()) +GLOBAL_LIST_INIT(powers_species_restrictions, generate_powers_species_restrictions()) + /proc/generate_powers_requirements_list() var/list/requirements_list = list() var/list/all_powers_list = subtypesof(/datum/power) @@ -40,6 +42,21 @@ GLOBAL_LIST_INIT(powers_inverse_requirements_list, generate_powers_inverse_requi return inverse_requirements_list +// Gets all the powers that have a blacklist. +/proc/generate_powers_species_restrictions() + var/list/restrictions = list() + for(var/datum/power/power_type as anything in subtypesof(/datum/power)) + if(initial(power_type.abstract_parent_type) == power_type) + continue + var/datum/power/power_instance = new power_type + if(islist(power_instance.species_blacklist) && power_instance.species_blacklist.len) + restrictions[power_type] = list( + "list" = power_instance.species_blacklist, + "whitelist" = power_instance.species_blacklist_is_whitelist, + ) + qdel(power_instance) + return restrictions + //Used to process and handle roundstart powers // - Power strings are used for faster checking in code @@ -124,12 +141,13 @@ PROCESSING_SUBSYSTEM_DEF(powers) /// and returns a new list of powers that would be valid. /// If no changes need to be made, will return the same list. /// Expects all power names to be unique, but makes no other expectations. -/datum/controller/subsystem/processing/powers/proc/filter_invalid_powers(list/powers_to_check) +/datum/controller/subsystem/processing/powers/proc/filter_invalid_powers(list/powers_to_check, client/applied_client) powers_removed = list() var/current_balance = 0 var/maximum_balance = MAXIMUM_POWER_POINTS var/list/intermediary_powers = list() var/list/all_powers = get_powers() + var/datum/species/mob_species = applied_client?.prefs?.read_preference(/datum/preference/choiced/species) // Track distinct paths we accept while filtering this batch var/list/unique_paths = list() @@ -142,6 +160,11 @@ PROCESSING_SUBSYSTEM_DEF(powers) if(!ispath(power_type)) continue + // Checks against hte power's species blacklist. + if(!isnull(mob_species) && !is_species_appropriate(power_type, mob_species)) + LAZYADD(powers_removed, "[power_name] is not available to your species.") + continue + // Checks if the power exceeds the max. current_balance += power_type.value if(current_balance > maximum_balance) @@ -229,3 +252,22 @@ PROCESSING_SUBSYSTEM_DEF(powers) if(intermediary_powers.len == powers_to_check.len) return powers_to_check return intermediary_powers + +/// If a power is able to be selected for the mob's species. +/datum/controller/subsystem/processing/powers/proc/is_species_appropriate(datum/power/power_type, datum/species/mob_species) + if(isnull(mob_species)) + return TRUE + // Gets the power from the power_species_restriction global list if its in there. + var/list/species_restrictions = GLOB.powers_species_restrictions[power_type] + if(!islist(species_restrictions)) // not in there? cool skip this step. + return TRUE + var/list/species_blacklist = species_restrictions["list"] + var/is_whitelist = species_restrictions["whitelist"] + if(!islist(species_blacklist) || !species_blacklist.len) + return TRUE + var/is_listed = (mob_species in species_blacklist) + // whitelist inverts + if(is_whitelist) + return is_listed + // if its in there, yes/no. + return !is_listed From 5d7a8cc1a764391c28ccb751a7552313c1300ab8 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 1 Apr 2026 22:38:20 +0200 Subject: [PATCH 168/212] Fixes a lot of shapechange related bugs. Adds thaumaturge robes for sec and engi. --- .../code/cargo/thaumaturgic_supplies.dm | 8 ++- .../powers/resonant/aberrant/shapechange.dm | 16 +++-- .../thaumaturge/affinity/thaumaturge_robes.dm | 62 ++++++++++++++++++ .../icons/items/thaumaturge_robes.dmi | Bin 0 -> 3941 bytes .../icons/items/thaumaturge_robes_digi.dmi | Bin 0 -> 3017 bytes .../code/tg_vendors/wardrobes.dm | 6 +- tgstation.dme | 1 + 7 files changed, 85 insertions(+), 8 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_robes.dm create mode 100644 modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi create mode 100644 modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi diff --git a/modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm b/modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm index 4fd9e8c0e061e0..b09f2278dfb6f8 100644 --- a/modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm +++ b/modular_doppler/modular_powers/code/cargo/thaumaturgic_supplies.dm @@ -19,15 +19,19 @@ /obj/item/clothing/head/wizard/marisa/fake, /obj/item/clothing/head/wizard/tape/fake, /obj/item/clothing/head/wizard/chanterelle, + /obj/item/clothing/head/wizard/secwiz, + /obj/item/clothing/head/wizard/viszard ) var/list/robe_pool = list( /obj/item/clothing/suit/wizrobe/fake, /obj/item/clothing/suit/wizrobe/marisa/fake, /obj/item/clothing/suit/wizrobe/tape/fake, + /obj/item/clothing/suit/wizrobe/secwiz, + /obj/item/clothing/suit/wizrobe/viszard ) // There's a small chance that we manage to sneak in real wizard robes. - var/real_robe_set_chance = 3 + var/real_robe_set_chance = 5 var/list/real_robe_sets = list( list( /obj/item/clothing/suit/wizrobe/magusblue, @@ -35,7 +39,7 @@ ), list( /obj/item/clothing/head/wizard/magus, - /obj/item/clothing/suit/wizrobe/magusblue, + /obj/item/clothing/suit/wizrobe/magusred, ), list( /obj/item/clothing/head/wizard/black, 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 035981d2234703..1380f61b997779 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange.dm @@ -87,7 +87,7 @@ return DISPEL_RESULT_DISPELLED return NONE -// Special checks to do with hunger. +// Special checks because changing mobs like this is apparently quite janky. /datum/action/cooldown/power/aberrant/shapechange/can_use(mob/living/user, atom/target) . = ..() if(!.) @@ -95,15 +95,21 @@ if(user.IsStun() || user.IsKnockdown()) owner.balloon_alert(user, "stunned!") return FALSE - // Allow reverting even if starving. - if(user.has_status_effect(/datum/status_effect/shapechange_mob/aberrant)) - return TRUE + // Can't shift while being held as an item (e.g. undersized in-hand). + if(istype(user.loc, /obj/item/mob_holder)) + owner.balloon_alert(user, "can't while held!") + return FALSE + // Can't shift while ventcrawling; it breaks transfer into the new mob. + if(user.movement_type & VENTCRAWLING) + owner.balloon_alert(user, "can't while ventcrawling!") + return FALSE // We shouldn't have any active powers because it will make this power 10x more glitchy. This checks against it. var/datum/action/cooldown/power/blocking_power = get_blocking_active_power(user) if(blocking_power) owner.balloon_alert(user, "active: [blocking_power.name]") return FALSE - if(user.nutrition <= NUTRITION_LEVEL_STARVING) + // 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 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_robes.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_robes.dm new file mode 100644 index 00000000000000..dfab8726780678 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_robes.dm @@ -0,0 +1,62 @@ +/// Special job specific robes that give affinity. + +// Viszard; affinity 4 body, no bonus perks besides not being flammable. +/obj/item/clothing/suit/wizrobe/viszard + name = "vizard robe" + desc = "Most people would think this is a high-vis raincoat, but those fools are WRONG. These are the proud garments of a thaumaturge. They look the same? Well, one is worn in rainy weather, and the other is the highly regarded robes of the great thaumaturges that can untangle the 'M6 Spaghetti Junction' with the flick of a wrist. " + icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' + worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' + icon_state = "hivizmob" + inhand_icon_state = "hivizobj" + armor_type = /datum/armor/none + cold_protection = CHEST|GROIN|ARMS|HANDS|LEGS + heat_protection = CHEST|GROIN|ARMS|HANDS|LEGS + affinity = 3 + supported_bodyshapes = list(BODYSHAPE_HUMANOID, BODYSHAPE_DIGITIGRADE) + bodyshape_icon_files = list(BODYSHAPE_HUMANOID_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi', + BODYSHAPE_DIGITIGRADE_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi') + +// Viszard; affinity 3 head, no bonus perks besides not being flammable. +/obj/item/clothing/head/wizard/viszard + name = "viszard hat" + desc = "An incredibly obvious wizard hat; as if the pointiness wasn't obvious enough. Despite being granted to the Engineering department, it does not pass as a helmet for workplace safety standards, so please beware falling objects. However, it is fireproof." + icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' + worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' + icon_state = "hivizhat" + inhand_icon_state = "hivizhatobj" + armor_type = /datum/armor/none + fishing_modifier = -3 // high vishing + affinity = 3 + +// Secrobe; affinity 3 armor. Has the stats of a secjacket and covers the legs, and also has affinity, but also has a slight amount of slowdown. +/obj/item/clothing/suit/wizrobe/secwiz + name = "security thaumaturge robe" + desc = "The garments of a security-contracted Thaumaturge. The robes have been reinforced and provide a high amount of protection across a large degree of the body, at the cost of being bulkier. The proportion of armor to robe has been fine-tuned for the most optimal results; it seems that armored wizards isn't particularly popular in the zeitgeist, reducing the impact of armor on robes." + icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' + worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' + icon_state = "secrobemob" + inhand_icon_state = "secrobesobj" + armor_type = /datum/armor/armor_secjacket + cold_protection = CHEST|GROIN|ARMS|HANDS|LEGS + heat_protection = CHEST|GROIN|ARMS|HANDS|LEGS + resistance_flags = FLAMMABLE + affinity = 3 + slowdown = 0.2 + supported_bodyshapes = list(BODYSHAPE_HUMANOID, BODYSHAPE_DIGITIGRADE) + bodyshape_icon_files = list(BODYSHAPE_HUMANOID_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi', + BODYSHAPE_DIGITIGRADE_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi') + +// Secrobe; affinity 3 head, no bonus perks besides not being flammable. +/obj/item/clothing/head/wizard/secwiz + name = "security thaumaturge hat" + desc = "A wizard's hat, painted in the colors of the security department. Jokingly referred to as the Magic Police, Thaumaturges experience an unique skillset that is very useful by Security Officer. Given their requirements to dress \ + for their powers, security has commissioned these special hats." + icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' + worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' + icon_state = "sechat" + inhand_icon_state = "sechatobj" + armor_type = /datum/armor/none + fishing_modifier = -2 + resistance_flags = FLAMMABLE + affinity = 3 + diff --git a/modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi b/modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi new file mode 100644 index 0000000000000000000000000000000000000000..4168ba412f4520d5362b7372569777ad3c107d83 GIT binary patch literal 3941 zcma)<`#%$k|HmihE;@3*lU&LrG;-S*xn%B_I!Ho{Eq5`-jEu;V`~8+XtK6ep(xD^w z%gCh>%Ketn#_Zeo^V{dQ&*SlWzhD2r^L=?GSzDTLu?w>U001sCQzP3yUh=0ftjvFU zipN8dKlT!3e>cR)E7&6l9uNZee+&SG7uY!l1cja)*##VsR~7FlDUJ+{+P!g59amra zetpzJ;a@;d%JX(DbzAjGb#uvr?=L^Sw3oC~kZHQ!qzG2Hla#D3+f*$m@A>(kT*r)@ ze0KM8#i3yEjfbFQr{#Ekj&e7Q!k^)M5C7WdMC#Kaxeo945()b z65ZCxT>}6vrkWWU*oPPF6uKh#2KWY8HeX#Zg!N$ko^kbJ3Q8*B1*|FZz7@5y<~4t@ z8Q0ccRoj5ptyZWwiXZ-^zihMcHsr}WR-cQ-qmXJ-x9tVh^pi#^APOk zrqbeyr&3K)i^vn~&IY8R4$_~jc@LVsChwHr6)@cYyRmQ*Qy*G zh!9=Tp2;oYJ5uNgCsuF`WW>sgbWUmRK%k!zp5Z_sCc{KdPA;D`WrSqpvy2b!fZcB& z+Nfh5dn4K=2K&1@*?yU$z2tTkyi^0h>YjhlA!?YptLj(#NJWFW0R2nt6BYliFHj=Dzz`K8J1S zeE{o5-6j3R62~Q;>7RtZdIv4d8;~BBElfKWsz9N%%h;_qKa1eXx2WY?iyOh9Bd(26 zW=2N;%^A%fd)7|fdA3e6I*;8@x+w4LVAqkIJH^R^qBFgR6fxh!{XS&fU`J<_$bbH;i1D zJ^f?&iGX-^GxnU?S>z~}>RMF6jhwjR956&q6S69dc$?A1*0`B&^}f=ryMylZH}^3o z=l0moF?pBGuNbSbB>*-Ru@uI2ad!{_^<|E?V`08{`*m1tr1G}Sip~7%>(5Tl%-34x zLX!ntX5#36Vh5OJza8Pim-qz)pD`RrD=GpTFK#k!77;qmzXScGacvttMkO%IUHr%l zS;f`b<`K%y`(d_ZU6Ho>D>pFZ(~E;B#vEwFO1-?H(=cZ>Ub zpuDg%V&&MSI9nJ)-u<~IKP~RGVpqN?hvziPm9^|<>cDWJ&Bf3$ZW(2fcTZUDwO&YA11;a1#dr zP}}J}cm6p-ei8~T+@-cy-A_UX1$lNIvru~F#*770cF*UIxX3uChcU%k{G+#5hp9C} zJR>*{x>^WKqVHXleu`FL%6-?Q3)YM8&+7>owHNvh5BSlTEt>-`GIA`w?c$KRGgAA) z&jC4Qof&n5{oM3L4HaQuVdm%`YqBvjTT(hn4AoEX?n~7jOeg&NZ#W;8*C{-T8|ZFR z^+Nwvo=R`UhF!l@RmM0zA@lF#REgPU1hJ8~n1d029@L}+7tmA;)IOpc@>h_sR6Yc zcmIA`V*a!4FU)SBSQOx1&Y;Pc2kUG;@DWWAtdZr<$S>Qe-nwzlZ%Whkr-{y_`pcrD zi?FHNOG2MbmM%qYi)e4j8pN{9ztPRZEx`H_JNHPz#!X~4=Y8Ci=1e!#H~(+1E_R|S z;~XSw`K$a!fl6LQaaQ0ugA~(SvA_x$LB&k3A?EG?@Grx5IYM@F z%la$JijwfjL+ym4^!|_fQm|Qo^SaBoK%XC+g5XSIG5bahcUk!JUtP@K8cIfE#etFP z{q9E&8i!^+I;P3<)~gImtAy*E{YV?}+Qa-Bf8Tr(^D&a~$Y$LB;0NVJH=9{wuP!Ok zuz!MA>K533zXhuwBm9FZY0uca@xoO~*!`vDOXPoFi2pwr!Rdxyu7bB++&(;Uf6qx? z&OT6^au%=xI%DBL^tL-x{R=npF_cxOdb_8&G%<{@Hv$oz_qf(wj>=vr)lBp#KRFZ* zxVAxm?_ms+5Lu#zldAMtysGrtAL1!VH{2e(-)nI46mF5^;mMa_GfxGVy zFU4oLmqNn;zZutXFsY6Q8QB_akL`*YHMgU8E-EW^{Ids(ZZmq<+85KdwKj!SvF+ z@FR#;Zdra@mQ{Yzt=kIFYnv)!r&UxFt24RJZwB6~(5~bUS$O=4Q?XDIj=ohZrt zBfs8!4=wEW!G{c9G&8V^cbvlY349Qs!_~GG1f$mFz8Z1s_Yg&XC-@Q7q8Wl{%7U&# z5jQ(IR(mlg(;vUn)xSHAyYW4zR60~qiNB5Y&L@q+eiP+}5R4M566XReU7G2T@!Q!s z48sSbCT?>vr_WM)dP6f_!m*$>2Y%YCw;46~EtjqQ9?Gn$WYx@7FLL_QBt-sNW%&dO zD6x*gYm~yyf3&ViwQ#foCF0*6+z>333Fpb`<&U~|6v2}!0+nv0hQH&g<0n zk%7ycZBGXiwuK*V%Ch;`C8!9;Co$xlFd|(cxmo7aJM-C2Z`a1FB3;(K{|Tj}_h5>N zA1(o=Xwtm63x7{>+3`%Uo87lnmn>+zA#Cq&A$T|c9wv38sBy#}NNbj~m|)Sz;v26W z4-Z9M_2mAVg-fvNT{S9vsvRj3Em_1w%6qpubP()HV&Y5T^wdvED0p_G#m7P)q+N8{ z6hsfbo;c6EAo(eM)@7z@X(ewM#SRjIq`5-~fGfS@n4LMkHEA3e$%| z(F9;l)^sIL4u}cPDwyIy7+z?!$V}`KLE2s0IHT8bwM2lrA_Vwod0}}lsiR56euE0D zrNYhorCur@PXs?tF|Mra0bO~GuW1grlp-Y!Pf57w<(LM>z%Es8>l zYsnFiTmM`({soAqlt!|p@{k(g&g20w+Q|R?g(yy=;n7c9SI}-_lKJyS)Hkx8JPF6$ zRz8yKZ@RB$;T@&a+gzCGUMo)Sc}L zWr$oq>5U%^6l&`|0@7>Bu8HLB#1vDx{7c>n^s+29vvU3ZuI0M-5P}FpR$g^Kw`S&8 zAzZ+8Tvm*4_=kc#x6|D2WBnFx=~+Ee|r^x140Q z>l3ACuo+I$0iiu2Mz(PwHevbt1NGU`)Lb>mV>A40E9YA6ceeQ{6MmODh=Gr?=2wmO z56W0TWIi1k9k>%i@b7CozYd)!I`THjHhm-3`m{h9;>(MUNOWWK`Pl=l3o)xP?4r&`iaZF|1+~UpC*PBmU zV*{4yA7dl}*2mD(T@8MhTX9balRuXPwW`;>TO0KYxL7=7V2C$;xFRm`sIkgJTyt!| z8r_hA^?=Z_Q)>&Kod5ISuRA6x8Sx|#tCpZ!{bB3mN#CHuj_AE!r7u_}08k>6*Q@fd zPe3Yetb!g~wY%7P;@F1Va?;ZiYY9F*7ITb?>%@jI4inqHs)9fjC;6Hrv8jaESg{t5 zSykPrg-!{VQ}870{cF;grpBFug1x5wKWp6@81R5warqlL()VX8m>R3;+|E_0Vws9~ zE!HV4LOqJ<6RAFx`VA1Ifo)Z_0=FAGLYALJ!)7|eC4GSC==XmDP{%xQii@!^4x>lF zzUa_U?1cAy9&c*#gSlmL8RpYVxvNE(jcLWGScS)i{V$|cv6veK4u5{uz0)ZcjbrJm z^6xeqyj45&7b2}ZtegTM>sxG(TZ2Qjm9z_MGifN~dy zt{Q(on?F?8O3YU3H!e*uvl|Yo!G#y@3z+=}ib>lDTJhKR+p(tYrl)YKS@CeEf&^8! zDZXH9bw4G;^FLr%p1P3E1^!1Om<)1O&IEX?g?uf$5~cs-W`LQor4hmK0rvj@ai*)` literal 0 HcmV?d00001 diff --git a/modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi b/modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi new file mode 100644 index 0000000000000000000000000000000000000000..24986bca021914042a592e674ec5527f5dae5900 GIT binary patch literal 3017 zcmV;)3pVtLP)V=-0C=2JR&a84_w-Y6@%7{?OD!tS%+FJ>RWQ*r;NmRLOex6#a*U0*I5Sc+ z(=$pSoZ^zil2jm5DI>Eivnn?~Nr{UyC9|j)q>qa;ttc@!6~s2=;>^j1MQZhGPAt&I(vzM{83c>hDa{?VhSoSxM#KV|3yKX}`fqtBL@ z=i3vd@@3ROT<4SD#Q3h4qc-o?&PIUykzb5zBiLePs|yDE zf1~#9RqD<=_o(A1r`7t}#8pA6(Gp*Ca&|$ldH(R>1NuH6SmJ*vdhT?@IjPXh*@$y} zE#iJxso~*jq!gtN{_Q|?-9&V~KYoeP@t5?ofvKsN_4bv%Np!1#=$Jl-h7wQ=@b-xrHBjBCzPVl(?rDqP1fuFEKmLQ>etzE%G#})dnwr$d z;1sreO;ZpWg>Zv1Z{G5h8mhjiJ?q}P_bDib3-)mw=W!jb$9+;sF2Ma)`u?cXisXk= zKoL7ep3-aVOMYCB``YC<0S*`y3wU?k`X}|-;;*BD$9ME{;^=p+pV>48ErtKHYW~u+ znp>DrcilG?d3My<^;MJW5Q3xQN*y`2HwweCXn#T-^pFP3^h5p)o2ohuAq8TM^fNlX zknk6khLD1mjJikjJ^a%todSVbor-U=dS=T$0G57!pHhp9aVH1?(7@TNd7sA!T!($! zcW}RzEV=m)A5Kq!Mes}Txd3EZ;-2_|9C?vx01iFT!^4vvd+gK-Om+tQ`=U@&%VvOs z{jqXBt6ovRdgAm7+&e#ieLN&ps^Ky|=DdUagZAINLr(Pp zC>DkWFgzxw2g*I(Cd9GA3zi;bzW~n`7zgr&S|LSs|Yj#iy6vhqX~Mwuf}IRi~KSQTHWIV=<{HN z*bp%=*f3?t3#-@BK$hZLo%4;f2A9cizW)NZOUi9I4@d)uF!^b9%r}~aI8#9xh~ zSu&)7a&=OMfPjF2fPjF2fPjF2fPn6YQ?-Iw6VG3R%Bs=-R&AuQV|Dx*;`qn$Ylv6Q zo-FMZ0Qj+n4DIq;zm5iEMFif%xc(Ys3p5VvGr^80wrMKxgz<$SKXNKV*w;T z&f7lI`Ue?!Ick06F;*ZTI*N({Hhkv7N9xb-|3`5ffEPjuFo;qkjUu7}%DsE*=7irw z%)c3%jlV9O%gt{c0Z=l$o^|nfBguOCqH6P{@=w(-s8g%fCQ5+vv5(_8kL!F#f>b_u zDwmfV0Q=;}bvBV;Z2%}Awea#+Ap9bQVqDbD`-Z9fq?b$S6WydG;Fb1{N-3c6+h6~b{XBnourFC9?#?3zsK=21v%TcFwsQu)BA^+omN z%79W|8_*5FKA_Ln-%C11AAn*_{>`;DG5^9jz5aAasWWGzbQncq!oLcA#JQHww>AKZ zVp$CgrK3h370hPQst<6qL(luDenzdS=scS!T>|Ipt1CJsY^|(Us@7hA(2W1)**EoZ ztdCVUI`dnnAk@IK9v-#vsF8>ASvT*yBpi*@jWGbv<6;8W?4KHS);ySnj&-@9N002esnkvhits-X~;Z;C%>x9quWhcV-#Q7eAt*|Bbj59qan4 zbf+`k{pz@JI<<#}Nr9NbT~QfYGsF_{(h*dHJ1Y!saLX zn0(+u09e`x5o z#@N`HRyaH^Z4~tNopAUVd}dn}?ss!|#-W`1X^PY4{!hOS^@tCLsLws8idX(Xp9c z$bH_^ZWJ(;Wg&|n$ZLG32;e`ZJoDrUy-&sC7-}Ycf{oB#T^nn8Jw_t0)Iac+ev0O| ziU5A7VOAr%{#A<$;ALqY9*{LZ=w^PW6hz3BQ%03C8#psFlN@8%>oIr_@;e)VZ$l9g z9Dd>$`UKlA1#Lhjl;Q1V@H>x!09hmNy8||{80Xanr~49629CoE7d__xGWeZ;6Q=Tc zJgauzH_#cTDWOQ^7s>DRfd#0rE*1dKK0YpsR&NZN9A0FT(?4j-@YTC<6>qxxCS;HoqHSB`iy>&Duv)4e$8) zy|$_l@`9BLcI(0Q~wx|206gm{r1M^aCabWS~5ht(Rcm2}cd&cTohk%N&ZD zCF1;MtJjndSQ`KhO;a{Kb8ha9!a8R@J1x>6Q<-Vd7o6q+K$;TDsL_{y7a--x`e>rt zk=t8Ykcuw_*=SQ4WmG7e)yYx@7=a&8 Date: Wed, 1 Apr 2026 22:39:34 +0200 Subject: [PATCH 169/212] fuck it, better fishing modifier --- .../sorcerous/thaumaturge/affinity/thaumaturge_robes.dm | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 dfab8726780678..9b8fe526aed50f 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,6 +11,7 @@ armor_type = /datum/armor/none cold_protection = CHEST|GROIN|ARMS|HANDS|LEGS heat_protection = CHEST|GROIN|ARMS|HANDS|LEGS + fishing_modifier = -6 // high vishing affinity = 3 supported_bodyshapes = list(BODYSHAPE_HUMANOID, BODYSHAPE_DIGITIGRADE) bodyshape_icon_files = list(BODYSHAPE_HUMANOID_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi', @@ -25,7 +26,7 @@ icon_state = "hivizhat" inhand_icon_state = "hivizhatobj" armor_type = /datum/armor/none - fishing_modifier = -3 // high vishing + fishing_modifier = -5 // high vishing affinity = 3 // Secrobe; affinity 3 armor. Has the stats of a secjacket and covers the legs, and also has affinity, but also has a slight amount of slowdown. @@ -42,6 +43,7 @@ resistance_flags = FLAMMABLE affinity = 3 slowdown = 0.2 + fishing_modifier = -3 supported_bodyshapes = list(BODYSHAPE_HUMANOID, BODYSHAPE_DIGITIGRADE) bodyshape_icon_files = list(BODYSHAPE_HUMANOID_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi', BODYSHAPE_DIGITIGRADE_T = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes_digi.dmi') From 689d9865a73f79cf18bfa9584ff8e0535fb9e36e Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 1 Apr 2026 22:42:53 +0200 Subject: [PATCH 170/212] typo that will kill me if I don't fix it --- .../sorcerous/thaumaturge/affinity/thaumaturge_robes.dm | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 9b8fe526aed50f..edaa0e8abc99c6 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 @@ -32,7 +32,7 @@ // Secrobe; affinity 3 armor. Has the stats of a secjacket and covers the legs, and also has affinity, but also has a slight amount of slowdown. /obj/item/clothing/suit/wizrobe/secwiz name = "security thaumaturge robe" - desc = "The garments of a security-contracted Thaumaturge. The robes have been reinforced and provide a high amount of protection across a large degree of the body, at the cost of being bulkier. The proportion of armor to robe has been fine-tuned for the most optimal results; it seems that armored wizards isn't particularly popular in the zeitgeist, reducing the impact of armor on robes." + desc = "The garments of a security-contracted Thaumaturge. The robes have been reinforced and provide a high amount of protection across a large degree of the body, at the cost of being bulkier to move in. The proportion of armor to robe has been fine-tuned for the most optimal results; it seems that armored wizards aren't particularly popular in the worldly zeitgeist, reducing the impact of armor on robes." icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' icon_state = "secrobemob" @@ -51,7 +51,7 @@ // Secrobe; affinity 3 head, no bonus perks besides not being flammable. /obj/item/clothing/head/wizard/secwiz name = "security thaumaturge hat" - desc = "A wizard's hat, painted in the colors of the security department. Jokingly referred to as the Magic Police, Thaumaturges experience an unique skillset that is very useful by Security Officer. Given their requirements to dress \ + desc = "A wizard's hat, painted in the colors of the security department. Jokingly referred to as the Magic Police, Thaumaturges experience an unique skillset that is very useful to have as a Security Officer. Given their requirements to dress \ for their powers, security has commissioned these special hats." icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' From 44aab997c490a68fc5f3b35db58227d723c8fc77 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 2 Apr 2026 07:23:17 +0200 Subject: [PATCH 171/212] Adds beter feedback to theologist, fixes a bure with webs requirements, removes permitted cybernetics (and moves nutrient pump to powers) and added TK to the genemod quirk blacklist. --- .../genemod_quirk/code/genemod_quirk.dm | 2 +- .../mortal/augmented/simple_augments.dm | 6 ++ .../aberrant/web_crafter/binding_webs.dm | 2 +- .../aberrant/web_crafter/snare_webs.dm | 2 +- .../aberrant/web_crafter/tripwire_webs.dm | 2 +- .../theologist/_theologist_root_revered.dm | 1 + .../theologist/_theologist_root_shared.dm | 8 +- .../permitted_cybernetic/code/preferences.dm | 20 ----- .../permitted_cybernetic.dm | 79 ------------------- tgstation.dme | 2 - 10 files changed, 13 insertions(+), 111 deletions(-) delete mode 100644 modular_doppler/modular_quirks/permitted_cybernetic/code/preferences.dm delete mode 100644 modular_doppler/modular_quirks/permitted_cybernetic/permitted_cybernetic.dm diff --git a/modular_doppler/genemod_quirk/code/genemod_quirk.dm b/modular_doppler/genemod_quirk/code/genemod_quirk.dm index 7ce81473fcb711..1c399a928e1a82 100644 --- a/modular_doppler/genemod_quirk/code/genemod_quirk.dm +++ b/modular_doppler/genemod_quirk/code/genemod_quirk.dm @@ -33,7 +33,7 @@ can_randomize = FALSE /proc/generate_genemod_quirk_list() - var/list/stuff_we_dont_want = list(/datum/mutation/self_amputation, /datum/mutation/hulk, /datum/mutation/clever, /datum/mutation/blind, /datum/mutation/thermal, /datum/mutation/telepathy, /datum/mutation/void, /datum/mutation/badblink, /datum/mutation/acidflesh) + var/list/stuff_we_dont_want = list(/datum/mutation/self_amputation, /datum/mutation/hulk, /datum/mutation/clever, /datum/mutation/blind, /datum/mutation/thermal, /datum/mutation/telepathy, /datum/mutation/telekinesis, /datum/mutation/void, /datum/mutation/badblink, /datum/mutation/acidflesh) var/list/genemods = list() for (var/datum/mutation/mut as anything in subtypesof(/datum/mutation)) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm index 723e35c7a312e2..bd753d65f0bf7c 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/simple_augments.dm @@ -68,6 +68,12 @@ The game sometimes calls this spine. value = 3 augment = /obj/item/organ/cyberimp/chest/spine +/datum/power/augmented/nutriment_pump + name = "Nutriment Pump Implant" + desc = "This implant will synthesize and pump into your bloodstream a small amount of nutriment when you are starving." + + value = 3 + augment = /obj/item/organ/cyberimp/chest/nutriment /* EYE HUDS. Keep in mind these are HUDS. Not actual eye replacements. 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 d5ae6bbb06ece4..628d37195ed9a7 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 @@ -6,7 +6,7 @@ security_threat = POWER_THREAT_MAJOR value = 3 - required_powers = list(/datum/action/cooldown/power/aberrant/web_crafter) + required_powers = list(/datum/power/aberrant/web_crafter) /datum/power/aberrant/binding_webs/post_add(client/client_source) . = ..() 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 8e89242ae6a015..b099598ef44bbb 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 @@ -8,7 +8,7 @@ security_threat = POWER_THREAT_MAJOR value = 3 - required_powers = list(/datum/action/cooldown/power/aberrant/web_crafter) + required_powers = list(/datum/power/aberrant/web_crafter) /datum/power/aberrant/snare_webs/post_add(client/client_source) ..() 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 a99f26c382676a..47b6b264bead8f 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 @@ -6,7 +6,7 @@ security_record_text = "Subject can craft tripwires from their spider silk." value = 3 - required_powers = list(/datum/action/cooldown/power/aberrant/web_crafter) + required_powers = list(/datum/power/aberrant/web_crafter) /datum/power/aberrant/tripwire_webs/post_add(client/client_source) . = ..() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index ac12abd82fd41f..c0748bc3273fd4 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -35,6 +35,7 @@ active = TRUE if(active_effect && target == owner) healing_self = TRUE + playsound(target, 'sound/effects/magic/staff_healing.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) return TRUE /datum/action/cooldown/power/theologist/theologist_root/revered/set_click_ability(mob/on_who) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index e335eb48fc6994..0e57d66304e292 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -157,14 +157,10 @@ RegisterSignal(user, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) RegisterSignal(target, COMSIG_ATOM_DISPEL, PROC_REF(on_dispel)) - target_glow = mutable_appearance( - icon = 'icons/effects/effects.dmi', - icon_state = "shield-yellow", - layer = current_target.layer - 0.1, - appearance_flags = RESET_ALPHA|RESET_COLOR|RESET_TRANSFORM|KEEP_APART - ) + target_glow = mutable_appearance('icons/mob/effects/genetics.dmi', "servitude", -MUTATIONS_LAYER) current_target.add_overlay(target_glow) active_effect = current_target.apply_status_effect(/datum/status_effect/power/burden_shared) + playsound(target, 'sound/effects/magic/staff_healing.ogg', 75, TRUE, MEDIUM_RANGE_SOUND_EXTRARANGE) return TRUE diff --git a/modular_doppler/modular_quirks/permitted_cybernetic/code/preferences.dm b/modular_doppler/modular_quirks/permitted_cybernetic/code/preferences.dm deleted file mode 100644 index 056b0caf0939c7..00000000000000 --- a/modular_doppler/modular_quirks/permitted_cybernetic/code/preferences.dm +++ /dev/null @@ -1,20 +0,0 @@ -/datum/preference/choiced/permitted_cybernetic - category = PREFERENCE_CATEGORY_MANUALLY_RENDERED - savefile_key = "permitted_cybernetic" - savefile_identifier = PREFERENCE_CHARACTER - can_randomize = FALSE - -/datum/preference/choiced/permitted_cybernetic/init_possible_values() - return list("Random") + assoc_to_keys(GLOB.possible_quirk_implants) - -/datum/preference/choiced/permitted_cybernetic/create_default_value() - return "Random" - -/datum/preference/choiced/permitted_cybernetic/is_accessible(datum/preferences/preferences) - if (!..()) - return FALSE - - return "Permitted Cybernetic" in preferences.all_quirks - -/datum/preference/choiced/permitted_cybernetic/apply_to_human(mob/living/carbon/human/target, value) - return diff --git a/modular_doppler/modular_quirks/permitted_cybernetic/permitted_cybernetic.dm b/modular_doppler/modular_quirks/permitted_cybernetic/permitted_cybernetic.dm deleted file mode 100644 index bdd6e55ac4f275..00000000000000 --- a/modular_doppler/modular_quirks/permitted_cybernetic/permitted_cybernetic.dm +++ /dev/null @@ -1,79 +0,0 @@ -GLOBAL_LIST_INIT(possible_quirk_implants, list( - "Engineering Toolset" = /obj/item/organ/cyberimp/arm/toolkit/toolset, - "Surgery Toolset" = /obj/item/organ/cyberimp/arm/toolkit/surgery, - "Hydroponics Toolset" = /obj/item/organ/cyberimp/arm/toolkit/botany, - "Sanitation Toolset" = /obj/item/organ/cyberimp/arm/toolkit/janitor, - "Razorclaw Arm" = /obj/item/organ/cyberimp/arm/toolkit/razor_claws, - "Excavator Arm" = /obj/item/organ/cyberimp/arm/toolkit/mining_drill, - "Nutriment Pump Implant" = /obj/item/organ/cyberimp/chest/nutriment, - "Flash Shielded Eyes" = /obj/item/organ/eyes/robotic/shield, -)) - -/datum/quirk/permitted_cybernetic - name = "Permitted Cybernetic" - desc = "You're allowed a cybernetic implant aboard the station, though this is information is available for security." - value = 8 - mob_trait = TRAIT_PERMITTED_CYBERNETIC - icon = FA_ICON_WRENCH - /// Which implant to give the user - var/obj/item/organ/desired_implant - -/datum/quirk_constant_data/implanted - associated_typepath = /datum/quirk/permitted_cybernetic - customization_options = list(/datum/preference/choiced/permitted_cybernetic) - -/datum/quirk/permitted_cybernetic/add_unique(client/client_source) - desired_implant = GLOB.possible_quirk_implants[client_source?.prefs?.read_preference(/datum/preference/choiced/permitted_cybernetic)] - if(isnull(desired_implant)) //Client gone or they chose a random implant - desired_implant = GLOB.possible_quirk_implants[pick(GLOB.possible_quirk_implants)] - - var/mob/living/carbon/carbon_holder = quirk_holder - if(carbon_holder.dna.species.type in GLOB.species_blacklist_no_humanoid) - to_chat(carbon_holder, span_warning("Due to your species type, the [name] quirk has been disabled.")) - return - if(carbon_holder.mind?.assigned_role.title == JOB_PRISONER) - to_chat(carbon_holder, span_warning("Due to your job, the [name] quirk has been disabled.")) - return - medical_record_text = "Patient has a company approved [desired_implant.name] installed within their body." - -/datum/quirk/permitted_cybernetic/post_add() - var/obj/item/organ/implant = new desired_implant() - var/mob/living/carbon/carbon_holder = quirk_holder - if(implant.zone in GLOB.arm_zones) - if(HAS_TRAIT(carbon_holder, TRAIT_LEFT_HANDED)) //Left handed person? Give them a leftie implant - implant.zone = BODY_ZONE_L_ARM - implant.slot = ORGAN_SLOT_LEFT_ARM_AUG - implant.Insert(carbon_holder, special = TRUE, movement_flags = DELETE_IF_REPLACED) - -/datum/quirk/permitted_cybernetic/add(client/client_source) - . = ..() - quirk_holder.update_implanted_hud() - -/datum/quirk/permitted_cybernetic/remove() - var/mob/living/old_holder = quirk_holder - . = ..() - old_holder.update_implanted_hud() - -/mob/living/prepare_data_huds() - . = ..() - update_implanted_hud() - -/// Adds the HUD element if src has its trait. Removes it otherwise. -/mob/living/proc/update_implanted_hud() - var/image/quirk_holder = hud_list?[SEC_IMPLANT_HUD] - if(isnull(quirk_holder)) - return - - var/datum/universal_icon/temporary_icon = uni_icon(icon, icon_state, dir) - quirk_holder.pixel_y = temporary_icon.scale(32, -world.icon_size) - - if(iscarbon(src)) - var/mob/living/carbon/carbon_holder = src - if(carbon_holder.dna.species.type in GLOB.species_blacklist_no_humanoid) - return - if(HAS_TRAIT(src, TRAIT_PERMITTED_CYBERNETIC)) - set_hud_image_active(SEC_IMPLANT_HUD) - quirk_holder.icon = 'modular_doppler/overwrites/huds/hud.dmi' - quirk_holder.icon_state = "hud_imp_quirk" - else - set_hud_image_inactive(SEC_IMPLANT_HUD) diff --git a/tgstation.dme b/tgstation.dme index e2fe0914d1ff76..340be092344026 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7653,8 +7653,6 @@ #include "modular_doppler\modular_quirks\paycheck_rations\code\rationpacks.dm" #include "modular_doppler\modular_quirks\paycheck_rations\code\ticket_book.dm" #include "modular_doppler\modular_quirks\paycheck_rations\code\tickets.dm" -#include "modular_doppler\modular_quirks\permitted_cybernetic\permitted_cybernetic.dm" -#include "modular_doppler\modular_quirks\permitted_cybernetic\code\preferences.dm" #include "modular_doppler\modular_quirks\psychicholding\floating_items.dm" #include "modular_doppler\modular_quirks\system_shock\system_shock.dm" #include "modular_doppler\modular_quirks\tranquility\code\tranquility.dm" From 4d3729648925c2443eb7e79421cf69eca6fa80d3 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 2 Apr 2026 08:50:05 +0200 Subject: [PATCH 172/212] Toggleable typing bubble for telepathy. --- .../code/powers/resonant/psyker/telepathy.dm | 85 ++++++++++++++++++- .../powers/resonant/psyker/telepathy_area.dm | 2 +- 2 files changed, 83 insertions(+), 4 deletions(-) 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 b4cce24f40bf2f..b89fdec3ab9271 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm @@ -6,7 +6,7 @@ #define TELE_CLICK_RIGHT 2 /datum/power/psyker_power/telepathy name = "Telepathy" - desc = "Allows you to mentally communicate messages to targets. Generates a petit amount of stress." + desc = "Allows you to mentally communicate messages to targets. Generates a very small amount of stress. Has a speech-bubble that can be toggled on and off using middle click." security_record_text = "Subject can initiate one-way communication with a target telepathically." value = 1 required_powers = list(/datum/power/psyker_root) @@ -14,7 +14,7 @@ /datum/action/cooldown/power/psyker/telepathy name = "Telepathy" - desc = "Allows you to mentally communicate messages to the target." + desc = "Allows you to mentally communicate messages to the target. Middle click to toggle speech-bubble while typing." button_icon = 'icons/mob/actions/actions_spells.dmi' button_icon_state = "telepathy" click_to_activate = TRUE @@ -32,9 +32,23 @@ var/aoe_enabled = FALSE /// Which mouse click is used in use_action var/tele_click_type = 0 + /// Whether to show a speech-bubble while typing a telepathy message. + var/show_typing_bubble = FALSE + /// Active telepathy typing bubble overlay. + var/tmp/mutable_appearance/telepathy_typing_bubble + /// Timer id for delayed bubble removal. + var/tmp/telepathy_remove_timer /datum/action/cooldown/power/psyker/telepathy/InterceptClickOn(mob/living/clicker, params, atom/target) var/list/mods = params2list(params) + // toggles between telepathy bubble + if(LAZYACCESS(mods, MIDDLE_CLICK)) + show_typing_bubble = !show_typing_bubble + if(show_typing_bubble) + clicker.balloon_alert(clicker, "Typing bubble enabled") + else + clicker.balloon_alert(clicker, "Typing bubble disabled") + return TRUE if(LAZYACCESS(mods, RIGHT_CLICK)) if(!aoe_enabled) return FALSE @@ -57,21 +71,26 @@ // define mob and set message var/mob/living/cast_on = target + if(show_typing_bubble) + start_telepathy_typing_overlay(user) message = tgui_input_text(user, "What do you wish to whisper to [cast_on]?", "[src]", max_length = MAX_MESSAGE_LEN) - // if anything happens before we finish typing the message. if(QDELETED(src) || QDELETED(user) || QDELETED(cast_on)) + stop_telepathy_typing_overlay(user, FALSE) return FALSE // out of range if(target_range && get_dist(user, cast_on) > target_range) user.balloon_alert(user, "they're too far!") + stop_telepathy_typing_overlay(user, FALSE) return FALSE // no message if(!message) + stop_telepathy_typing_overlay(user, FALSE) return FALSE send_thought(user, cast_on, message) + stop_telepathy_typing_overlay(user, TRUE) return TRUE /datum/action/cooldown/power/psyker/telepathy/on_action_success(mob/living/user, atom/target) @@ -107,10 +126,14 @@ // AoE transmission /datum/action/cooldown/power/psyker/telepathy/proc/send_area_thought(mob/living/user) + if(show_typing_bubble) + start_telepathy_typing_overlay(user) message = tgui_input_text(user, "What do you wish to whisper to everyone in view?", "[src]", max_length = MAX_MESSAGE_LEN) if(QDELETED(src) || QDELETED(user)) + stop_telepathy_typing_overlay(user, FALSE) return FALSE if(!message) + stop_telepathy_typing_overlay(user, FALSE) return FALSE // We need to revalidate targeting on each person; you shouldn't be able to whisper to mental or magic immune people @@ -123,6 +146,7 @@ targets += target if(!length(targets)) + stop_telepathy_typing_overlay(user, FALSE) user.balloon_alert(user, "no minds in view!") return FALSE @@ -133,6 +157,7 @@ // basically goes through send_thought for each target for(var/mob/living/target as anything in targets) send_thought(user, target, message, disable_feedback = TRUE) + stop_telepathy_typing_overlay(user, TRUE) return TRUE // Tells the ghosts that telepathy talk is happening. @@ -153,3 +178,57 @@ to_mob_name = span_name("[target]") to_chat(ghost, "[from_link] [from_mob_name] [formatted_message] [to_link] [to_mob_name]") + +/// Starts a separate typing bubble overlay while the telepathy prompt is open. +/datum/action/cooldown/power/psyker/telepathy/proc/start_telepathy_typing_overlay(mob/living/user) + if(!user || QDELETED(user)) + return FALSE + if(HAS_TRAIT(user, TRAIT_THINKING_IN_CHARACTER) || user.active_typing_indicator || user.active_thinking_indicator) + return FALSE + if(telepathy_remove_timer) + deltimer(telepathy_remove_timer) + telepathy_remove_timer = null + if(telepathy_typing_bubble) + user.cut_overlay(telepathy_typing_bubble) // cut the old to force a sprite update + telepathy_typing_bubble.icon_state = "default3" + telepathy_typing_bubble.color = COLOR_LIGHT_PINK + telepathy_typing_bubble.appearance_flags = RESET_COLOR | KEEP_APART + user.add_overlay(telepathy_typing_bubble) + return TRUE + telepathy_typing_bubble = mutable_appearance('icons/mob/effects/talk.dmi', "default3", MOB_LAYER + 1, appearance_flags = RESET_COLOR | KEEP_APART) + telepathy_typing_bubble.color = COLOR_LIGHT_PINK + user.add_overlay(telepathy_typing_bubble) + return TRUE + +/// Stops the separate typing bubble overlay. +/datum/action/cooldown/power/psyker/telepathy/proc/stop_telepathy_typing_overlay(mob/living/user, sent_message) + if(!user || QDELETED(user)) + return + if(!telepathy_typing_bubble) + return + if(!sent_message) // if we didnt send a message + if(telepathy_remove_timer) + deltimer(telepathy_remove_timer) + telepathy_remove_timer = null + user.cut_overlay(telepathy_typing_bubble) + telepathy_typing_bubble = null + return + // if we did send a message + user.cut_overlay(telepathy_typing_bubble) // cut the old to force a sprite update + telepathy_typing_bubble.icon_state = "default0" + telepathy_typing_bubble.color = COLOR_LIGHT_PINK + telepathy_typing_bubble.appearance_flags = RESET_COLOR | KEEP_APART + user.add_overlay(telepathy_typing_bubble) // reapply to update. + telepathy_remove_timer = addtimer(CALLBACK(src, PROC_REF(finalize_telepathy_typing_overlay), user, telepathy_typing_bubble), 2.5 SECONDS, TIMER_UNIQUE | TIMER_OVERRIDE | TIMER_STOPPABLE) + +/// Removes the telepathy typing bubble overlay after the linger delay, if still applicable. +/datum/action/cooldown/power/psyker/telepathy/proc/finalize_telepathy_typing_overlay(mob/living/user, mutable_appearance/bubble) + telepathy_remove_timer = null + if(!user || QDELETED(user)) + return + if(!telepathy_typing_bubble || telepathy_typing_bubble != bubble) + return + if(telepathy_typing_bubble.icon_state != "default0") // we've started typing a new message. + return + user.cut_overlay(telepathy_typing_bubble) + telepathy_typing_bubble = null diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy_area.dm index d901cd915d2ee8..69db8a9bf1f655 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 @@ -11,7 +11,7 @@ var/datum/action/cooldown/power/psyker/telepathy/telepathy_action = telepathy_power?.action_path if(telepathy_action) telepathy_action.aoe_enabled = TRUE - telepathy_action.desc = "Allows you to mentally communicate messages to the target. Left click to send the message to one target, right click to all targets in view." + telepathy_action.desc = "Allows you to mentally communicate messages to the target. Left click to send the message to one target, right click to all targets in view, middle click to toggle speech-bubble while typing. ." /datum/power/psyker_power/telepathy_area/remove() . = ..() From e9c8c7e23ff335a27f81e77e9d671156532066f4 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 2 Apr 2026 09:41:22 +0200 Subject: [PATCH 173/212] Fixes scrying mob offsets being wonky. Allows middle clicking with telepathy to instantly hide the speech bubble. --- .../code/powers/resonant/psyker/scrying.dm | 27 ++++++++++++++++--- .../code/powers/resonant/psyker/telepathy.dm | 1 + 2 files changed, 24 insertions(+), 4 deletions(-) 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 25d5176e9d2558..fcdcb6ed7c37e0 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -395,30 +395,49 @@ // Apply masks for newly seen mobs (baseline: everyone) for(var/mob/living/seen_mob as anything in current_mobs) if(masked_mobs[seen_mob]) - update_silhouette_dir(seen_mob) + sync_mask_image(seen_mob) continue mask_mob(viewer, seen_mob) -// makes the silhouettes directional -/datum/scrying_immunity_mask/proc/update_silhouette_dir(mob/living/target_mob) +// Keep silhouettes aligned with the target's current appearance (transform/pixel offsets/dir). +/datum/scrying_immunity_mask/proc/sync_mask_image(mob/living/target_mob) var/image/mask_image = masked_mobs[target_mob] if(!mask_image) return + // Copy the full appearance so transforms and pixel offsets stay in sync. + mask_image.appearance = target_mob.appearance + mask_image.override = TRUE + mask_image.name = "Unknown" + mask_image.color = "#000000" + mask_image.alpha = 180 + mask_image.appearance_flags |= RESET_TRANSFORM mask_image.dir = target_mob.dir + // Avoid double-applying mob pixel offsets; the image is already anchored to the mob. + mask_image.pixel_w = 0 + mask_image.pixel_x = 0 + mask_image.pixel_y = 0 + mask_image.pixel_z = 0 + SET_PLANE_EXPLICIT(mask_image, ABOVE_GAME_PLANE, target_mob) /datum/scrying_immunity_mask/proc/mask_mob(mob/living/viewer, mob/living/target_mob) if(!viewer?.client || QDELETED(target_mob)) return - // Delusion-style override: a client-only mask image that owns the click/name. + // Delusion-style hallucination override: a client-only mask image that owns the click/name. var/image/mask_image = image(loc = target_mob) mask_image.appearance = target_mob.appearance mask_image.override = TRUE mask_image.name = "Unknown" mask_image.color = "#000000" mask_image.alpha = 180 + mask_image.appearance_flags |= RESET_TRANSFORM mask_image.dir = target_mob.dir + // Avoid double-applying mob pixel offsets; the image is already anchored to the mob. + mask_image.pixel_w = 0 + mask_image.pixel_x = 0 + mask_image.pixel_y = 0 + mask_image.pixel_z = 0 SET_PLANE_EXPLICIT(mask_image, ABOVE_GAME_PLANE, target_mob) viewer.client.images += mask_image 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 b89fdec3ab9271..1b63cf1b69b3db 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/telepathy.dm @@ -48,6 +48,7 @@ clicker.balloon_alert(clicker, "Typing bubble enabled") else clicker.balloon_alert(clicker, "Typing bubble disabled") + stop_telepathy_typing_overlay(clicker, FALSE) // turns it off instantly if needed return TRUE if(LAZYACCESS(mods, RIGHT_CLICK)) if(!aoe_enabled) From 4e57c1144e703e009fd323b1cb9cca4db342abfa Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 2 Apr 2026 09:50:19 +0200 Subject: [PATCH 174/212] Minor comments --- .../modular_powers/code/powers/resonant/psyker/scrying.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 fcdcb6ed7c37e0..85a2accaf11da6 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/scrying.dm @@ -443,7 +443,7 @@ viewer.client.images += mask_image masked_mobs[target_mob] = mask_image - // Keep your existing “don’t leak info” hooks + // Hides data about the mob with vague examines + no huds. RegisterSignal(target_mob, COMSIG_ATOM_EXAMINE, PROC_REF(on_target_examine)) hide_data_huds(viewer, target_mob) From dfd9a6d20c79f5f66fdde7e0445ea007bdde9abf Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 2 Apr 2026 18:26:36 +0200 Subject: [PATCH 175/212] Aberrant no longer has roots, tweaks to cocoon to make it less jank. Many stars and set fire to dry hay now route through use_action. Web crafter now has double-tap support for quick-craft. --- .../resonant/aberrant/_aberrant_root.dm | 2 +- .../code/powers/resonant/aberrant/cocoon.dm | 7 ++- .../aberrant/web_crafter/web_crafter.dm | 63 ++++++++++++++----- .../powers/resonant/cultivator/many_stars.dm | 39 ++++++++---- .../cultivator/set_fire_to_dry_hay.dm | 47 +++++++++----- 5 files changed, 111 insertions(+), 47 deletions(-) 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 0efffe2ebf4277..e15301bbd72558 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,4 +6,4 @@ mob_trait = TRAIT_ARCHETYPE_RESONANT archetype = POWER_ARCHETYPE_RESONANT path = POWER_PATH_ABERRANT - priority = POWER_PRIORITY_ROOT + priority = POWER_PRIORITY_BASIC // removing roots after the fact 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 607764bea46503..4ec608c66fce1e 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/aberrant/cocoon.dm @@ -65,11 +65,12 @@ // Both cast time and visual effects are resolved in this. /datum/action/cooldown/power/aberrant/cocoon/do_use_time(mob/living/user, atom/target) - // Woooow I worked hard on this and you just var-edit it away BAKA. - if(use_time <= 0) + if(use_time <= 0)// Woooow I worked hard on this and you just var-edit it away BAKA. return TRUE if(!target) return FALSE + if(isliving(target) && !can_cocoon_mob(user, target)) // I'd put this in can_use but can_cooon_mob also checks can_use so it will create a recursive loop. + return FALSE var/turf/target_turf = get_turf(target) if(!target_turf) return do_after(user, use_time, target = target, timed_action_flags = use_time_flags) @@ -152,7 +153,7 @@ if(user.pulling != target || user.grab_state < GRAB_AGGRESSIVE) user.balloon_alert(user, "You must aggressively grab the target!") return FALSE - if(target.body_position != LYING_DOWN) + if(target.body_position != LYING_DOWN || !HAS_TRAIT(target, TRAIT_FLOORED)) user.balloon_alert(user, "Target must be prone!") return FALSE return TRUE diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/web_crafter/web_crafter.dm index c539b9ab83ef43..90e3ae669728ed 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,29 +2,27 @@ /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." - security_record_text = "Subject can craft spider-like silk from their body." + \n Creating anything using web crafter makes you hungry, and you cannot use it if you are starving.\ + \n Double-tap to quickly create the last item you crafted." + mob_trait = TRAIT_WEB_SURFER // lets us walk on webs + security_record_text = "Subject can create spider-like silk from their body." value = 3 required_powers = list(/datum/power/aberrant_root/beastial) action_path = /datum/action/cooldown/power/aberrant/web_crafter -// Lets us walk on webs -/datum/power/aberrant/web_crafter/add(client/client_source) - if(power_holder) - ADD_TRAIT(power_holder, TRAIT_WEB_SURFER, REF(src)) - -/datum/power/aberrant/web_crafter/remove() - if(power_holder) - REMOVE_TRAIT(power_holder, TRAIT_WEB_SURFER, REF(src)) - /datum/action/cooldown/power/aberrant/web_crafter name = "Web Crafter" - desc = "Spend some of your satiation to craft web-like objects!" + desc = "Spend some of your satiation to craft web-like objects! Double-tap to quickly create the last item you crafted." button_icon = 'icons/effects/web.dmi' button_icon_state = "webpassage" - cooldown_time = 5 + /// Double-tap window to quick-craft the last made item. + var/double_tap_window = 0.8 SECONDS + /// World time of the last menu tap. + var/last_menu_tap_time = 0 + /// Most recently crafted entry, if any. + var/datum/web_craft_entry/last_crafted_entry /// Entries shown in the radial menu. Other powers can append to this. /// Accepts /datum/web_craft_entry instances or typepaths of that datum. @@ -34,6 +32,32 @@ ) /datum/action/cooldown/power/aberrant/web_crafter/use_action(mob/living/user, atom/target) + var/current_time = world.time + var/radial_uniqueid = get_radial_uniqueid(user) + var/datum/radial_menu/menu = GLOB.radial_menus[radial_uniqueid] + // Doublet-tap interaction to quickly make last item + if(menu && current_time <= last_menu_tap_time + double_tap_window) + if(menu) + menu.finished = TRUE + if(!last_crafted_entry) + user.balloon_alert(user, "no recent craft!") + return FALSE + if(!can_craft_entry(user, last_crafted_entry)) + return FALSE + if(!do_after(user, last_crafted_entry.craft_time, target = user)) + return FALSE + // Craft the item. + if(!create_obj(user, last_crafted_entry)) + return FALSE + last_menu_tap_time = current_time + return TRUE + else if(menu) // if you're too slow, activating the action again will just close it if the menu is open + menu.finished = TRUE + last_menu_tap_time = current_time + return FALSE + + // stores last tap so we know if its a double-time + last_menu_tap_time = current_time var/list/entries = get_web_craft_entries() if(!length(entries)) user.balloon_alert(user, "no web crafts!") @@ -45,7 +69,7 @@ user.balloon_alert(user, "no web crafts!") return FALSE - var/picked_key = show_radial_menu(user, user, radial_options, tooltips = TRUE) + var/picked_key = show_radial_menu(user, user, radial_options, uniqueid = radial_uniqueid, tooltips = TRUE) if(!picked_key) return FALSE @@ -64,10 +88,13 @@ if(!create_obj(user, entry)) return FALSE + last_crafted_entry = entry + return TRUE +/datum/action/cooldown/power/aberrant/web_crafter/on_action_success(mob/living/user, atom/target) + . = ..() if(!HAS_TRAIT(user, TRAIT_NOHUNGER)) - user.adjust_nutrition(-entry.hunger_cost) - return TRUE + user.adjust_nutrition(-last_crafted_entry.hunger_cost) /datum/action/cooldown/power/aberrant/web_crafter/can_use(mob/living/user, atom/target) . = ..() @@ -109,6 +136,10 @@ key_to_entry[key] = entry return options +// Unique radial menu id for this action and user. +/datum/action/cooldown/power/aberrant/web_crafter/proc/get_radial_uniqueid(mob/living/user) + return "web_crafter_[REF(user)]" + // Check before crafting. /datum/action/cooldown/power/aberrant/web_crafter/proc/can_craft_entry(mob/living/user, datum/web_craft_entry/entry) // Are we hungy? diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm index d1e4952efa529c..6ea835496a4385 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/many_stars.dm @@ -10,6 +10,10 @@ required_powers = list(/datum/power/cultivator_root/astral_touched) action_path = /datum/action/cooldown/power/cultivator/many_stars +#define STARS_CLICK_NONE 0 +#define STARS_CLICK_LEFT 1 +#define STARS_CLICK_RIGHT 2 + /datum/action/cooldown/power/cultivator/many_stars name = "The Many Stars that Dot the Endless Sky" desc = "Activating the ability sends forth a little star, which stops when it reaches it's destination (or hits an object) passively glowing in an area as a light source for 5 minutes. \ @@ -22,6 +26,7 @@ click_to_activate = TRUE unset_after_click = FALSE anti_magic_on_target = FALSE + click_cd_override = 3 // matches cooldown between shots // icon & state var/star_icon = 'icons/effects/eldritch.dmi' @@ -58,9 +63,25 @@ // Cached alignment action for gating effects. var/datum/action/cooldown/power/cultivator/alignment/astral_touched/astral_alignment + /// Which mouse click is used in use_action + var/stars_click_type = STARS_CLICK_NONE + +/datum/action/cooldown/power/cultivator/many_stars/InterceptClickOn(mob/living/clicker, params, atom/target) + var/list/modifiers = params2list(params) + if(LAZYACCESS(modifiers, RIGHT_CLICK)) // EXPLOSION + stars_click_type = STARS_CLICK_RIGHT + else + stars_click_type = STARS_CLICK_LEFT + . = ..() + if(!.) + stars_click_type = STARS_CLICK_NONE + return TRUE /datum/action/cooldown/power/cultivator/many_stars/use_action(mob/living/user, atom/target) - if(world.time < next_star_shot_time) + // Sets the click type. + if(stars_click_type == STARS_CLICK_RIGHT) // if right click, explode + return explode_active_stars(user) + if(world.time < next_star_shot_time) // otherwise, we shoot stars. return FALSE next_star_shot_time = world.time + star_shot_delay if(fire_projectile(user, target, /obj/projectile/resonant/many_stars)) @@ -68,16 +89,6 @@ return TRUE return FALSE -/datum/action/cooldown/power/cultivator/many_stars/InterceptClickOn(mob/living/clicker, params, atom/target) - var/list/modifiers = params2list(params) - if(LAZYACCESS(modifiers, RIGHT_CLICK)) // EXPLOSION - explode_active_stars(clicker) - return TRUE - . = ..() - if(clicker != owner) - return FALSE - return . - /datum/action/cooldown/power/cultivator/many_stars/proc/dispel(atom/target, atom/dispeller) var/list/stars_to_del = active_stars.Copy() if(stars_to_del) @@ -119,8 +130,6 @@ return if(!active_stars || !length(active_stars)) return - if(!can_use(user, target)) // we need to revalidate can_use since right click normally doesnt have that. - return if(energy_component.energy < star_explosion_cost) user.balloon_alert(user, "needs more energy!") if(user) @@ -342,3 +351,7 @@ color = COLOR_CYAN duration = 0.5 SECONDS amount_to_scale = 1.5 + +#undef STARS_CLICK_NONE +#undef STARS_CLICK_LEFT +#undef STARS_CLICK_RIGHT diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm index 8b9feadf0e0d47..e747f792319eab 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/set_fire_to_dry_hay.dm @@ -9,6 +9,10 @@ required_powers = list(/datum/power/cultivator_root/flame_soul) action_path = /datum/action/cooldown/power/cultivator/set_fire_to_dry_hay +#define FIRE_CLICK_NONE 0 +#define FIRE_CLICK_LEFT 1 +#define FIRE_CLICK_RIGHT 2 + /datum/action/cooldown/power/cultivator/set_fire_to_dry_hay name = "Set Fire to Dry Hay" desc = "You can set fire onto anything you touch. This works similary to a lighter in terms of functionality. \ @@ -19,6 +23,7 @@ click_to_activate = TRUE unset_after_click = FALSE + click_cd_override = 5 // matches cooldown between shots // Cooldown for right click projectile, in deciseconds. var/projectile_delay = 5 @@ -41,8 +46,25 @@ var/flameblast_impact_sound = 'sound/effects/fire_puff.ogg' // Cached alignment action for gating right click effects. var/datum/action/cooldown/power/cultivator/alignment/flame_soul/flame_soul_alignment + /// Which mouse click is used in use_action + var/fire_click_type = FIRE_CLICK_NONE + +// We use both left and right mouse button. +/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/InterceptClickOn(mob/living/clicker, params, atom/target) + var/list/modifiers = params2list(params) + if(LAZYACCESS(modifiers, RIGHT_CLICK)) + fire_click_type = FIRE_CLICK_RIGHT + else + fire_click_type = FIRE_CLICK_LEFT + . = ..() + if(!.) + fire_click_type = FIRE_CLICK_NONE + return TRUE // Always consume the click to avoid normal click interactions. /datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/use_action(mob/living/user, atom/target) + // Sets the click type. + if(fire_click_type == FIRE_CLICK_RIGHT) // shoots flameblasts instead of lighting cigs. + return shoot_flameblast(user, target) if(!target) return FALSE // Lighter version only works in melee range. @@ -58,6 +80,12 @@ lighter.attack(target_mob, user, list(), list()) qdel(lighter) return TRUE + // Allow lighting loose cigarettes directly. + if(istype(target, /obj/item/cigarette)) + var/obj/item/cigarette/cig = target + cig.attackby(lighter, user, list(), list()) + qdel(lighter) + return TRUE // Only ignite flammable targets. if((target.resistance_flags & FLAMMABLE) && !(target.resistance_flags & FIRE_PROOF)) @@ -67,17 +95,6 @@ // Always return TRUE to keep the click ability active. return TRUE -// We use both left and right mouse button. -/datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/InterceptClickOn(mob/living/clicker, params, atom/target) - var/list/modifiers = params2list(params) - if(LAZYACCESS(modifiers, RIGHT_CLICK)) - if(!shoot_flameblast(clicker, target)) - return FALSE - return TRUE - ..() - // Always consume the click to avoid normal click interactions. - return TRUE - // Gets & caches flame soul alignment for gating the right click. /datum/action/cooldown/power/cultivator/set_fire_to_dry_hay/proc/is_flame_soul_alignment_active(mob/living/user) if(!flame_soul_alignment) @@ -93,8 +110,6 @@ if(!is_flame_soul_alignment_active(user)) user.balloon_alert(user, "alignment required!") return FALSE - if(!can_use(user, target)) // we need to revalidate can_use since right click normally doesnt have that. - return FALSE if(world.time < next_projectile_time) return FALSE next_projectile_time = world.time + projectile_delay @@ -130,11 +145,11 @@ name = "\improper cultivator flame" desc = "A conjured spark of flame." fancy = TRUE - lit = TRUE heat_while_on = HIGH_TEMPERATURE_REQUIRED - 100 /obj/item/cultivator_virtual_lighter/Initialize(mapload) . = ..() + lit = FALSE // so we have to make sure its unlit before we light it or it won't work. I love it here. set_lit(TRUE) /obj/item/cultivator_virtual_lighter/get_fuel() @@ -172,3 +187,7 @@ if(ignite_target.resistance_flags & FIRE_PROOF) continue ignite_target.fire_act(500) + +#undef FIRE_CLICK_NONE +#undef FIRE_CLICK_LEFT +#undef FIRE_CLICK_RIGHT From 33bd19e601ad81a085a47d81d41b32f15151bd95 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 2 Apr 2026 18:30:16 +0200 Subject: [PATCH 176/212] Buffs pious prayer for people with a spiritual personality. Tweaks redundant flags in theologist --- .../code/powers/sorcerous/theologist/entropic_mending.dm | 2 -- .../code/powers/sorcerous/theologist/pious_prayer.dm | 6 ++---- .../code/powers/sorcerous/theologist/smiting_strike.dm | 2 -- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm index 6b364f55e36e3a..1a35614cbe3ffc 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/entropic_mending.dm @@ -11,8 +11,6 @@ Entropic Mending removes wounds (sometimes) and speeds up the target's metabolis action_path = /datum/action/cooldown/power/theologist/entropic_mending value = 6 - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THEOLOGIST required_powers = list(/datum/power/theologist_root/twisted) /datum/action/cooldown/power/theologist/entropic_mending diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm index 61ec8e8a10bb0a..6d3e4bda89280f 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/pious_prayer.dm @@ -6,8 +6,6 @@ security_record_text = "Subject fuels their powers with visits to the Chapel." value = 3 - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THEOLOGIST action_path = /datum/action/cooldown/power/theologist/pious_prayer required_powers = list(/datum/power/theologist_root) required_allow_subtypes = TRUE @@ -74,9 +72,9 @@ // Are you the chaplain? if(is_chaplain_job(user.mind?.assigned_role)) total_chance += 20 - // Do you have the religious quirk? + // Do you have the spiritual personality trait? if(HAS_TRAIT(user, TRAIT_SPIRITUAL)) - total_chance += 5 + total_chance += 15 // Do you carry the bible on your person? if(has_bible(user)) total_chance += 10 diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm index 9d05b5fa812710..1e2bf5416772fb 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/smiting_strike.dm @@ -7,8 +7,6 @@ action_path = /datum/action/cooldown/power/theologist/smiting_strike value = 4 - archetype = POWER_ARCHETYPE_SORCEROUS - path = POWER_PATH_THEOLOGIST required_powers = list(/datum/power/theologist_root/revered) /datum/action/cooldown/power/theologist/smiting_strike From e77d24faccfd40c42a4b3fe8afe54acc039a9ae5 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 2 Apr 2026 18:32:09 +0200 Subject: [PATCH 177/212] Tweaks a span class issue with meditate. --- modular_doppler/modular_powers/code/powers/resonant/meditate.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm index 7b3c4246dcadb7..0228098edabe1c 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/meditate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/meditate.dm @@ -57,7 +57,7 @@ Reduces stress for psykers and restores Energy for cultivators break while (keep_going) - to_chat(owner, "You stop meditating.") + to_chat(owner, span_notice("You stop meditating.")) active = FALSE spotlighttarget.remove_status_effect(/datum/status_effect/spotlight_light/meditation) return From f356017934db70a72c1f91618951633b4790f599 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 2 Apr 2026 19:55:44 +0200 Subject: [PATCH 178/212] Final tweaks to descriptions to match document. Conjure Rain now works with blood but has capped floor splash. --- .../powers/mortal/augmented/pneumatic_arm.dm | 6 +- .../mortal/augmented/precognition_eyes.dm | 4 +- .../powers/resonant/aberrant/tail_sweep.dm | 6 +- .../code/powers/resonant/psyker/mirage.dm | 37 +++++++++++ .../sorcerous/thaumaturge/conjure_rain.dm | 34 ++++++++-- .../thaumaturge/sanguine_absorption.dm | 2 +- .../theologist/_theologist_root_revered.dm | 62 ++++++------------- .../theologist/_theologist_root_twisted.dm | 3 +- 8 files changed, 96 insertions(+), 58 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm index e0bcad91266b40..611cb78c6b538a 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/pneumatic_arm.dm @@ -4,7 +4,7 @@ /datum/power/augmented/pneumatic_arm name = "Premium DSTR Pneumatic Arm" desc = "A popular choice for the augmented bodyguards and manufactured by Praetor Dynamics. Passively increases your punch damage by +5 with that arm. \ - \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 2 spaces (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ + \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 1 space (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)." security_record_text = "Subject has a DSTR Pneumatic Arm, increasing their lethality with unarmed strikes." security_threat = POWER_THREAT_MAJOR @@ -15,7 +15,7 @@ /obj/item/organ/cyberimp/arm/pneumatic_arm name = "DSTR Pneumatic Arm" desc = "A popular choice for the augmented bodyguards and manufactured by Praetor Dynamics. Passively increases your punch damage by +5 with that arm. \ - \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 2 spaces (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ + \n In addition, it allows you actively 'overcharge' the arm, making your next punch knockback someoneone 1 space (potentially stunning them on walls) and dealing an additional 15 brute damage in exchange for a hefty quality cost.\ \n Quality decreases from using the pneumatic arm's active ability. Quality affects damage (passive and active)." icon_state = "toolkit_generic" @@ -30,7 +30,7 @@ var/bonus_active_damage = 15 // Knockback on punch - var/knockback = 2 + var/knockback = 1 // Is the throw 'throw'? False means it can cause wallstuns and such. var/gentle_throw = FALSE diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm index 978e2f1423aa58..2e85746851d7de 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/precognition_eyes.dm @@ -23,6 +23,8 @@ premium = TRUE var/enabled = TRUE + /// How much quality do we lose on trigger? + var/quality_loss = AUGMENTED_PREMIUM_QUALITY_MINOR / 2 /// Skillchip installed by this augment. var/obj/item/skillchip/installed_chip /// Did we add an extra skillchip slot? @@ -97,7 +99,7 @@ if(. & EMP_PROTECT_SELF) return if(premium_component) - premium_component.adjust_quality(-AUGMENTED_PREMIUM_QUALITY_MINOR) + premium_component.adjust_quality(-quality_loss) enabled = FALSE COOLDOWN_START(src, emp_reenable_cooldown, emp_cooldown) premium_component?.update_quality_actions() 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 f9b0ed8c8f5b50..a8f9ecab2930ea 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 @@ -3,18 +3,18 @@ */ /datum/power/aberrant/tailsweep name = "Tail Sweep" - desc = "Your tail is a weapon in its own right. When activated, damages all creatures adjacent to you for 10 brute and 20 stamina, and knocks them away 2 spaces, potentially into walls.\ + desc = "Your tail is a weapon in its own right. When activated, damages all creatures adjacent to you for 20 brute and 30 stamina, and knocks them away 2 spaces, potentially into walls.\ \n Has a short cooldown, consumes hunger and the damage is affected by your opponent's chest armor. Requires a tail. If you are a large mob (such as with the Oversized quirk), you gain +1 range." security_record_text = "Subject can use their tail to damage and knock back foes in active combat." security_threat = POWER_THREAT_MAJOR - value = 3 + value = 4 required_powers = list(/datum/power/aberrant_root/beastial) action_path = /datum/action/cooldown/power/aberrant/tailsweep /datum/action/cooldown/power/aberrant/tailsweep name = "Tail Sweep" - desc = "Your tail is a weapon in its own right. When activated, damages all creatures adjacent to you for 10 brute and 20 stamina, and knocks them away 2 spaces, potentially into walls." + desc = "Your tail is a weapon in its own right. When activated, damages all creatures adjacent to you for 20 brute and 30 stamina, and knocks them away 2 spaces, potentially into walls." button_icon = 'icons/mob/actions/actions_xeno.dmi' button_icon_state = "tailsweep" cooldown_time = 6 SECONDS diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm index 1b50a4113069dd..af9712cc29f6e0 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm @@ -66,6 +66,7 @@ // Causes it to act immediately. new_mirage.FindTarget() + new_mirage.taunt_nearest_hostile(5) modify_stress(stress_cost) playsound(new_mirage, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) @@ -112,6 +113,7 @@ attack_verb_continuous = "attacks" attack_verb_simple = "attack" +// imposes the caster onto the mob /mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/set_action_ref(datum/action/cooldown/power/psyker/mirage/action) action_ref = WEAKREF(action) if(!alt_appearance_key) @@ -132,6 +134,41 @@ move_to_delay = desired set_varspeed(desired) +// Draw a nearby hostile's aggro to sell the illusion. +/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/taunt_nearest_hostile(range_limit = 5) + var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve() + var/mob/living/nearest_mob + var/nearest_dist + + for(var/mob/living/living_mob in range(range_limit, src)) + if(living_mob == src || QDELETED(living_mob)) // no self taunting + continue + if(istype(living_mob, /mob/living/simple_animal/hostile/illusion)) // no taunting other illusions + continue + if(living_mob.mind) // no sentient taunting + continue + if(!islist(living_mob.faction) || (!(FACTION_HOSTILE in living_mob.faction) && !(FACTION_MINING in living_mob.faction))) // has to be in the hostile mob faction or the mining faction + continue + if(FACTION_BOSS in living_mob.faction) // "There is no aggro reset. (...) There is some shit about an aggro reset when people don't know how to manage their aggro." + continue + if(action && !action.can_affect_mental(living_mob)) // can't be immune to mental shit + continue + if(!istype(living_mob, /mob/living/simple_animal/hostile) && !living_mob.ai_controller) // either a hostile mob or has to have an ai controler + continue + var/distance = get_dist(src, living_mob) + if(isnull(nearest_dist) || distance < nearest_dist) // get the nearest mob in range + nearest_mob = living_mob + nearest_dist = distance + if(nearest_mob) + if(istype(nearest_mob, /mob/living/simple_animal/hostile)) // hostile mobs forced target + var/mob/living/simple_animal/hostile/hostile_mob = nearest_mob + hostile_mob.GiveTarget(src) + else if(nearest_mob.ai_controller) // otherwise we just force the blackboard to use a different target. + nearest_mob.ai_controller.CancelActions() + nearest_mob.ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + nearest_mob.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, src) + nearest_mob.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, src) + // Applies the selection AI mode. Have your illusions act as you please :D /mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/apply_mode(new_mode) last_mode = new_mode diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm index ed34dbce354f0a..17d3406a32bf51 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/conjure_rain.dm @@ -25,7 +25,6 @@ use_time_overlay_type = /obj/effect/temp_visual/conjure_rain use_time = 1 SECONDS - use_time_flags = IGNORE_USER_LOC_CHANGE // the chem that the base rain uses var/datum/reagent/rain_chem = /datum/reagent/water @@ -37,6 +36,21 @@ var/max_reagents_dupe = 60 // bonus to max reagents per affinity above 3. var/affinity_max_reagents = 20 + // Max units of reagent to expose per turf when splashing on the ground. + var/ground_expose_cap = 10 + // If TRUE, only allow chems that can be synthesized (unless bypassed below). + var/require_synthesizable = TRUE + // Chems that bypass synthesizable check. + var/list/synth_bypass_chems = list(/datum/reagent/blood) // blood is cool and has synergy iwth sanguine absorption + +/datum/action/cooldown/power/thaumaturge/conjure_rain/proc/is_allowed_rain_reagent(datum/reagent/reagent) + if(!reagent) + return FALSE + if(reagent.type in synth_bypass_chems) + return TRUE + if(!require_synthesizable) + return TRUE + return (reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED) // We piggyback into do_use_time to add a telegraph of the rain. /datum/action/cooldown/power/thaumaturge/conjure_rain/do_use_time(mob/living/user, atom/target) @@ -53,7 +67,7 @@ // We need to make sure that the chems are synthesizable so that people aren't surprised that they can't blood rain var/list/datum/reagent/synth_reagents = list() for(var/datum/reagent/reagent in held_container.reagents.reagent_list) - if(reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED) + if(is_allowed_rain_reagent(reagent)) synth_reagents += reagent // If all succeeds, mix the rain color. if(length(synth_reagents)) @@ -80,7 +94,7 @@ if(istype(held_container) && held_container.reagents?.total_volume) var/synth_volume = 0 for(var/datum/reagent/reagent as anything in held_container.reagents.reagent_list) - if(reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED)// Prevents us from duping SPECIAL CHEMS. + if(is_allowed_rain_reagent(reagent))// Prevents us from duping SPECIAL CHEMS (unless bypassed). synth_volume += reagent.volume var/drain_amount = min(buffer.buffer_volume, synth_volume) if(drain_amount > 0) @@ -88,7 +102,7 @@ var/chem_ratio = base_chem_ratio var/part = drain_amount / synth_volume for(var/datum/reagent/reagent as anything in held_container.reagents.reagent_list) - if(!(reagent.chemical_flags & REAGENT_CAN_BE_SYNTHESIZED)) + if(!is_allowed_rain_reagent(reagent)) continue var/transfer_amount = reagent.volume * part if(transfer_amount > 0) @@ -107,6 +121,9 @@ var/bonus_affinity = max(0, affinity - 3) var/max_spread = max_reagents_dupe + (bonus_affinity * affinity_max_reagents) var/per_container = 0 + var/ground_expose_modifier = 1 + if(buffer.reagents.total_volume > 0) + ground_expose_modifier = min(1, ground_expose_cap / buffer.reagents.total_volume) // Get every reagent container in range and calculate how we spread the rain. if(length(area_containers)) @@ -114,8 +131,13 @@ // every tile in range... for(var/turf/area_turf in range(1, target_turf)) - // splash it onto the space. - buffer.reagents.expose(area_turf, TOUCH) + var/has_container = FALSE + for(var/obj/item/reagent_containers/target_container in area_turf) + has_container = TRUE + break + // splash it onto the space (skip if we're filling a container on that turf). + if(!has_container) + buffer.reagents.expose(area_turf, TOUCH, ground_expose_modifier) // splashes it onto every mob in the area for(var/mob/living/area_mob in area_turf) buffer.reagents.expose(area_mob, TOUCH) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm index f249ae99b98f9c..6711fc663e7e41 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/sanguine_absorption.dm @@ -3,7 +3,7 @@ */ /datum/power/thaumaturge/sanguine_absorption name = "Sanguine Absorption" - desc = "You draw nearby blood into the target. This draws up to 100u of blood from adjacent floor/wall splatters, containers and other mobs (in that order). It then tranfers that blood to the target and converts it to universally accepted blood.\ + desc = "You draw nearby blood into the target. This draws up to 100u of blood from adjacent floor/wall splatters, containers and other mobs (in that order). It then transfers that blood to the target and converts it to universally accepted blood.\ \nAny excess blood in the target creature beyond 100% is transformed into healing, at a 10u per 4 damage ratio. This can only heal organic bodyparts and does not heal any damage-types besides Brute or Burn. This also does not affect creatures with non-blood bloodtypes such as Ethereals or Slimepeople. \ \nRequires Affinity 3. Additional affinity increases the healing ratio by 0.5 per point" security_record_text = "Subject can draw blood from varying sources (including humanoids) and transmute it into universal blood, potentially healing the target." diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm index c0748bc3273fd4..04afec8fedfcc5 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_revered.dm @@ -1,8 +1,8 @@ /datum/power/theologist_root/revered name = "A Burden Revered" - desc = "Nullifies pain and slowly heals the targeted creature over a prolonged period of time. This may be yourself. \ - Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again. \ + desc = "Nullifies pain and slowly heals the targeted creature's burn and brute damage over a prolonged period of time. This may be yourself. \ + Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again. Does not work on synthetic bodyparts. \ This is mutually exclusive with the other 'A Burden...' powers." security_record_text = "Subject can magically mend their own wounds and the wounds of others slowly over a long duration." security_threat = POWER_THREAT_MAJOR @@ -12,8 +12,8 @@ /datum/action/cooldown/power/theologist/theologist_root/revered name = "A Burden Revered" - desc = "Nullifies pain and slowly heals the targeted creature over a prolonged period of time. This may be yourself. \ - Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again." + desc = "Nullifies pain and slowly heals the targeted creature's burn and brute damage over a prolonged period of time. This may be yourself. \ + Grants piety based on healing done, ends prematurely if the target reaches full health or if it is cast again. Does not work on synthetic bodyparts." button_icon = 'icons/obj/weapons/guns/magic.dmi' button_icon_state = "revivewand" // I need something better cooldown_time = 50 @@ -114,53 +114,31 @@ var/healing_amount = (base_healing_amount * seconds_between_ticks) new /obj/effect/temp_visual/heal(get_turf(owner), "#ddd166") - // Expire if at full health. - if(owner && owner.health >= owner.maxHealth) - expire() - return // Expire if we've reached the max. if(healing_done >= healing_max) expire() return - // Only include damage types that actually need healing - var/list/damage_choices = list() - var/brute_damage = owner.getBruteLoss() - var/burn_damage = owner.getFireLoss() - var/tox_damage = owner.getToxLoss() - var/oxy_damage = owner.getOxyLoss() - - if(brute_damage > 0) damage_choices += "brute" - if(burn_damage > 0) damage_choices += "burn" - if(tox_damage > 0) damage_choices += "tox" - if(oxy_damage > 0) damage_choices += "oxy" - - // Nothing to heal - if(!damage_choices.len) + // Limb-based healing: only organic bodyparts. + if(!istype(owner, /mob/living/carbon)) + expire() return - var/damage_choice = pick(damage_choices) - - switch(damage_choice) - if("brute") - var/heal_done = min(healing_amount, brute_damage) - owner.adjustBruteLoss(-heal_done) + var/mob/living/carbon/mob = owner + var/healed_any = FALSE + // gets random bodypart, heals it, bam. + for(var/obj/item/bodypart/bodypart in mob.get_damaged_bodyparts(1, 1, BODYTYPE_ORGANIC)) + var/heal_done = bodypart.heal_damage(healing_amount, healing_amount, required_bodytype = BODYTYPE_ORGANIC) + if(heal_done) + mob.update_damage_overlays() healing_done += heal_done + healed_any = TRUE + break - if("burn") - var/heal_done = min(healing_amount, burn_damage) - owner.adjustFireLoss(-heal_done) - healing_done += heal_done - - if("tox") - var/heal_done = min(healing_amount, tox_damage) - owner.adjustToxLoss(-heal_done) - healing_done += heal_done - - if("oxy") - var/heal_done = min(healing_amount, oxy_damage) - owner.adjustOxyLoss(-heal_done) - healing_done += heal_done + // Expire if there's nothing left to heal. + if(!healed_any) + expire() + return // QDEL destroys burden_power /datum/status_effect/power/burden_revered/proc/expire() diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm index 54b87f8393df04..ca7d984a203eee 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_twisted.dm @@ -4,7 +4,6 @@ Gives Piety proportional to the amount of damage twisted. \ This is mutually exclusive with the other 'A Burden...' powers." security_record_text = "Subject can rapidly transmute the wounds of a target into smaller, insubstantial wounds." - security_threat = POWER_THREAT_MAJOR action_path = /datum/action/cooldown/power/theologist/theologist_root/twisted value = 5 @@ -15,7 +14,7 @@ Gives Piety proportional to the amount of damage twisted." button_icon = 'icons/mob/actions/actions_cult.dmi' button_icon_state = "hand" - cooldown_time = 200 + cooldown_time = 150 target_range = 1 target_type = /mob/living click_to_activate = TRUE From 1c4e53b9f37039cdd05bb6a8eced775ebb4dde9a Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 2 Apr 2026 20:57:39 +0200 Subject: [PATCH 179/212] Removes the telepathy quirk. --- .../telepathy_qol/telepathy_quirk.dm | 32 ------------------- tgstation.dme | 1 - 2 files changed, 33 deletions(-) delete mode 100644 modular_doppler/telepathy_qol/telepathy_quirk.dm diff --git a/modular_doppler/telepathy_qol/telepathy_quirk.dm b/modular_doppler/telepathy_qol/telepathy_quirk.dm deleted file mode 100644 index 69d13f2d163970..00000000000000 --- a/modular_doppler/telepathy_qol/telepathy_quirk.dm +++ /dev/null @@ -1,32 +0,0 @@ -/datum/quirk/telepathic - name = "Telepathic" - desc = "You are able to transmit your thoughts to other living creatures." - gain_text = span_purple("Your mind roils with psychic energy.") - lose_text = span_notice("Mundanity encroaches upon your thoughts once again.") - medical_record_text = "Patient has an unusually enlarged Broca's area visible in cerebral biology, and appears to be able to communicate via extrasensory means." - value = 8 - icon = FA_ICON_HEAD_SIDE_COUGH - /// Ref used to easily retrieve the action used when removing the quirk from silicons - var/datum/weakref/tele_action_ref - -/datum/quirk/telepathic/add(client/client_source) - if (iscarbon(quirk_holder)) - var/mob/living/carbon/human/human_holder = quirk_holder - human_holder.dna.add_mutation(/datum/mutation/telepathy, MUTATION_SOURCE_QUIRK) - else if (issilicon(quirk_holder)) - var/mob/living/silicon/robot_holder = quirk_holder - var/datum/action/cooldown/spell/pointed/telepathy/tele_action = new - - tele_action.Grant(robot_holder) - tele_action_ref = WEAKREF(tele_action) - -/datum/quirk/telepathic/remove() - var/datum/action/cooldown/spell/pointed/telepathy/tele_action = tele_action_ref?.resolve() - if (isnull(tele_action)) - tele_action_ref = null - if (iscarbon(quirk_holder)) - var/mob/living/carbon/human/human_holder = quirk_holder - human_holder.dna.remove_mutation(/datum/mutation/telepathy, MUTATION_SOURCE_QUIRK) - else if (issilicon(quirk_holder) && !isnull(tele_action)) - QDEL_NULL(tele_action) - tele_action_ref = null diff --git a/tgstation.dme b/tgstation.dme index 36986c8249196c..6d93ff382b5de2 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7981,7 +7981,6 @@ #include "modular_doppler\taurs\code\taur_mechanics\taur_clothing_offset.dm" #include "modular_doppler\taurs\code\taur_sprites\suits.dm" #include "modular_doppler\telepathy_qol\telepathy_action.dm" -#include "modular_doppler\telepathy_qol\telepathy_quirk.dm" #include "modular_doppler\telepathy_qol\telepathy_reply_emote.dm" #include "modular_doppler\temporary_flavor_text\temp_flavor_text.dm" #include "modular_doppler\the-business\code\twitch.dm" From 676ce1aaf23c4e2e300d86a98167e56f54257fa8 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Thu, 2 Apr 2026 22:16:27 +0200 Subject: [PATCH 180/212] Fixes unit test stuff. Belts for spell focuses are no longer accessible. Adds a base status effect abstract type. Mirage now uses a basic mob instead of a simple_mob. --- .../code/powers/resonant/psyker/mirage.dm | 150 ++++++++++++------ .../affinity/thaumaturge_spell_focus.dm | 20 +-- .../code/powers_status_effect.dm | 4 + tgstation.dme | 1 + 4 files changed, 117 insertions(+), 58 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers_status_effect.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm index af9712cc29f6e0..ead772af2730f8 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm @@ -31,7 +31,7 @@ // Mirage behavior mode var/mode = MIRAGE_MODE_STATIONARY // Stress cost - var/stress_cost = PSYKER_STRESS_MODERATE * 1.5 + var/stress_cost = PSYKER_STRESS_MODERATE // WE get the right click behavior to cycle behavior. /datum/action/cooldown/power/psyker/mirage/InterceptClickOn(mob/living/clicker, params, atom/target) @@ -57,7 +57,7 @@ return FALSE // Creates a new instance of the mirrage - var/mob/living/simple_animal/hostile/illusion/mirage/resonant/new_mirage = new(spawn_turf) + var/mob/living/basic/resonant_mirage/new_mirage = new(spawn_turf) new_mirage.Copy_Parent(owner, 20 SECONDS, 1, 0) new_mirage.set_action_ref(src) new_mirage.apply_mode(mode) @@ -65,8 +65,8 @@ active_mirages += new_mirage // Causes it to act immediately. - new_mirage.FindTarget() new_mirage.taunt_nearest_hostile(5) + new_mirage.kick_ai() modify_stress(stress_cost) playsound(new_mirage, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) @@ -87,34 +87,71 @@ /datum/action/cooldown/power/psyker/mirage/Remove(mob/removed_from) . = ..() - for(var/mob/living/simple_animal/hostile/illusion/mirage/resonant/mirage as anything in active_mirages) + for(var/mob/living/basic/resonant_mirage/mirage as anything in active_mirages) if(!QDELETED(mirage)) qdel(mirage) active_mirages.Cut() /datum/action/cooldown/power/psyker/mirage/proc/cleanup_mirages() - for(var/mob/living/simple_animal/hostile/illusion/mirage/resonant/mirage as anything in active_mirages.Copy()) + for(var/mob/living/basic/resonant_mirage/mirage as anything in active_mirages.Copy()) if(QDELETED(mirage)) active_mirages -= mirage /* - Mirage mob: simple animal used for aggro, but with per-viewer masking. + Mirage mob: basic mob used for aggro, but with per-viewer masking. */ -/mob/living/simple_animal/hostile/illusion/mirage/resonant +/mob/living/basic/resonant_mirage + name = "illusion" + desc = "It's a fake!" + icon = 'icons/effects/effects.dmi' + icon_state = "static" + icon_living = "static" + icon_dead = "null" + gender = NEUTER + mob_biotypes = NONE + faction = list(FACTION_ILLUSION) + basic_mob_flags = DEL_ON_DEATH + death_message = "vanishes into thin air! It was a fake!" + /// Weakref to what we're copying + var/datum/weakref/parent_mob_ref + +/mob/living/basic/resonant_mirage/proc/Copy_Parent(mob/living/original, life = 5 SECONDS, hp = 100, damage = 0) + appearance = original.appearance + parent_mob_ref = WEAKREF(original) + setDir(original.dir) + maxHealth = hp + updatehealth() // re-cap health to new value + melee_damage_type = BRUTE + wound_bonus = CANT_WOUND + exposed_wound_bonus = 0 + sharpness = NONE + armour_penetration = 0 + obj_damage = 0 + environment_smash = ENVIRONMENT_SMASH_NONE + transform = initial(transform) + pixel_x = base_pixel_x + pixel_y = base_pixel_y + addtimer(CALLBACK(src, TYPE_PROC_REF(/mob/living, death)), life) + +/mob/living/basic/resonant_mirage/examine(mob/user) + var/mob/living/parent_mob = parent_mob_ref?.resolve() + if(parent_mob) + return parent_mob.examine(user) + return ..() + +/mob/living/basic/resonant_mirage var/datum/weakref/action_ref var/last_mode = MIRAGE_MODE_STATIONARY var/alt_appearance_key density = TRUE - melee_damage_lower = 0 - melee_damage_upper = 0 obj_damage = 0 environment_smash = ENVIRONMENT_SMASH_NONE attack_verb_continuous = "attacks" attack_verb_simple = "attack" // imposes the caster onto the mob -/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/set_action_ref(datum/action/cooldown/power/psyker/mirage/action) +/mob/living/basic/resonant_mirage/proc/set_action_ref(datum/action/cooldown/power/psyker/mirage/action) action_ref = WEAKREF(action) if(!alt_appearance_key) alt_appearance_key = "mirage_alpha_[REF(src)]" @@ -125,17 +162,11 @@ RegisterSignal(src, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_mirage_dir_change)) RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_mirage_dispel)) -/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/match_owner_speed(mob/living/owner) - if(!owner) - return - if(!isnum(owner.cached_multiplicative_slowdown)) - return - var/desired = max(0.1, owner.cached_multiplicative_slowdown) - move_to_delay = desired - set_varspeed(desired) +/mob/living/basic/resonant_mirage/proc/match_owner_speed(mob/living/owner) + set_varspeed(1) // this was more complex when it was a simple mob to match the owner, but I had to change it. // Draw a nearby hostile's aggro to sell the illusion. -/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/taunt_nearest_hostile(range_limit = 5) +/mob/living/basic/resonant_mirage/proc/taunt_nearest_hostile(range_limit = 5) var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve() var/mob/living/nearest_mob var/nearest_dist @@ -143,7 +174,7 @@ for(var/mob/living/living_mob in range(range_limit, src)) if(living_mob == src || QDELETED(living_mob)) // no self taunting continue - if(istype(living_mob, /mob/living/simple_animal/hostile/illusion)) // no taunting other illusions + if(istype(living_mob, /mob/living/simple_animal/hostile/illusion) || istype(living_mob, /mob/living/basic/resonant_mirage)) // no taunting other illusions continue if(living_mob.mind) // no sentient taunting continue @@ -170,25 +201,39 @@ nearest_mob.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, src) // Applies the selection AI mode. Have your illusions act as you please :D -/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/apply_mode(new_mode) +/mob/living/basic/resonant_mirage/proc/apply_mode(new_mode) last_mode = new_mode switch(new_mode) if(MIRAGE_MODE_STATIONARY) - stop_automated_movement = TRUE - toggle_ai(AI_IDLE) + set_ai_controller_type(null) if(MIRAGE_MODE_AGGRESSIVE) - stop_automated_movement = FALSE - retreat_distance = 0 - minimum_distance = 0 - toggle_ai(AI_ON) + set_ai_controller_type(/datum/ai_controller/basic_controller/simple/simple_hostile) if(MIRAGE_MODE_FLEE) - stop_automated_movement = FALSE - retreat_distance = 10 - minimum_distance = 10 - toggle_ai(AI_ON) + set_ai_controller_type(/datum/ai_controller/basic_controller/simple/simple_fearful) -/mob/living/simple_animal/hostile/illusion/mirage/resonant/Destroy() +/mob/living/basic/resonant_mirage/proc/set_ai_controller_type(controller_type) + if(isnull(controller_type)) + QDEL_NULL(ai_controller) + return + if(istype(ai_controller, controller_type)) + ai_controller.reset_ai_status() + ai_controller.set_blackboard_key(BB_TARGETING_STRATEGY, /datum/targeting_strategy/basic/mirage) + return + QDEL_NULL(ai_controller) + ai_controller = new controller_type(src) + ai_controller.set_blackboard_key(BB_TARGETING_STRATEGY, /datum/targeting_strategy/basic/mirage) + +// we kick it to make it work. When this was a simple animal this wasn't as big of a problem, but /basic/ mobs are just more sluggish. +/mob/living/basic/resonant_mirage/proc/kick_ai() + if(!ai_controller) + return + ai_controller.set_ai_status(AI_STATUS_ON) + ai_controller.SelectBehaviors(0.1) + for(var/datum/ai_behavior/current_behavior as anything in ai_controller.current_behaviors) + ai_controller.ProcessBehavior(0.1, current_behavior) + +/mob/living/basic/resonant_mirage/Destroy() if(alt_appearance_key) remove_alt_appearance(alt_appearance_key) alt_appearance_key = null @@ -197,32 +242,26 @@ action_ref = null return ..() -/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/on_mirage_dispel(datum/source, atom/dispeller) +/mob/living/basic/resonant_mirage/proc/on_mirage_dispel(datum/source, atom/dispeller) SIGNAL_HANDLER qdel(src) return DISPEL_RESULT_DISPELLED // We need to tell the alt appearance variant to turn. -/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/on_mirage_dir_change(datum/source, old_dir, new_dir) +/mob/living/basic/resonant_mirage/proc/on_mirage_dir_change(datum/source, old_dir, new_dir) SIGNAL_HANDLER var/image/appearance_image = hud_list?[alt_appearance_key] if(appearance_image) appearance_image.dir = new_dir // If you have disbelieved the illusion (immune to mental) you can just walk through them. -/mob/living/simple_animal/hostile/illusion/mirage/resonant/CanAllowThrough(atom/movable/mover, border_dir) +/mob/living/basic/resonant_mirage/CanAllowThrough(atom/movable/mover, border_dir) if(should_ignore_target(mover)) return TRUE return ..() -// We don't aggro our owner, -/mob/living/simple_animal/hostile/illusion/mirage/resonant/CanAttack(atom/the_target) - if(should_ignore_target(the_target)) - return FALSE - return ..() - // Basically we check if they're our owner, are affected by mental or are an illusion of the same mob. -/mob/living/simple_animal/hostile/illusion/mirage/resonant/proc/should_ignore_target(atom/target) +/mob/living/basic/resonant_mirage/proc/should_ignore_target(atom/target) var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve() if(!action || !ismob(target) || !isliving(target)) return FALSE @@ -236,14 +275,22 @@ var/mob/living/simple_animal/hostile/illusion/illusion_target = living_target if(illusion_target.parent_mob_ref?.resolve() == owner) return TRUE + if(istype(living_target, /mob/living/basic/resonant_mirage)) + var/mob/living/basic/resonant_mirage/illusion_target = living_target + if(illusion_target.parent_mob_ref?.resolve() == owner) + return TRUE return FALSE // We basically do a fake attack to sell the 'illusion'. We don't want it to actually deal damage, or people will have hissyfit arguments that these are 'harmful' and should be 'illegal' -/mob/living/simple_animal/hostile/illusion/mirage/resonant/AttackingTarget(atom/attacked_target) - if(!isliving(attacked_target)) +/mob/living/basic/resonant_mirage/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE) + if(!isliving(target)) + return FALSE + if(should_ignore_target(target)) + return FALSE + if(!early_melee_attack(target, modifiers, ignore_cooldown)) return FALSE - var/mob/living/living_target = attacked_target + var/mob/living/living_target = target do_attack_animation(living_target, ATTACK_EFFECT_PUNCH) var/verb_continuous = attack_verb_continuous || "attacks" @@ -261,6 +308,19 @@ if(attacked_sound) playsound(loc, attacked_sound, 25, TRUE, -1) + SEND_SIGNAL(src, COMSIG_HOSTILE_POST_ATTACKINGTARGET, target, TRUE) + return TRUE + +// Targeting strategy: never pick targets the mirage should ignore. +/datum/targeting_strategy/basic/mirage/can_attack(mob/living/living_mob, atom/target, vision_range) + . = ..() + if(!.) + return FALSE + if(!istype(living_mob, /mob/living/basic/resonant_mirage)) + return . + var/mob/living/basic/resonant_mirage/mirage = living_mob + if(mirage.should_ignore_target(target)) + return FALSE return TRUE diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm index 335e47db28652a..91f5edc23d275c 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/affinity/thaumaturge_spell_focus.dm @@ -3,14 +3,14 @@ desc = "An orb of raw thaumaturgic resonance, adjustable to take on any form of your choosing, one-time only. Needed to restore thaumaturgic powers." icon = 'icons/obj/weapons/guns/projectiles.dmi' icon_state = "ice_1" - slot_flags = ITEM_SLOT_BELT + slot_flags = NONE w_class = WEIGHT_CLASS_TINY obj_flags = UNIQUE_RENAME affinity = 2 // check thaumaturge_affinity.dm if you ever wonder what deserves what affinity /// If FALSE, suppress belt sprite entirely (prevents missing belt sprites). var/shows_on_belt = FALSE /// Short description of what this item is capable of, for radial menu uses. - var/menu_description = "An orb of energy. Fits in pockets. Can be worn on the belt. Very convenient, gives affinity 2 and is not visible in your hands, but doesn't do much more than that." + var/menu_description = "An orb of energy. Fits in pockets. Very convenient, gives affinity 2 and is not visible in your hands, but doesn't do much more than that." /obj/item/spell_focus/Initialize(mapload) . = ..() @@ -25,12 +25,6 @@ AddComponent(/datum/component/subtype_picker, focuses, CALLBACK(src, PROC_REF(on_spell_focus_picked))) -// Supresses belt sprites if unwanted whilst still allowing the slot to be used. -/obj/item/spell_focus/get_belt_overlay() - if(!shows_on_belt) - return null - return ..() - /obj/item/spell_focus/proc/on_spell_focus_picked(obj/item/spell_focus/new_focus, mob/living/picker) if(!istype(new_focus)) return @@ -51,15 +45,15 @@ inhand_icon_state = "kojiki" // they have no inhands but affinity3 needs inhands so we borrow another blue book instead throw_speed = 1 throw_range = 5 - slot_flags = ITEM_SLOT_BELT - shows_on_belt = TRUE + slot_flags = NONE + shows_on_belt = FALSE w_class = WEIGHT_CLASS_NORMAL attack_verb_continuous = list("bashes", "whacks", "educates") attack_verb_simple = list("bash", "whack", "educate") drop_sound = 'sound/items/handling/book_drop.ogg' pickup_sound = 'sound/items/handling/book_pickup.ogg' affinity = 3 - menu_description = "An arcane tome. Fits in your backpack and on your belt, and provides affinity 3; but does not fit in the pockets and is fairly conspicuous." + menu_description = "An arcane tome. Fits in your backpack, and provides affinity 3; but does not fit in the pockets and is fairly conspicuous." /obj/item/spell_focus/wand name = "thaumaturge's wand" @@ -69,10 +63,10 @@ inhand_icon_state = "wand" lefthand_file = 'icons/mob/inhands/items_lefthand.dmi' righthand_file = 'icons/mob/inhands/items_righthand.dmi' - slot_flags = ITEM_SLOT_BELT + slot_flags = NONE w_class = WEIGHT_CLASS_NORMAL affinity = 3 - menu_description = "A classical magic wand. Fits in your backpack and on your belt, and provides affinity 3; but does not fit in any pockets and is clearly visible when held." + menu_description = "A classical magic wand. Fits in your backpack, and provides affinity 3; but does not fit in any pockets and is clearly visible when held." /obj/item/spell_focus/staff name = "thaumaturge's staff" diff --git a/modular_doppler/modular_powers/code/powers_status_effect.dm b/modular_doppler/modular_powers/code/powers_status_effect.dm new file mode 100644 index 00000000000000..1cf3bd591ba5ea --- /dev/null +++ b/modular_doppler/modular_powers/code/powers_status_effect.dm @@ -0,0 +1,4 @@ +// base status effect type, just for the unit test. Might do something with this later. +/datum/status_effect/power + id = "power_abstract" + alert_type = null diff --git a/tgstation.dme b/tgstation.dme index 6d93ff382b5de2..1f598a9fdc4f16 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7467,6 +7467,7 @@ #include "modular_doppler\modular_powers\code\powers_living.dm" #include "modular_doppler\modular_powers\code\powers_prefs.dm" #include "modular_doppler\modular_powers\code\powers_prefs_middleware.dm" +#include "modular_doppler\modular_powers\code\powers_status_effect.dm" #include "modular_doppler\modular_powers\code\powers_subsystem.dm" #include "modular_doppler\modular_powers\code\powers_vv.dm" #include "modular_doppler\modular_powers\code\cargo\antiresonant_cuffs.dm" From 9cf8fa99656f0327c9bd112c5a702ee947b4dc3f Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 3 Apr 2026 09:11:06 +0200 Subject: [PATCH 181/212] More unit test fixing; makes actions still be granted by augments, fixes some status effects having no tick. Removed prestidigation. --- .../mortal/augmented/_premium_action.dm | 8 ++- .../powers/mortal/augmented/auto_retriever.dm | 6 +- .../powers/mortal/augmented/reagent_cannon.dm | 3 - .../travel_under_the_veil_of_night.dm | 4 +- .../powers/resonant/psyker/_psyker_organ.dm | 2 +- .../sorcerous/enigmatist/_enigmatist_root.dm | 5 -- .../code/powers/sorcerous/prestidigitation.dm | 60 ------------------- .../thaumaturge/affinity/thaumaturge_robes.dm | 5 -- .../theologist/_theologist_root_shared.dm | 2 +- tgstation.dme | 1 - 10 files changed, 13 insertions(+), 83 deletions(-) delete mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/prestidigitation.dm 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 8ff3e73ca1caf2..783a5039023339 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 @@ -36,8 +36,12 @@ // We have to delay giving the action because we communicate with the button, and this causes runtimes at roundstart. We use signalers to delay it until the huds there. /datum/action/item_action/organ_action/premium/GiveAction(mob/viewer) - if(!viewer || !viewer.hud_used) - if(viewer && !pending_hud_grant) + if(!viewer) + return + if(!viewer.hud_used) + // Still grant the action even without a HUD so unit tests and headless mobs pass. + LAZYOR(viewer.actions, src) + if(!pending_hud_grant) pending_hud_grant = TRUE RegisterSignal(viewer, COMSIG_MOB_HUD_CREATED, PROC_REF(on_hud_created), override = TRUE) return diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm index 104924edea67ee..a38f46504ea240 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/auto_retriever.dm @@ -5,7 +5,7 @@ name = "Premium ANGL Auto Retriever" desc = "Some assets are far too wealthy to risk losing. Created by DeForest, this allows their premium customers to be rescued from the most grievous of circumstances; and recently came with a support API for other healthcare providers.\ \n Once you reach critical condition or when manually activated, you begin a slow (and obvious) 10 second teleport towards your station's medbay lobby (regardless of Z-level).\ - Once it fires, a warning message is issued over the radio. The teleportation sets the quality to 0%, and can be interupted by Epinephrine in the bloodstream, EMP, or healing you above the critical threshold; after which it loses 25% quality and enters a several minute cooldown period.\ + Once it fires, a warning message is issued over the radio. The teleportation sets the quality to 0%, and can be interupted by Epinephrine, Atrophine or Stabilizing Agent in the bloodstream; EMP, or healing you above the critical threshold; after which it loses 25% quality and enters a several minute cooldown period.\ \n Decreases in quality twice as fast. Lower quality decreases the speed of the teleport." security_record_text = "Subject has a ANGL Auto Retriever and will teleport to medbay if critically injured." security_threat = POWER_THREAT_MAJOR @@ -17,7 +17,7 @@ name = "ANGL Auto Retriever" desc = "Some assets are far too wealthy to risk losing. Created by DeForest, this allows their premium customers to be rescued from the most grievous of circumstances; and recently came with a support API for other healthcare providers.\ \n Once you reach critical condition or when manually activated, you begin a slow (and obvious) 10 second teleport towards your station's medbay lobby (regardless of Z-level).\ - Once it fires, a warning message is issued over the radio. The teleportation sets the quality to 0%, and can be interupted by Epinephrine in the bloodstream, EMP, or healing you above the critical threshold; after which it loses 25% quality and enters a several minute cooldown period.\ + Once it fires, a warning message is issued over the radio. The teleportation sets the quality to 0%, aand can be interupted by Epinephrine, Atrophine or Stabilizing Agent in the bloodstream; EMP, or healing you above the critical threshold; after which it loses 25% quality and enters a several minute cooldown period.\ \n Decreases in quality twice as fast. Lower quality decreases the speed of the teleport." icon_state = "reviver_implant" slot = ORGAN_SLOT_HEART_AID @@ -154,7 +154,7 @@ return FALSE if(owner.stat < SOFT_CRIT) return TRUE - if(owner.reagents?.has_reagent(/datum/reagent/medicine/epinephrine) || owner.reagents?.has_reagent(/datum/reagent/medicine/atropine)) + if(owner.reagents?.has_reagent(/datum/reagent/medicine/epinephrine) || owner.reagents?.has_reagent(/datum/reagent/medicine/atropine) || owner.reagents?.has_reagent(/datum/reagent/stabilizing_agent)) return TRUE return FALSE diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm index 3459ddca3ddc90..c50ee7d25e0572 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/reagent_cannon.dm @@ -73,9 +73,6 @@ . = ..() if(!has_robotic_arm()) to_chat(arm_owner, span_warning("Your [name] does not fit in a non-cybernetic arm!")) - Remove(arm_owner, special = TRUE) - if(arm_owner?.loc) - forceMove(get_turf(arm_owner)) return // On EMP diff --git a/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm b/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm index 5dae79c9d782f0..22f4a3c43feeed 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/cultivator/travel_under_the_veil_of_night.dm @@ -6,7 +6,7 @@ /datum/power/cultivator/travel_under_the_veil_of_night name = "Travel Under the Veil of Night" - desc = "Whilst your alignment is active, you can spend 2 seconds channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Energy cost; no cooldown." + desc = "Whilst your alignment is active, you can spend 1 second channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Energy cost; no cooldown." security_record_text = "Subject can teleport in darkness while in their heightened state." security_threat = POWER_THREAT_MAJOR value = 4 @@ -15,7 +15,7 @@ /datum/action/cooldown/power/cultivator/travel_under_the_veil_of_night name = "Travel Under the Veil of Night" - desc = "Whilst your alignment is active, you can spend 2 seconds channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Energy cost; no cooldown." + desc = "Whilst your alignment is active, you can spend 1 second channeling in a space of darkness to teleport to another space of darkness within line of sight. Has a Energy cost; no cooldown." button_icon = 'icons/effects/effects.dmi' button_icon_state = "blank" diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm index 15eaf9deda9770..2bef5e445a31ad 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/_psyker_organ.dm @@ -149,7 +149,7 @@ // Warning message for high stress /datum/status_effect/power/stress_warning id = "stress_warning" - tick_interval = -1 SECONDS // This one's just a warning + tick_interval = STATUS_EFFECT_NO_TICK // This one's just a warning alert_type = /atom/movable/screen/alert/status_effect/stress_warning /atom/movable/screen/alert/status_effect/stress_warning diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm index f104ae4dcfe5f8..b3b40fda8722ec 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/enigmatist/_enigmatist_root.dm @@ -9,8 +9,3 @@ path = POWER_PATH_ENIGMATIST priority = POWER_PRIORITY_ROOT -/datum/power/enigmatist_root/add(client/client_source) - var/datum/action/cooldown/spell/touch/prestidigitation/that_magic_touch = new - that_magic_touch.Grant(power_holder) - - power_holder.mind?.teach_crafting_recipe(/datum/crafting_recipe/resonant_chalk) diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/prestidigitation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/prestidigitation.dm deleted file mode 100644 index 46155cc497965f..00000000000000 --- a/modular_doppler/modular_powers/code/powers/sorcerous/prestidigitation.dm +++ /dev/null @@ -1,60 +0,0 @@ - -/datum/action/cooldown/spell/touch/prestidigitation - name = "Prestidigitation" - desc = "Channel electricity to your hand to shock people with. Mostly harmless! Mostly... " - button_icon_state = "zap" - sound = 'sound/effects/magic/staff_healing.ogg' - cooldown_time = 7 SECONDS - invocation_type = INVOCATION_NONE - spell_requirements = SPELL_REQUIRES_NO_ANTIMAGIC - antimagic_flags = MAGIC_RESISTANCE - can_cast_on_self = TRUE - - hand_path = /obj/item/melee/touch_attack/prestidigitation - draw_message = span_notice("You channel resonance around your hand.") - drop_message = span_notice("You let the resonance around your hand dissipate.") - -/datum/action/cooldown/spell/touch/prestidigitation/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) - if(!iscarbon(victim)) - return FALSE - victim.wash(CLEAN_SCRUB) - return TRUE - -/datum/action/cooldown/spell/touch/prestidigitation/cast_on_secondary_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/caster) - if(!iscarbon(victim)) - return FALSE - var/obj/item/cigarette/cig = attached_hand.help_light_cig(victim) - if(isnull(cig)) - return FALSE - cig.light("With a single flick of [caster.p_their()] wrist, [caster] smoothly lights [caster == victim ? caster.p_their() : "[victim]'s"] [cig]. Damn [caster.p_theyre()] cool.") - return TRUE - -/obj/item/melee/touch_attack/prestidigitation - name = "\improper prestidigitation" - desc = "This is kind of like when you rub your feet on a shag rug so you can zap your friends, only a lot less safe." - icon = 'icons/obj/weapons/hand.dmi' - icon_state = "zapper" - inhand_icon_state = "zapper" - - // I can light my candles *from a distance*. - reach = 2 - // Doesn't need a permit, as opposed to other touch attacks. - item_flags = ABSTRACT | HAND_ITEM - // Allow you to light candles with this. - heat = HIGH_TEMPERATURE_REQUIRED - 100 - /// Sparks effect for special effects. - var/datum/effect_system/spark_spread/sparks - -/obj/item/melee/touch_attack/prestidigitation/Initialize(mapload) - . = ..() - sparks = new - sparks.set_up(2, 0, src) - sparks.attach(src) - -/obj/item/melee/touch_attack/prestidigitation/ignition_effect(atom/atom, mob/user) - if(!get_temperature()) - return - return span_infoplain(span_rose("With a single flick of [user.p_their()] wrist, [user] smoothly lights [atom]. Damn [user.p_theyre()] cool.")) - -/obj/item/melee/touch_attack/prestidigitation/attack_self(mob/user) - sparks.start() 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 edaa0e8abc99c6..ccabd7c4f0f578 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 @@ -7,7 +7,6 @@ icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' icon_state = "hivizmob" - inhand_icon_state = "hivizobj" armor_type = /datum/armor/none cold_protection = CHEST|GROIN|ARMS|HANDS|LEGS heat_protection = CHEST|GROIN|ARMS|HANDS|LEGS @@ -24,7 +23,6 @@ icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' icon_state = "hivizhat" - inhand_icon_state = "hivizhatobj" armor_type = /datum/armor/none fishing_modifier = -5 // high vishing affinity = 3 @@ -36,7 +34,6 @@ icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' icon_state = "secrobemob" - inhand_icon_state = "secrobesobj" armor_type = /datum/armor/armor_secjacket cold_protection = CHEST|GROIN|ARMS|HANDS|LEGS heat_protection = CHEST|GROIN|ARMS|HANDS|LEGS @@ -56,9 +53,7 @@ icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' worn_icon = 'modular_doppler/modular_powers/icons/items/thaumaturge_robes.dmi' icon_state = "sechat" - inhand_icon_state = "sechatobj" armor_type = /datum/armor/none fishing_modifier = -2 resistance_flags = FLAMMABLE affinity = 3 - diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm index 0e57d66304e292..b55ed1508e3110 100644 --- a/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm +++ b/modular_doppler/modular_powers/code/powers/sorcerous/theologist/_theologist_root_shared.dm @@ -325,7 +325,7 @@ /datum/status_effect/power/burden_shared id = "burden_shared" duration = 5 MINUTES // If somehow it overestays its welcome - tick_interval = -1 SECONDS // This one's just cosmetic + tick_interval = STATUS_EFFECT_NO_TICK alert_type = /atom/movable/screen/alert/status_effect/burden_shared /atom/movable/screen/alert/status_effect/burden_shared diff --git a/tgstation.dme b/tgstation.dme index 1f598a9fdc4f16..e78e8773dde540 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7588,7 +7588,6 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\hallucinate.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\severe\vomit.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\_resonant_projectile.dm" -#include "modular_doppler\modular_powers\code\powers\sorcerous\prestidigitation.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_chalks.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_root.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\enigmatist\_enigmatist_spell.dm" From 9192f1bb795088e49b6ca9c36f1d704f1ae6f039 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 3 Apr 2026 09:27:11 +0200 Subject: [PATCH 182/212] nulls premium augment ref data before destruction --- .../code/powers/mortal/augmented/_premium_augment.dm | 2 ++ 1 file changed, 2 insertions(+) diff --git a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm index 894513e44623fd..f206cc7112797b 100644 --- a/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm +++ b/modular_doppler/modular_powers/code/powers/mortal/augmented/_premium_augment.dm @@ -39,6 +39,8 @@ /datum/component/premium_augment/Destroy() STOP_PROCESSING(SSfastprocess, src) premium_actions = null + if(host && host.premium_component == src) // null ref datum before destruction + host.premium_component = null host = null return ..() From 7e4f2916a66ccd96b811d809c23f07228bca918b Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 4 Apr 2026 13:24:38 +0200 Subject: [PATCH 183/212] Fixes levitate float costs (it was based around the old 1.5 regen) --- .../modular_powers/code/powers/resonant/psyker/levitate.dm | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 84e7b237a8db2d..4b9f966b0821ce 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/levitate.dm @@ -57,9 +57,9 @@ // Passive stress cost if(active) var/mob/living/carbon/human/psyker = owner - var/cost = PSYKER_STRESS_TRIVIAL * 2 - if(psyker.get_quirk(/datum/quirk/paraplegic)) // There'll probably be several that'd like to do this. Effecively puts you just below the rate at which regen will keep up. - cost = PSYKER_STRESS_TRIVIAL + var/cost = PSYKER_STRESS_TRIVIAL * 1.5 + if(psyker.get_quirk(/datum/quirk/paraplegic)) // paraplegic gets it better + cost = PSYKER_STRESS_TRIVIAL * 0.5 modify_stress(cost * seconds_per_tick) // Dispel function; basically off-switch and possibly comedic faceplant From 107238b46869f4ffe961b2bfe36dc86f2fed83c5 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Mon, 6 Apr 2026 16:10:06 +0200 Subject: [PATCH 184/212] Adds prestidigitation. Lets you feel magical without any real investment in terms of charges and powers. --- .../sorcerous/thaumaturge/prestidigitation.dm | 218 ++++++++++++++++++ tgstation.dme | 1 + 2 files changed, 219 insertions(+) create mode 100644 modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm diff --git a/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm new file mode 100644 index 00000000000000..c85dae312b80e0 --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/sorcerous/thaumaturge/prestidigitation.dm @@ -0,0 +1,218 @@ +// The classic cantrip that does neat things to make you feel magical, without needing to spend precious spell charges. +/datum/power/thaumaturge/prestidigtation + name = "Prestidigtation" + desc = "Perform a minor feat of magic. Right-click to select between modes, Left-click to execute.\ + \nAllows you to do various actions like summoning sparks, cleaning objects and flavor food.\ + \nRequires Affinity 1. Does not scale with Affinity and does not use charges." + security_record_text = "Subject can perform minor magical tricks, such as creating sparks and flavoring food." + value = 1 + + action_path = /datum/action/cooldown/power/thaumaturge/prestidigtation + required_powers = list(/datum/power/thaumaturge_root) + +#define PRESTI_SUMMON_SPARKS "Summon Sparks" +#define PRESTI_CLEAN_OBJECTS "Clean Objects" +#define PRESTI_FLASH_MAGIC "Flash Magic" +#define PRESTI_FLAVOR_GOOD "Flavor Food (Good)" +#define PRESTI_FLAVOR_BAD "Flavor Food (Bad)" + +/datum/action/cooldown/power/thaumaturge/prestidigtation + name = "Prestidigtation" + desc = "Perform a minor feat of magic on an object or location within touch range. Right-click to select between modes, Left-click to execute" + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "spell_default" + + max_charges = 0 // does not interact with the charges system + required_affinity = 1 + + click_to_activate = TRUE + target_range = 1 + aim_assist = FALSE // complex targeting + + /// Currently selected prestidigitation mode. + var/selected_mode = PRESTI_SUMMON_SPARKS + +/datum/action/cooldown/power/thaumaturge/prestidigtation/InterceptClickOn(mob/living/clicker, params, atom/target) + var/list/mods = params2list(params) + if(LAZYACCESS(mods, RIGHT_CLICK)) + open_selection_menu(clicker) + return TRUE + + return ..() + +/// Routes for our various unique actions +/datum/action/cooldown/power/thaumaturge/prestidigtation/use_action(mob/living/user, atom/target) + switch(selected_mode) + if(PRESTI_SUMMON_SPARKS) + return summon_sparks(user, target) + if(PRESTI_CLEAN_OBJECTS) + return clean_objects(user, target) + if(PRESTI_FLASH_MAGIC) + return flash_magic(user, target) + if(PRESTI_FLAVOR_GOOD) + return flavor_food_good(user, target) + if(PRESTI_FLAVOR_BAD) + return flavor_food_bad(user, target) + return FALSE + +/// Right click selection menu that lets you choose what you are doing with your presti. +/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/open_selection_menu(mob/living/user) + if(!check_selection_menu(user)) + return FALSE + + var/list/radial_items = get_radial_items() + var/choice = show_radial_menu( + user, + user, // anchor for placement + radial_items, + custom_check = CALLBACK(src, PROC_REF(check_selection_menu), user, target), + tooltips = TRUE + ) + + if(!choice) + return FALSE + + selected_mode = choice + user.balloon_alert(user, "[selected_mode]") + return TRUE + +/// Validation for the right click +/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/check_selection_menu(mob/living/user, atom/target) + if(QDELETED(src)) + return FALSE + if(!istype(user)) + return FALSE + if(!can_use(user, target)) + return FALSE + return TRUE + +/// Populates our list of 'actions' our prestidigation spell can take. +/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/get_radial_items() + var/static/list/radial_items + if(radial_items) + return radial_items + + radial_items = list() + + var/list/options = list( + PRESTI_SUMMON_SPARKS = list("icon" = 'icons/effects/effects.dmi', "state" = "electricity3"), + PRESTI_CLEAN_OBJECTS = list("icon" = 'icons/obj/watercloset.dmi', "state" = "soap"), + PRESTI_FLASH_MAGIC = list("icon" = 'icons/mob/actions/actions_spells.dmi', "state" = "exit_possession"), + PRESTI_FLAVOR_GOOD = list("icon" = 'icons/obj/drinks/mixed_drinks.dmi', "state" = "wizz_fizz"), + PRESTI_FLAVOR_BAD = list("icon" = 'icons/obj/drinks/drinks.dmi', "state" = "acidspitglass") + ) + + for(var/option_name in options) + var/list/entry = options[option_name] + var/datum/radial_menu_choice/choice = new() + choice.name = option_name + choice.image = image(icon = entry["icon"], icon_state = entry["state"]) + radial_items[option_name] = choice + + return radial_items + +/// Summons sparks as if you were spamming the RCD +/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/summon_sparks(mob/living/user, atom/target) + var/turf/target_turf = get_turf(target) + if(!target_turf) + return FALSE + do_sparks(5, FALSE, target_turf) + return TRUE + +/// Cleans everything on the target turf. +/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/clean_objects(mob/living/user, atom/target) + var/turf/target_turf = get_turf(target) + if(!target_turf) + return FALSE + target_turf.wash(CLEAN_WASH, TRUE) + playsound(user, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + new /obj/effect/temp_visual/presti_clean(target_turf) + to_chat(user, span_notice("You clean [target]!")) + return TRUE + +/// Calls flash_blue. You did a magic thing; as placebo as it gets. +/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/flash_magic(mob/living/user, atom/target) + flash_blue(target) + playsound(user, 'sound/effects/magic/charge.ogg', 75, TRUE, SHORT_RANGE_SOUND_EXTRARANGE) + to_chat(user, span_notice("You make [target] feel magical! Wow!")) + return TRUE + +/// Flashes a target item blue briefly. +/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/flash_blue(atom/target) + if(!isatom(target)) + return + var/filter_id = "presti_flash" + target.add_filter(filter_id, 1, list(type = "outline", color = "#7266dd", size = 2, alpha = 255)) + target.transition_filter(filter_id, list("alpha" = 0), 2 SECONDS) // this actually looks smoother + addtimer(CALLBACK(target, PROC_REF(remove_filter), filter_id), 2 SECONDS) + +/// Adds a flavor component to food that makes it slightly better +/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/flavor_food_good(mob/living/user, atom/target) + if(!IS_EDIBLE(target)) + return FALSE + if(!target.reagents) + target.create_reagents(5, INJECTABLE) + target.AddComponent(/datum/component/prestidigitation_flavor, TRUE) + flash_blue(target) // temporary filter just to show people are tampering with food + playsound(user, 'sound/effects/magic/charge.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + to_chat(user, span_notice("You make [target] taste better!")) + return TRUE + +/// Adds a flavor component to food that makes it notoriously worse (its easier to screw it up than to make it better) +/datum/action/cooldown/power/thaumaturge/prestidigtation/proc/flavor_food_bad(mob/living/user, atom/target) + if(!IS_EDIBLE(target)) + return FALSE + if(!target.reagents) + target.create_reagents(5, INJECTABLE) + target.AddComponent(/datum/component/prestidigitation_flavor, FALSE) + flash_blue(target) // temporary filter just to show people are tampering with food + playsound(user, 'sound/effects/magic/charge.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) + to_chat(user, span_notice("You make [target] taste worse!")) + return TRUE + +#undef PRESTI_SUMMON_SPARKS +#undef PRESTI_CLEAN_OBJECTS +#undef PRESTI_FLASH_MAGIC +#undef PRESTI_FLAVOR_GOOD +#undef PRESTI_FLAVOR_BAD + +/// Temp effect for the cleaning sparkles +/obj/effect/temp_visual/presti_clean + icon_state = "shieldsparkles" + duration = 1 SECONDS + +/// Flavor component added by presti: tweaks quality and expires on eat. +/datum/component/prestidigitation_flavor + dupe_mode = COMPONENT_DUPE_UNIQUE_PASSARGS + /// TRUE for good flavor, FALSE for bad. + var/is_good = TRUE + /// Quality bonus applied to the food. + var/quality_bonus = 0 + +/datum/component/prestidigitation_flavor/Initialize(good_flavor = TRUE) + if(!IS_EDIBLE(parent)) + return COMPONENT_INCOMPATIBLE + is_good = good_flavor + quality_bonus = is_good ? 1 : 0 + +/datum/component/prestidigitation_flavor/RegisterWithParent() + RegisterSignal(parent, COMSIG_FOOD_EATEN, PROC_REF(on_food_eaten)) + if(quality_bonus) + RegisterSignal(parent, COMSIG_FOOD_GET_EXTRA_COMPLEXITY, PROC_REF(add_quality), TRUE) + +/datum/component/prestidigitation_flavor/UnregisterFromParent() + UnregisterSignal(parent, COMSIG_FOOD_EATEN) + UnregisterSignal(parent, COMSIG_FOOD_GET_EXTRA_COMPLEXITY) + +/datum/component/prestidigitation_flavor/proc/add_quality(datum/source, list/extra_complexity) + SIGNAL_HANDLER + extra_complexity[1] += quality_bonus + +/datum/component/prestidigitation_flavor/proc/on_food_eaten(datum/source, mob/eater, mob/feeder, bitecount, bite_consumption) + SIGNAL_HANDLER + if(!isliving(eater)) + return + var/mob/living/living_eater = eater + if(!is_good) // just give the disgusting food moodlet despite existing taste. + living_eater.add_mood_event("presti_flavor_bad", /datum/mood_event/disgusting_food) + qdel(src) diff --git a/tgstation.dme b/tgstation.dme index e78e8773dde540..390bb6e2da303c 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7602,6 +7602,7 @@ #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\gale_blast.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\magic_barrage.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\phantasmal_tool.dm" +#include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\prestidigitation.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\sanguine_absorption.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\vitalize_flora.dm" #include "modular_doppler\modular_powers\code\powers\sorcerous\thaumaturge\affinity\thaumaturge_affinity.dm" From 6b64b81248c61e1f21efb83f0262efd1c50a6f68 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Wed, 8 Apr 2026 07:01:10 +0200 Subject: [PATCH 185/212] respecs wolf from tank spec to melee dps spec. fixes a bug with sec textes. --- .../resonant/aberrant/shapechange_spider.dm | 6 ++++-- .../resonant/aberrant/shapechange_wolf.dm | 17 +++++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) 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 c2b30ec46ac15e..4772632615a7bf 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 @@ -1,4 +1,4 @@ -// Shapechange spider override. +/// Shapechange spider override. /datum/power/aberrant/shapechange_spider name = "Shapechange: Spider" desc = "Overrides your chosen Shapechange form with a spider variant. \n Hunters are fast but fragile, guards are slow and sturdy and ambush spiders are very slow, but have strong grabs, hard-hitting attacks and invisiblity in webs." @@ -15,11 +15,13 @@ return previous_form = shape_action.animal_form shape_action.animal_form = get_spider_form() + power_holder?.refresh_security_power_records() /datum/power/aberrant/shapechange_spider/remove() var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action() if(shape_action) shape_action.animal_form = previous_form + power_holder?.refresh_security_power_records() previous_form = null return ..() @@ -41,7 +43,7 @@ return spider_type return GLOB.shapechange_spider_form_types["Guard"] -// Preference choice for Shapechange spider form selection. +/// Preference choice for Shapechange spider form selection. /datum/preference/choiced/shapechange_spider_form category = PREFERENCE_CATEGORY_MANUALLY_RENDERED savefile_key = "shapechange_spider_form" diff --git a/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm b/modular_doppler/modular_powers/code/powers/resonant/aberrant/shapechange_wolf.dm index 5c20387bd0b012..ed2ef0955789c4 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 @@ -1,8 +1,8 @@ -// Inside you are two wolves. This one's an example of how to override the shapechange with special mobs. +/// Inside you are two wolves. This one's an example of how to override the shapechange with special mobs. /datum/power/aberrant/shapechange_wolf name = "Shapechange: Wolf" - desc = "Overrides your chosen Shapechange form with a Wolf; a sturdy creature with a strong bite attack." - value = 1 + desc = "Overrides your chosen Shapechange form with a Wolf; a fast creature with a strong bite attack." + value = 2 required_powers = list(/datum/power/aberrant/shapechange) /// Saved form so we can restore on removal. @@ -14,12 +14,14 @@ if(!shape_action) return previous_form = shape_action.animal_form - shape_action.animal_form = /mob/living/basic/mining/wolf/shapechange + shape_action.animal_form = /mob/living/basic/mining/wolf/fast + power_holder?.refresh_security_power_records() // updates sec records so it lists the right mob /datum/power/aberrant/shapechange_wolf/remove() var/datum/action/cooldown/power/aberrant/shapechange/shape_action = get_shapechange_action() if(shape_action) shape_action.animal_form = previous_form + power_holder?.refresh_security_power_records() // updates sec records so it lists the right mob previous_form = null return ..() @@ -32,7 +34,10 @@ return shape_action return null -// Wolves are pack animals and only deal 7dmg wich is SAD. We have a special verison -/mob/living/basic/mining/wolf/shapechange +// Wolves are pack animals and only deal 7dmg wich is SAD. We have a special version, which is less tanky but faster and bitier +/mob/living/basic/mining/wolf/fast + maxHealth = 100 + health = 100 melee_damage_lower = 10 melee_damage_upper = 20 + speed = -0.1 // keeps pace with naked humanoid mobs From 877f4c4ca106ed58d85977069671f3c07a36a79d Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 10 Apr 2026 20:11:14 +0200 Subject: [PATCH 186/212] Fixes mirage not to be a mess of redundant vars; adds two more psyker backlash events.. --- .../code/powers/resonant/psyker/mirage.dm | 75 +++---- .../catastrophic/mirage_gangup.dm | 189 ++++++++++++++++++ .../catastrophic/silence_trauma.dm | 2 +- .../catastrophic/tossed_around.dm | 119 +++++++++++ tgstation.dme | 2 + 5 files changed, 342 insertions(+), 45 deletions(-) create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm create mode 100644 modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm index ead772af2730f8..d190ef1c49acc1 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/mirage.dm @@ -31,7 +31,7 @@ // Mirage behavior mode var/mode = MIRAGE_MODE_STATIONARY // Stress cost - var/stress_cost = PSYKER_STRESS_MODERATE + var/stress_cost = PSYKER_STRESS_MODERATE * 1.5 // WE get the right click behavior to cycle behavior. /datum/action/cooldown/power/psyker/mirage/InterceptClickOn(mob/living/clicker, params, atom/target) @@ -58,15 +58,14 @@ // Creates a new instance of the mirrage var/mob/living/basic/resonant_mirage/new_mirage = new(spawn_turf) - new_mirage.Copy_Parent(owner, 20 SECONDS, 1, 0) + new_mirage.Copy_Parent(owner, 20 SECONDS) new_mirage.set_action_ref(src) new_mirage.apply_mode(mode) - new_mirage.match_owner_speed(owner) active_mirages += new_mirage // Causes it to act immediately. new_mirage.taunt_nearest_hostile(5) - new_mirage.kick_ai() + new_mirage.wake_ai() modify_stress(stress_cost) playsound(new_mirage, 'sound/effects/magic/magic_missile.ogg', 75, TRUE, SILENCED_SOUND_EXTRARANGE) @@ -108,27 +107,29 @@ icon_state = "static" icon_living = "static" icon_dead = "null" - gender = NEUTER mob_biotypes = NONE faction = list(FACTION_ILLUSION) basic_mob_flags = DEL_ON_DEATH death_message = "vanishes into thin air! It was a fake!" + + health = 1 + maxHealth = 1 + environment_smash = ENVIRONMENT_SMASH_NONE + /// Weakref to what we're copying var/datum/weakref/parent_mob_ref + var/datum/weakref/action_ref + /// the mode that was used to summon this creature + var/last_mode = MIRAGE_MODE_STATIONARY + /// ref for the alt apperance + var/alt_appearance_key -/mob/living/basic/resonant_mirage/proc/Copy_Parent(mob/living/original, life = 5 SECONDS, hp = 100, damage = 0) +/// Copies stats from the parent entity that summoned it, if any. +/mob/living/basic/resonant_mirage/proc/Copy_Parent(mob/living/original, life = 5 SECONDS) appearance = original.appearance + gender = original.gender parent_mob_ref = WEAKREF(original) setDir(original.dir) - maxHealth = hp - updatehealth() // re-cap health to new value - melee_damage_type = BRUTE - wound_bonus = CANT_WOUND - exposed_wound_bonus = 0 - sharpness = NONE - armour_penetration = 0 - obj_damage = 0 - environment_smash = ENVIRONMENT_SMASH_NONE transform = initial(transform) pixel_x = base_pixel_x pixel_y = base_pixel_y @@ -140,17 +141,7 @@ return parent_mob.examine(user) return ..() -/mob/living/basic/resonant_mirage - var/datum/weakref/action_ref - var/last_mode = MIRAGE_MODE_STATIONARY - var/alt_appearance_key - density = TRUE - obj_damage = 0 - environment_smash = ENVIRONMENT_SMASH_NONE - attack_verb_continuous = "attacks" - attack_verb_simple = "attack" - -// imposes the caster onto the mob +/// imposes the caster onto the mob /mob/living/basic/resonant_mirage/proc/set_action_ref(datum/action/cooldown/power/psyker/mirage/action) action_ref = WEAKREF(action) if(!alt_appearance_key) @@ -162,15 +153,13 @@ RegisterSignal(src, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_mirage_dir_change)) RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_mirage_dispel)) -/mob/living/basic/resonant_mirage/proc/match_owner_speed(mob/living/owner) - set_varspeed(1) // this was more complex when it was a simple mob to match the owner, but I had to change it. - -// Draw a nearby hostile's aggro to sell the illusion. +/// Draw a nearby hostile's aggro to sell the illusion. /mob/living/basic/resonant_mirage/proc/taunt_nearest_hostile(range_limit = 5) var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve() var/mob/living/nearest_mob var/nearest_dist + // Validation, cause taunting mobs is COMPLICATED for(var/mob/living/living_mob in range(range_limit, src)) if(living_mob == src || QDELETED(living_mob)) // no self taunting continue @@ -190,6 +179,7 @@ if(isnull(nearest_dist) || distance < nearest_dist) // get the nearest mob in range nearest_mob = living_mob nearest_dist = distance + if(nearest_mob) if(istype(nearest_mob, /mob/living/simple_animal/hostile)) // hostile mobs forced target var/mob/living/simple_animal/hostile/hostile_mob = nearest_mob @@ -200,7 +190,7 @@ nearest_mob.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, src) nearest_mob.ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, src) -// Applies the selection AI mode. Have your illusions act as you please :D +/// Applies the selection AI mode. Have your illusions act as you please :D /mob/living/basic/resonant_mirage/proc/apply_mode(new_mode) last_mode = new_mode @@ -212,6 +202,7 @@ if(MIRAGE_MODE_FLEE) set_ai_controller_type(/datum/ai_controller/basic_controller/simple/simple_fearful) +/// Sets the behavior type on the AI. /mob/living/basic/resonant_mirage/proc/set_ai_controller_type(controller_type) if(isnull(controller_type)) QDEL_NULL(ai_controller) @@ -224,8 +215,8 @@ ai_controller = new controller_type(src) ai_controller.set_blackboard_key(BB_TARGETING_STRATEGY, /datum/targeting_strategy/basic/mirage) -// we kick it to make it work. When this was a simple animal this wasn't as big of a problem, but /basic/ mobs are just more sluggish. -/mob/living/basic/resonant_mirage/proc/kick_ai() +/// 'Waking it up'. When this was a simple animal this wasn't as big of a problem, but /basic/ mobs are just more sluggish and with mirrages being meant to divert aggro, we want them reacting asap. +/mob/living/basic/resonant_mirage/proc/wake_ai() if(!ai_controller) return ai_controller.set_ai_status(AI_STATUS_ON) @@ -247,20 +238,20 @@ qdel(src) return DISPEL_RESULT_DISPELLED -// We need to tell the alt appearance variant to turn. +/// We need to tell the alt appearance variant to turn. /mob/living/basic/resonant_mirage/proc/on_mirage_dir_change(datum/source, old_dir, new_dir) SIGNAL_HANDLER var/image/appearance_image = hud_list?[alt_appearance_key] if(appearance_image) appearance_image.dir = new_dir -// If you have disbelieved the illusion (immune to mental) you can just walk through them. +/// If you have disbelieved the illusion (immune to mental) you can just walk through them. /mob/living/basic/resonant_mirage/CanAllowThrough(atom/movable/mover, border_dir) if(should_ignore_target(mover)) return TRUE return ..() -// Basically we check if they're our owner, are affected by mental or are an illusion of the same mob. +/// Basically we check if they're our owner, are affected by mental or are an illusion of the same mob. /mob/living/basic/resonant_mirage/proc/should_ignore_target(atom/target) var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve() if(!action || !ismob(target) || !isliving(target)) @@ -271,17 +262,13 @@ return TRUE if(!action.can_affect_mental(living_target)) // magic immune return TRUE - if(istype(living_target, /mob/living/simple_animal/hostile/illusion)) // ilusion of the same mob - var/mob/living/simple_animal/hostile/illusion/illusion_target = living_target - if(illusion_target.parent_mob_ref?.resolve() == owner) - return TRUE if(istype(living_target, /mob/living/basic/resonant_mirage)) var/mob/living/basic/resonant_mirage/illusion_target = living_target if(illusion_target.parent_mob_ref?.resolve() == owner) return TRUE return FALSE -// We basically do a fake attack to sell the 'illusion'. We don't want it to actually deal damage, or people will have hissyfit arguments that these are 'harmful' and should be 'illegal' +/// We basically do a fake attack to sell the 'illusion'. We don't want it to actually deal damage, or people will have hissyfit arguments that these are 'harmful' and should be 'illegal' /mob/living/basic/resonant_mirage/melee_attack(atom/target, list/modifiers, ignore_cooldown = FALSE) if(!isliving(target)) return FALSE @@ -311,7 +298,7 @@ SEND_SIGNAL(src, COMSIG_HOSTILE_POST_ATTACKINGTARGET, target, TRUE) return TRUE -// Targeting strategy: never pick targets the mirage should ignore. +/// Targeting strategy: never pick targets the mirage should ignore. /datum/targeting_strategy/basic/mirage/can_attack(mob/living/living_mob, atom/target, vision_range) . = ..() if(!.) @@ -324,7 +311,7 @@ return TRUE -// Alternate appearance for mirage: semi-transparent for owner and mental-immune viewers. +/// Alternate appearance for mirage: semi-transparent for owner and mental-immune viewers. /datum/atom_hud/alternate_appearance/basic/mirage_alpha var/datum/weakref/action_ref var/datum/weakref/owner_ref @@ -338,7 +325,7 @@ appearance_image.override = TRUE . = ..(key, appearance_image, options) -// Who is ALLOWED to see us for who we truly are? +/// Who is ALLOWED to see us for who we truly are? /datum/atom_hud/alternate_appearance/basic/mirage_alpha/mobShouldSee(mob/viewer) var/datum/action/cooldown/power/psyker/mirage/action = action_ref?.resolve() if(!action || !ismob(viewer) || !isliving(viewer)) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm new file mode 100644 index 00000000000000..9c750b14a9ea2f --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm @@ -0,0 +1,189 @@ +/// Summons copies of yourself to beat the snot out of you; or harrass others if you dare to run away. Unlike normal mirrages, these do hurt you. What hurts more is the social fuax pas of copies of yourself beating someoen else up. +/datum/psyker_event/catastrophic/mirage_gangup + lingering = TRUE + weight = PSYKER_EVENT_RARITY_RARE // this shouldn't be too common since this inconveniences others as well + /// How many mirages to spawn + var/spawn_count = 8 + /// Range around the psyker to spawn, in tiles + var/spawn_range = 3 + /// Lifetime of each mirage + var/mirage_lifetime = 20 SECONDS + +/datum/psyker_event/catastrophic/mirage_gangup/execute(mob/living/carbon/human/psyker) + to_chat(psyker, span_userdanger(PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE)) + psyker.cause_hallucination(/datum/hallucination/delusion/psyker_gangup, "psyker mirage gangup", duration = mirage_lifetime, psyker_owner = psyker) + + // Spawn a large semblence of illusions to heckle and harass us. + for(var/iteration = 0; iteration < spawn_count; iteration++) + addtimer(CALLBACK(src, PROC_REF(_spawn_gangup_mirage), psyker), iteration SECONDS) + + return TRUE + +/datum/psyker_event/catastrophic/mirage_gangup/proc/_spawn_gangup_mirage(mob/living/carbon/human/psyker) + if(!psyker || QDELETED(psyker)) + return + + var/turf/spawn_turf = pick_spawn_turf(psyker) + if(!spawn_turf) + return + + var/mob/living/basic/mirage_gangup/new_mirage = new(spawn_turf) + new_mirage.Copy_Parent(psyker, mirage_lifetime) + new_mirage.aggro_on(psyker) + +/datum/psyker_event/catastrophic/mirage_gangup/proc/pick_spawn_turf(mob/living/psyker) + var/list/valid_turfs = list() + for(var/turf/turf_candidate in view(spawn_range, psyker)) + if(!isopenturf(turf_candidate)) + continue + if(turf_candidate.is_blocked_turf(exclude_mobs = TRUE)) + continue + valid_turfs += turf_candidate + + if(length(valid_turfs)) + return pick(valid_turfs) + + return get_turf(psyker) + + +/// Mirage mob used by the gangup event. These are tougher and hit harder. We don't subtype the basic mirage because it has so much overhead with taunting +/mob/living/basic/mirage_gangup + name = "mirage" + desc = "An illusory copy turned deadly." + icon = 'icons/effects/effects.dmi' + icon_state = "static" + icon_living = "static" + icon_dead = "null" + mob_biotypes = NONE + faction = list(FACTION_ILLUSION) + basic_mob_flags = DEL_ON_DEATH + death_message = "dissipates into thin air!" + + health = 50 + maxHealth = 50 + melee_damage_lower = 10 + melee_damage_upper = 10 + environment_smash = ENVIRONMENT_SMASH_NONE + attack_sound = 'sound/items/weapons/punch1.ogg' + ai_controller = /datum/ai_controller/basic_controller/simple/simple_hostile // WHAT DO YOU MEAN THERE'S NO STANDARD AI CONTROLLER? + + /// Weakref to what we're copying + var/datum/weakref/parent_mob_ref + /// ref for the alt appearance + var/alt_appearance_key + +/// Copies stats from the parent entity that summoned it, if any. +/mob/living/basic/mirage_gangup/proc/Copy_Parent(mob/living/original, life = 20 SECONDS) + appearance = original.appearance + name = original.name + real_name = original.real_name + gender = original.gender + parent_mob_ref = WEAKREF(original) + setDir(original.dir) + transform = initial(transform) + pixel_x = base_pixel_x + pixel_y = base_pixel_y + _setup_alt_appearance(original) + addtimer(CALLBACK(src, TYPE_PROC_REF(/mob/living, death)), life) + +/// Force this mirage to focus the psyker. +/mob/living/basic/mirage_gangup/proc/aggro_on(mob/living/target) + if(!target || QDELETED(target) || !ai_controller) + return + if(ispath(ai_controller)) + ai_controller = new ai_controller(src) + ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, target) + ai_controller.insert_blackboard_key_lazylist(BB_BASIC_MOB_RETALIATE_LIST, target) + ai_controller.set_ai_status(AI_STATUS_ON) + ai_controller.SelectBehaviors(0.1) + +/// Set up the alternate appearance so the psyker and mental-immune viewers see through it. Also sneaks in the dispel signaler. +/mob/living/basic/mirage_gangup/proc/_setup_alt_appearance(mob/living/owner) + if(alt_appearance_key) + return + alt_appearance_key = "mirage_gangup_static_[REF(src)]" + var/image/appearance_image = image('icons/effects/effects.dmi', src, "static") + appearance_image.dir = dir + add_alt_appearance(/datum/atom_hud/alternate_appearance/basic/mirage_gangup_static, alt_appearance_key, appearance_image, owner) + RegisterSignal(src, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_mirage_dir_change)) + RegisterSignal(src, COMSIG_ATOM_DISPEL, PROC_REF(on_mirage_dispel)) + +/mob/living/basic/mirage_gangup/Destroy() + if(alt_appearance_key) + remove_alt_appearance(alt_appearance_key) + alt_appearance_key = null + UnregisterSignal(src, COMSIG_ATOM_DIR_CHANGE) + UnregisterSignal(src, COMSIG_ATOM_DISPEL) + return ..() + +/mob/living/basic/mirage_gangup/proc/on_mirage_dispel(datum/source, atom/dispeller) + SIGNAL_HANDLER + qdel(src) + return DISPEL_RESULT_DISPELLED + +/mob/living/basic/mirage_gangup/proc/on_mirage_dir_change(datum/source, old_dir, new_dir) + SIGNAL_HANDLER + var/image/appearance_image = hud_list?[alt_appearance_key] + if(appearance_image) + appearance_image.dir = new_dir + +/// Alternate appearance for mirage gangup: static outline for mental-immune viewers to show that they are in fact hostile mobs. +/datum/atom_hud/alternate_appearance/basic/mirage_gangup_static + var/datum/weakref/owner_ref + +/datum/atom_hud/alternate_appearance/basic/mirage_gangup_static/New(key, image/appearance_image, mob/living/owner, options = AA_TARGET_SEE_APPEARANCE) + owner_ref = WEAKREF(owner) + if(appearance_image) + appearance_image.override = TRUE + . = ..(key, appearance_image, options) + +/// Who is ALLOWED to see us for who we truly are? +/datum/atom_hud/alternate_appearance/basic/mirage_gangup_static/mobShouldSee(mob/viewer) + if(!ismob(viewer) || !isliving(viewer)) + return FALSE + var/mob/living/owner = owner_ref?.resolve() + if(owner && viewer == owner) + return FALSE + return !can_affect_mental(viewer) + +/// Validates if the target is affected by mental effects. +/datum/atom_hud/alternate_appearance/basic/mirage_gangup_static/proc/can_affect_mental(mob/living/target) + if(!target) + return FALSE + if(target.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0)) + return FALSE + if(target.can_block_magic(MAGIC_RESISTANCE, charge_cost = 0)) + return FALSE + if(target.can_block_resonance(0)) + return FALSE + if(HAS_TRAIT(target, TRAIT_DUMB)) + return FALSE + return TRUE + +/// Delusion: everyone looks like the psyker (for the psyker only). +/datum/hallucination/delusion/psyker_gangup + random_hallucination_weight = 0 + affects_us = FALSE + affects_others = TRUE + delusion_name = "psyker" + /// Who we're copying + var/datum/weakref/psyker_ref + +/datum/hallucination/delusion/psyker_gangup/New(mob/living/hallucinator, duration, mob/living/psyker_owner) + if(psyker_owner) + psyker_ref = WEAKREF(psyker_owner) + delusion_name = psyker_owner.name + return ..(hallucinator, duration) + +// override just to pass along psyker_owner +/datum/hallucination/delusion/psyker_gangup/make_delusion_image(mob/over_who) + var/image/funny_image = image(loc = over_who) + var/mob/living/psyker_owner = psyker_ref?.resolve() + if(psyker_owner) + funny_image.appearance = psyker_owner.appearance + else + funny_image.appearance = over_who.appearance + funny_image.name = delusion_name + funny_image.override = TRUE + SET_PLANE_EXPLICIT(funny_image, ABOVE_GAME_PLANE, over_who) + return funny_image diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm index 979877c9289dd9..3c7a6932bf868d 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/silence_trauma.dm @@ -1,6 +1,6 @@ // Gives a special deep-rooted trauma that silences Resonance powers all-together. /datum/psyker_event/catastrophic/silence_trauma - weight = PSYKER_EVENT_RARITY_UNCOMMON + weight = PSYKER_EVENT_RARITY_RARE /datum/psyker_event/catastrophic/silence_trauma/execute(mob/living/carbon/human/psyker) var/datum/brain_trauma/magic/trauma = new /datum/brain_trauma/magic/resonance_silenced diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm new file mode 100644 index 00000000000000..15790e854397ba --- /dev/null +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm @@ -0,0 +1,119 @@ +/// Tosses you around physically into various dangerous objects. +/datum/psyker_event/catastrophic/tossed_around + lingering = TRUE + weight = PSYKER_EVENT_RARITY_UNCOMMON + + /// Pity system + var/max_ticks = 20 + + // The throw range and speed + var/throw_range = 10 + var/throw_speed = 3 + + /// Hand-made list of objects we prefer to smash people into, and will default to when throwing. Should only contain items with funny effects when thrown into them. + var/list/special_object_types = list( + /turf/open/chasm, // this one's evil + /turf/open/space, + /turf/open/lava/plasma/ice_moon, // how cooked do you want your spaceman + /turf/open/floor/tram/plate, // THE TRAM CALLS + /obj/structure/table/glass, + /obj/structure/window, + /obj/structure/grille, + /obj/machinery/teleport/hub, + /obj/machinery/vending, // we have a special interaction where there is a chance they're knocked over. + /obj/structure/musician, // they make funny noises + /obj/machinery/disposal, // flushes you too + /obj/machinery/power/supermatter_crystal, // if you break down next to a SM crystal you deserve this + /mob/living, + + ) + + /// typecach for reference sake + var/static/list/special_object_typecache + /// Track impact handling for this event + var/mob/living/carbon/human/impact_owner + var/expecting_impact = FALSE + +/datum/psyker_event/catastrophic/tossed_around/execute(mob/living/carbon/human/psyker) + to_chat(psyker, span_userdanger("Your Resonant powers send you hurling through the air!")) + RegisterSignal(psyker, COMSIG_MOVABLE_IMPACT, PROC_REF(on_toss_impact)) + impact_owner = psyker + addtimer(CALLBACK(src, PROC_REF(_toss_tick), psyker, 0), 1 SECONDS) + return TRUE + +/datum/psyker_event/catastrophic/tossed_around/proc/_toss_tick(mob/living/carbon/human/psyker, tick_count) + if(!psyker || QDELETED(psyker)) + qdel(src) + return + if(tick_count >= max_ticks) + qdel(src) + return + + // no escape + psyker.Knockdown(3 SECONDS) + + if(!special_object_typecache) + special_object_typecache = typecacheof(special_object_types) + + var/list/nearby_specials = typecache_filter_list(oview(throw_range, psyker), special_object_typecache) + var/list/valid_specials = list() + + for(var/atom/special as anything in nearby_specials) + if(special == psyker) // makes it so mob/living doesnt throw the psyker at themselves + continue + if(can_see(psyker, special, throw_range)) + valid_specials += special + + var/turf/target_turf + var/atom/target_special + if(length(valid_specials)) // if we have special things to throw people at + target_special = pick(valid_specials) + target_turf = get_turf(target_special) + else // if we don't: just toss them somewhere random + target_turf = get_ranged_target_turf(psyker, pick(GLOB.alldirs), throw_range) + + var/datum/callback/throw_callback + if(target_turf) // YEET! + psyker.throw_at(target_turf, range = throw_range, speed = throw_speed, thrower = psyker, spin = TRUE, callback = throw_callback) + + // 95% chance to continue applying effects + if(!prob(95)) + qdel(src) + return + + addtimer(CALLBACK(src, PROC_REF(_toss_tick), psyker, tick_count + 1), 1 SECONDS) + +/datum/psyker_event/catastrophic/tossed_around/proc/flush_disposal(mob/living/carbon/human/psyker, obj/machinery/disposal/target_disposal) + if(!psyker || QDELETED(psyker) || !target_disposal || QDELETED(target_disposal)) + return + target_disposal.flush() + return + +/datum/psyker_event/catastrophic/tossed_around/proc/on_toss_impact(atom/movable/source, atom/hit_atom, datum/thrownthing/throwingdatum) + SIGNAL_HANDLER + + var/mob/living/carbon/human/psyker = source + if(!psyker || QDELETED(psyker)) + return + + // At least 5 brute on any impact + psyker.apply_damage(5, BRUTE) + + // If we hit a disposal bin, force the mob into it and flush. + if(istype(hit_atom, /obj/machinery/disposal)) + var/obj/machinery/disposal/target_disposal = hit_atom + if(psyker.loc != target_disposal) + psyker.forceMove(target_disposal) + target_disposal.update_appearance() + target_disposal.flush = TRUE + // If we hit a vending machine, give it a chance to knock over onto the psyker. + else if(istype(hit_atom, /obj/machinery/vending)) + if(prob(50)) + var/obj/machinery/vending/vendor = hit_atom + vendor.tilt(psyker) + +/datum/psyker_event/catastrophic/tossed_around/Destroy() + if(impact_owner) + UnregisterSignal(impact_owner, COMSIG_MOVABLE_IMPACT) + impact_owner = null + return ..() diff --git a/tgstation.dme b/tgstation.dme index 199f882bba7b4b..8332be7f3213fd 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -7579,8 +7579,10 @@ #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\brain_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\cardiac_arrest.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\magic_trauma.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\mirage_gangup.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\silence_trauma.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\telekinetic_backlash.dm" +#include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\catastrophic\tossed_around.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\dizziness.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\headache.dm" #include "modular_doppler\modular_powers\code\powers\resonant\psyker\psyker_events\mild\nosebleed.dm" From 5a2aa76e937d78be5fbeaf2c2f20c0f62516feb5 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Fri, 10 Apr 2026 20:22:58 +0200 Subject: [PATCH 187/212] I forgor the to_chat feedback, woops. --- .../resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm index 9c750b14a9ea2f..b57f898f2aca73 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/mirage_gangup.dm @@ -10,7 +10,7 @@ var/mirage_lifetime = 20 SECONDS /datum/psyker_event/catastrophic/mirage_gangup/execute(mob/living/carbon/human/psyker) - to_chat(psyker, span_userdanger(PSYKER_EVENT_CATASTROPHIC_STANDARD_MESSAGE)) + to_chat(psyker, span_userdanger("Your Resonant powers send your mind spiraling; everyone is looking like you, and at you!")) psyker.cause_hallucination(/datum/hallucination/delusion/psyker_gangup, "psyker mirage gangup", duration = mirage_lifetime, psyker_owner = psyker) // Spawn a large semblence of illusions to heckle and harass us. From e3d397df2fcce250d78724fa59036f75fd8e29c6 Mon Sep 17 00:00:00 2001 From: TheOneAndOnlyCreeperJoe Date: Sat, 11 Apr 2026 09:36:36 +0200 Subject: [PATCH 188/212] Minor QoL: hovering over the buttons lists their reuqirements. Tossed around now allows any lava instead of just ice moon's lava. --- .../psyker_events/catastrophic/tossed_around.dm | 2 +- .../code/powers_prefs_middleware.dm | 17 +++++++++++++++++ .../interfaces/PreferencesMenu/PowersMenu.tsx | 12 +++++++++++- .../tgui/interfaces/PreferencesMenu/types.ts | 3 +++ 4 files changed, 32 insertions(+), 2 deletions(-) diff --git a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm index 15790e854397ba..babd998618c90f 100644 --- a/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm +++ b/modular_doppler/modular_powers/code/powers/resonant/psyker/psyker_events/catastrophic/tossed_around.dm @@ -14,7 +14,7 @@ var/list/special_object_types = list( /turf/open/chasm, // this one's evil /turf/open/space, - /turf/open/lava/plasma/ice_moon, // how cooked do you want your spaceman + /turf/open/lava, // how cooked do you want your spaceman /turf/open/floor/tram/plate, // THE TRAM CALLS /obj/structure/table/glass, /obj/structure/window, diff --git a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm index 696fea44b470b8..839e28c3b7b9c8 100644 --- a/modular_doppler/modular_powers/code/powers_prefs_middleware.dm +++ b/modular_doppler/modular_powers/code/powers_prefs_middleware.dm @@ -94,6 +94,20 @@ var/datum/power_constant_data/constant_data = GLOB.all_power_constant_data[power_type] var/list/customization_options = constant_data?.get_customization_data() + // Gets the powers required per power and adds their names, to display when hovered over. + var/list/required_power_types = GLOB.powers_requirements_list[power_type] + var/list/required_power_names = list() + if(length(required_power_types)) + for(var/datum/power/required_power_type as anything in required_power_types) + var/required_power_name = required_power_type.name + // Trims abstract from abstract roots. + if(length(required_power_name) >= 9 && lowertext(copytext(required_power_name, 1, 10)) == "abstract ") + required_power_name = copytext(required_power_name, 10) + required_power_names += required_power_name + // Gets special requirements such as allow any and allow subtypes + var/required_allow_any = power_type.required_allow_any + var/required_allow_subtypes = power_type.required_allow_subtypes + var/final_list = list(list( "description" = power_type.desc, "name" = power_type.name, @@ -104,6 +118,9 @@ "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, diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx index fcdd361f01d313..83ab40fb32a6cf 100644 --- a/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/PowersMenu.tsx @@ -70,7 +70,17 @@ export const Powers = (props) => {