diff --git a/tff_modular/modules/psyonics/code/_psyonics.dm b/tff_modular/modules/psyonics/code/_psyonics.dm new file mode 100644 index 00000000000..cf6475344d3 --- /dev/null +++ b/tff_modular/modules/psyonics/code/_psyonics.dm @@ -0,0 +1,193 @@ +// Тут хранятся некрасивые базовые классы и прочее. Не смотрите сюда. + +/datum/action/cooldown/spell + // Сколько маны стоит кастануть спелл + var/mana_cost = 10 + // Некоторые спеллы могут отнимать стамину + var/stamina_cost = 0 + // Что написать жертве + var/target_msg + // Сила способности + var/cast_power = 0 + // Вторичная школа. Может дать особые эффекты при комбинациях + var/secondary_school = 0 + +// Спеллы для призвания предмета +/datum/action/cooldown/spell/conjure_item/psyonic + delete_old = FALSE + delete_on_failure = TRUE + requires_hands = TRUE + // Псионические способности (в основном) не блокируются, но выводят особенные сообщения тем, кто это может + antimagic_flags = MAGIC_RESISTANCE_MIND + spell_requirements = NONE + cooldown_reduction_per_rank = 0 SECONDS + +/datum/action/cooldown/spell/conjure_item/psyonic/New(Target, power, additional_school) + . = ..() + cast_power = power + secondary_school = additional_school + +// Проверяем достаточно ли маны +/datum/action/cooldown/spell/proc/check_for_mana() + var/mob/living/carbon/human/caster = owner + var/datum/quirk/psyonic/quirk_holder = caster.get_quirk(/datum/quirk/psyonic) + if(quirk_holder && (quirk_holder.mana_level - mana_cost) >= 0) + return TRUE + else + return FALSE + +// Сосём ману у псионика +/datum/action/cooldown/spell/proc/drain_mana(forced = FALSE) + var/mob/living/carbon/human/caster = owner + var/datum/quirk/psyonic/quirk_holder = caster.get_quirk(/datum/quirk/psyonic) + caster.adjustStaminaLoss(stamina_cost, forced = TRUE) + if(quirk_holder && (quirk_holder.mana_level - mana_cost) >= 0) + quirk_holder.mana_level -= mana_cost + return TRUE + else if (forced) + quirk_holder.mana_level = 0 + return TRUE + else + return FALSE + +/datum/action/cooldown/spell/conjure_item/psyonic/can_cast_spell(feedback) + . = ..() + if(!.) + return FALSE + + if(!check_for_mana()) + return FALSE + else + return TRUE + +/datum/action/cooldown/spell/conjure_item/psyonic/cast(atom/cast_on) + drain_mana() + return ..() + +// Для спеллов которые применяются на себя тыком кнопки a.k.a. выдача генов +/datum/action/cooldown/spell/psyonic + // Псионические способности (в основном) не блокируются, но выводят особенные сообщения тем, кто это может + antimagic_flags = MAGIC_RESISTANCE_MIND + + school = SCHOOL_UNSET + invocation_type = INVOCATION_NONE + spell_requirements = NONE + cooldown_reduction_per_rank = 0 SECONDS + +/datum/action/cooldown/spell/psyonic/New(Target, power, additional_school) + . = ..() + cast_power = power + secondary_school = additional_school + +/datum/action/cooldown/spell/psyonic/can_cast_spell(feedback) + . = ..() + if(!.) + return FALSE + + if(!check_for_mana()) + return FALSE + else + return TRUE + +// Спеллы для пострелушек +/datum/action/cooldown/spell/pointed/projectile/psyonic + // Псионические способности (в основном) не блокируются, но выводят особенные сообщения тем, кто это может + antimagic_flags = MAGIC_RESISTANCE_MIND + + school = SCHOOL_UNSET + invocation_type = INVOCATION_NONE + spell_requirements = NONE + cooldown_reduction_per_rank = 0 SECONDS + +/datum/action/cooldown/spell/pointed/projectile/psyonic/New(Target, power, additional_school) + . = ..() + cast_power = power + secondary_school = additional_school + +/datum/action/cooldown/spell/pointed/projectile/psyonic/can_cast_spell(feedback) + . = ..() + if(!.) + return FALSE + + if(!check_for_mana()) + return FALSE + else + return TRUE + +// Направленные спеллы a.k.a. псионик выбирают цель на дистанции +/datum/action/cooldown/spell/pointed/psyonic + // Псионические способности (в основном) не блокируются, но выводят особенные сообщения тем, кто это может + antimagic_flags = MAGIC_RESISTANCE_MIND + school = SCHOOL_UNSET + invocation_type = INVOCATION_NONE + spell_requirements = NONE + cooldown_reduction_per_rank = 0 SECONDS + +/datum/action/cooldown/spell/pointed/psyonic/New(Target, power, additional_school) + . = ..() + cast_power = power + secondary_school = additional_school + +/datum/action/cooldown/spell/pointed/psyonic/can_cast_spell(feedback) + . = ..() + if(!.) + return FALSE + + if(!check_for_mana()) + return FALSE + else + return TRUE + +// Спеллы которыми надо каснуться чего либо +/datum/action/cooldown/spell/touch/psyonic + // Псионические способности (в основном) не блокируются, но выводят особенные сообщения тем, кто это может + antimagic_flags = MAGIC_RESISTANCE_MIND + school = SCHOOL_UNSET + invocation_type = INVOCATION_NONE + spell_requirements = NONE + +/datum/action/cooldown/spell/touch/psyonic/New(Target, power, additional_school) + . = ..() + cast_power = power + secondary_school = additional_school + +/datum/action/cooldown/spell/touch/psyonic/can_cast_spell(feedback) + . = ..() + if(!.) + return FALSE + + if(!check_for_mana()) + return FALSE + else + return TRUE + +/datum/action/cooldown/spell/touch/psyonic/create_hand(mob/living/carbon/cast_on) + . = ..() + if(!.) + return . + var/obj/item/bodypart/transfer_limb = cast_on.get_active_hand() + if(IS_ROBOTIC_LIMB(transfer_limb)) + to_chat(cast_on, span_notice("You fail to channel your psyonic powers through your inorganic hand.")) + return FALSE + + return TRUE + +/particles/droplets/psyonic + icon = 'icons/effects/particles/generic.dmi' + icon_state = list("dot"=2,"drop"=1) + width = 32 + height = 36 + count = 20 + spawning = 0.2 + lifespan = 1.5 SECONDS + fade = 0.5 SECONDS + color = "#00a2ff" + position = generator(GEN_BOX, list(-9,-9,0), list(9,18,0), NORMAL_RAND) + scale = generator(GEN_VECTOR, list(0.9,0.9), list(1.1,1.1), NORMAL_RAND) + gravity = list(0, 0.95) + +// Проверка на то, есть ли квирк псионики у хумана +/mob/living/carbon/human/proc/ispsyonic() + if(has_quirk(/datum/quirk/psyonic)) + return TRUE + return FALSE diff --git a/tff_modular/modules/psyonics/code/_quirk.dm b/tff_modular/modules/psyonics/code/_quirk.dm new file mode 100644 index 00000000000..d48444b5e76 --- /dev/null +++ b/tff_modular/modules/psyonics/code/_quirk.dm @@ -0,0 +1,215 @@ +#define TRAIT_PSYONIC_USER "psyonicuser" +#define TRAIT_NO_PSYONICS "no_psyonics" +#define TRAIT_PRO_PSYONICS "pro_psyonics" + +#define LATENT_PSYONIC 0 +#define OPERANT_PSYONIC 1 +#define MASTER_PSYONIC 2 +#define GRANDMASTER_PSYONIC 3 +#define PARAMOUNT_PSYONIC 4 +#define GREATEST_PSYONIC 5 + +GLOBAL_LIST_INIT(psyonic_schools, list( + "Redaction", + "Coercion", + "Psychokinesis", + "Energistics", +)) + +/datum/quirk/psyonic + name = "Psyonic Abilities" + desc = "Either you were born like this or gained powers from implants/training or other events - you are a psyonic. \ + Your mind can access the world that lies beyond our mortal plane. One day voices from within had pierced your skull \ + like a tide wave turns a sailboat over in open sea, but you withstanded it and received abilities your father haven't \ + even dreamed of. From now on a special type of energy is stored in your mind, body and soul and you have control over it. \ + Every psyonic is a follower of a certain school: \ + Redaction - school of mending and curing bodies and souls; \ + Coercion - school of trickery and controlling others; \ + Psychokinesis - school of object manipulation; \ + Energistics - school of elecricity, fire and light; \ + You can select the school, but it's power will be randomised every round." + value = 12 // Отдадите за псионику жопу, чтобы потом вам Рэнди Рандом всегда слал наименьший уровень силы + medical_record_text = "Patient possesses connection to an another plain of reality." + quirk_flags = QUIRK_HIDE_FROM_SCAN|QUIRK_HUMAN_ONLY|QUIRK_PROCESSES // Сканеры не видят псиоников. Только псионик школы принуждения может точно определить, является ли живое существо псиоником + gain_text = span_cyan("You mind feels uneasy, but... so powerful.") + lose_text = span_warning("You lost something, that kept your connection with other realms.") + icon = "fa-star" + mob_trait = TRAIT_PSYONIC_USER + veteran_only = TRUE + allow_for_donator = TRUE + // Текущий уровень маны + var/mana_level = 0 + // Максимально возможный уровень маны + var/max_mana = 10 + // Уровень псионических способностей + var/psyonic_level = 0 + // Строка для описания уровня + var/psyonic_level_string = "Latent" + // Первичная школа псионики + var/school + // Вторичная школа псионики + var/secondary_school + /// Два вара скопированные из item_quirk для правильной выдачи лицензии + var/list/where_items_spawned + var/open_backpack = FALSE + +/datum/quirk/psyonic/add(client/client_source) + school = client_source?.prefs?.read_preference(/datum/preference/choiced/psyonic_school) + if(!school) + school = pick(GLOB.psyonic_schools) + secondary_school = client_source?.prefs?.read_preference(/datum/preference/choiced/psyonic_school_secondary) + if(!secondary_school) + secondary_school = pick(GLOB.psyonic_schools) + var/mob/living/carbon/human/whom_to_give = quirk_holder + var/fluff_1 = rand(0,1) + var/fluff_2 = rand(0,1) + var/fluff_3 = rand(0,1) + var/fluff_4 = rand(0,1) + psyonic_level = fluff_1 + fluff_2 + fluff_3 + fluff_4 + if(HAS_MIND_TRAIT(whom_to_give, TRAIT_MADNESS_IMMUNE)) // A.K.A. Психолог + psyonic_level += rand(0,1) // _возможное_ доп очко + switch(psyonic_level) + if(LATENT_PSYONIC) + psyonic_level_string = "Pi" + if(OPERANT_PSYONIC) + psyonic_level_string = "Omicron" + if(MASTER_PSYONIC) + psyonic_level_string = "Kappa" + if(GRANDMASTER_PSYONIC) + psyonic_level_string = "Lambda" + if(PARAMOUNT_PSYONIC) + psyonic_level_string = "Theta" + if(GREATEST_PSYONIC) // Дозволен только особо везучим психологам, у которых все предыдущие пять рандомов вышли на 1 + psyonic_level_string = "Epsilon" + max_mana = (psyonic_level + 1) * 20 // Минимальный - 20, максимальный - 100 + RegisterSignal(quirk_holder, COMSIG_MOB_GET_STATUS_TAB_ITEMS, PROC_REF(get_status_tab_item)) + if(school == secondary_school) + psyonic_level += 1 // Если вторичка совпадает с первой - добавляем один уровень, но не меняем описание + switch(school) + if("Redaction") + whom_to_give.try_add_redaction_school(psyonic_level, secondary_school) + if("Coercion") + whom_to_give.try_add_coercion_school(psyonic_level, secondary_school) + if("Psychokinesis") + whom_to_give.try_add_psychokinesis_school(psyonic_level, secondary_school) + if("Energistics") + whom_to_give.try_add_energistics_school(psyonic_level, secondary_school) + + if(secondary_school != school) // Если школы разные, добавить способность нулевого уровня вторичной школы + switch(secondary_school) + if("Redaction") + whom_to_give.try_add_redaction_school(0, 0) + if("Coercion") + whom_to_give.try_add_coercion_school(0, 0) + if("Psychokinesis") + whom_to_give.try_add_psychokinesis_school(0, 0) + if("Energistics") + whom_to_give.try_add_energistics_school(0, 0) + + var/fluff_text = span_cyan("Current psionic factors:") + "
" + \ + "[fluff_1 ? "Current star position is aligned to your soul." : "The stars do not precede luck to you."]" + "
" + \ + "[fluff_2 ? "Other realms are unusually active this shift." : "Other realms are quiet today."]" + "
" + \ + "[fluff_3 ? "Time-bluespace continuum seems to be stable today." : "Time-bluespace continuum is not giving you energy today."]" + "
" + \ + "[fluff_4 ? "Your mind is clearly open to otherwordly energy." : "Something clouds your connection to otherworld energy."]" + to_chat(quirk_holder, examine_block(span_infoplain(jointext(fluff_text, "\n• ")))) + psyonic_level -= 1 // Обязаловка, иначе выдаст спеллы которые нельзя кастануть + + var/obj/item/card/psyonic_license/new_license = new(whom_to_give) + + give_item_to_holder(new_license, list(LOCATION_BACKPACK = ITEM_SLOT_BACKPACK, LOCATION_HANDS = ITEM_SLOT_HANDS), flavour_text = "Make sure not to lose it. You can not remake this on the station.") + +/datum/quirk/psyonic/proc/give_item_to_holder(obj/item/quirk_item, list/valid_slots, flavour_text = null, default_location = "at your feet", notify_player = TRUE) + if(ispath(quirk_item)) + quirk_item = new quirk_item(get_turf(quirk_holder)) + + var/mob/living/carbon/human/human_holder = quirk_holder + + var/where = human_holder.equip_in_one_of_slots(quirk_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 [quirk_item] [where]. [flavour_text]")) + +/datum/quirk/psyonic/remove() + UnregisterSignal(quirk_holder, COMSIG_MOB_GET_STATUS_TAB_ITEMS) + +// Показывает текущее кол-во псионической энергии +/datum/quirk/psyonic/proc/get_status_tab_item(mob/living/source, list/items) + SIGNAL_HANDLER + + items += "Psyonic School: [school]" + items += "Secondary School: [secondary_school]" + items += "Psyonic Tier: [psyonic_level_string]" + items += "Current psyonic energy: [mana_level]/[max_mana]" + +/datum/quirk/psyonic/process(seconds_per_tick) + if(HAS_TRAIT(quirk_holder, TRAIT_NO_PSYONICS)) // Имплант подавления регена + return + + if(HAS_TRAIT(quirk_holder, TRAIT_MINDSHIELD)) // Womp womp + return + + var/additional_mana = 1 + if(quirk_holder.has_status_effect(/datum/status_effect/drugginess)) // Наркота даёт бафф к генерации маны + additional_mana *= 1.5 + + if(HAS_TRAIT(quirk_holder, TRAIT_PRO_PSYONICS)) // Если есть имплант для увеличения регена маны + additional_mana *= 2 + + if(mana_level <= max_mana) + mana_level += seconds_per_tick * 0.5 * additional_mana + mana_level = clamp(mana_level, 0, max_mana) + +/datum/quirk_constant_data/psyonic_school + associated_typepath = /datum/quirk/psyonic + customization_options = list(/datum/preference/choiced/psyonic_school, /datum/preference/choiced/psyonic_school_secondary) + +/datum/preference/choiced/psyonic_school + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "psyonic_school" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/psyonic_school/create_default_value() + return "Redaction" + +/datum/preference/choiced/psyonic_school/init_possible_values() + return GLOB.psyonic_schools + +/datum/preference/choiced/psyonic_school/is_accessible(datum/preferences/preferences) + . = ..() + if (!.) + return FALSE + + return "Psyonic Abilities" in preferences.all_quirks + +/datum/preference/choiced/psyonic_school/apply_to_human(mob/living/carbon/human/target, value) + return + +/datum/preference/choiced/psyonic_school_secondary + category = PREFERENCE_CATEGORY_MANUALLY_RENDERED + savefile_key = "psyonic_school_secondary" + savefile_identifier = PREFERENCE_CHARACTER + +/datum/preference/choiced/psyonic_school_secondary/create_default_value() + return "Redaction" + +/datum/preference/choiced/psyonic_school_secondary/init_possible_values() + return GLOB.psyonic_schools + +/datum/preference/choiced/psyonic_school_secondary/is_accessible(datum/preferences/preferences) + . = ..() + if (!.) + return FALSE + + return "Psyonic Abilities" in preferences.all_quirks + +/datum/preference/choiced/psyonic_school_secondary/apply_to_human(mob/living/carbon/human/target, value) + return + +#undef LATENT_PSYONIC +#undef OPERANT_PSYONIC +#undef MASTER_PSYONIC +#undef GRANDMASTER_PSYONIC +#undef PARAMOUNT_PSYONIC diff --git a/tff_modular/modules/psyonics/code/coersion.dm b/tff_modular/modules/psyonics/code/coersion.dm new file mode 100644 index 00000000000..445593b74c2 --- /dev/null +++ b/tff_modular/modules/psyonics/code/coersion.dm @@ -0,0 +1,484 @@ +#define IS_HYPNOTIZED(mob) (mob?.mind?.has_antag_datum(/datum/antagonist/hypnotized)) + +/// Школа внушения. 7 спеллов +/// Psyonic assay - скан, является ли человек псиоником +/// Psyonic focus - лечение мозга и псих болезней +/// Psyonic mind read - продвинутое чтение разума(Как обычная ген. мутация, но + работа + воспоминания). Выдаётся только и ТОЛЬКО психологу, если он псионик +/// Psyonic agony - работает как стан дубинка, исчезает после одного удара +/// Psyonic spasm - станит на полсекунды, заставляет выронить всё из рук. Работает дистанционно +/// Psyonic hypnosis - гипнотизирует цель фразой, которую выбрал псионик. ERP IS BAD. DO NOT ERP. +/// P.S. По гипнозу. В оригинале на финиках вообще было порабощение разума. +/// Psyonic blind - временно ослепляет. + +// Прок для проверки носит ли моб шляпку из фольги. Удивительно, но защищает от некоторых спеллов школы внушения) +/mob/living/carbon/human/proc/is_wearing_tinfoil_hat() + if(istype(head, /obj/item/clothing/head/costume/foilhat)) + return TRUE + return FALSE + +// Добавить школу внушения +/mob/living/carbon/human/proc/try_add_coercion_school(tier = 0, additional_school = 0) + if(tier >= 0) + var/datum/action/new_action = new /datum/action/cooldown/spell/touch/psyonic/psyonic_assay(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 1) + var/datum/action/new_action = new /datum/action/cooldown/spell/pointed/psyonic/psyonic_focus(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 2) + if(HAS_MIND_TRAIT(src, TRAIT_MADNESS_IMMUNE)) // A.K.A. станционный психолог + var/datum/action/new_action = new /datum/action/cooldown/spell/touch/psyonic/psyonic_mind_read(src.mind || src, tier, additional_school) + new_action.Grant(src) + var/datum/action/new_action2 = new /datum/action/cooldown/spell/touch/psyonic/psyonic_hypnosis(src.mind || src, tier, additional_school) + new_action2.Grant(src) + if(tier >= 3) + var/datum/action/new_action = new /datum/action/cooldown/spell/touch/psyonic/psyonic_agony(src.mind || src, tier, additional_school) + new_action.Grant(src) + var/datum/action/new_action2 = new /datum/action/cooldown/spell/pointed/psyonic/psyonic_spasm(src.mind || src, tier, additional_school) + new_action2.Grant(src) + if(tier >= 4) // Способность вызывать слепоту на ~15 секунд втихую на расстоянии это боль. + var/datum/action/new_action = new /datum/action/cooldown/spell/pointed/psyonic/psyonic_blind(src.mind || src, tier, additional_school) + new_action.Grant(src) + +// Спелл для чтения разума другого игрока на наличие псионических способностей +/datum/action/cooldown/spell/touch/psyonic/psyonic_assay + name = "Psyonic Assay" + desc = "Check if the target is a psyonic." + button_icon = 'icons/obj/medical/organs/organs.dmi' + button_icon_state = "brain" + cooldown_time = 3 SECONDS + mana_cost = 5 + stamina_cost = 0 + target_msg = "Your get a headache, but it quickly fades." + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to cleanse a patient.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = TRUE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_assay/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(ishuman(victim)) + var/mob/living/carbon/human/human_victim = victim + if(human_victim.can_block_magic(antimagic_flags)) + to_chat(human_victim, span_notice("Psionic nearby tries to check you for psyonic levels.")) + else + to_chat(human_victim, span_warning(target_msg)) + owner.visible_message(span_warning("[owner] presses his thumb onto [victim]s forehead."), + span_notice("You press your thumb onto [victim]s forehead and begin reading them.")) + to_chat(victim, span_danger("[owner] presses a thumb onto your forehead and holds it there. It burns sligthly!")) + if(do_after(mendicant, 6 SECONDS, human_victim, IGNORE_SLOWDOWNS, TRUE)) + read_psyonic_level(human_victim) + drain_mana() + return TRUE + else + return FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_assay/proc/read_psyonic_level(mob/living/carbon/human/patient) + if(issynthetic(patient) && secondary_school != "Psychokinesis") + to_chat(owner, span_notice("I can see... just numbers. No idea how to work with synths.")) + return FALSE + + if(patient.ispsyonic()) + var/datum/quirk/psyonic/target_quirk = patient.get_quirk(/datum/quirk/psyonic) + owner.visible_message(span_notice("[owner] backs off from [patient]."), + span_cyan("Target is a psyonic from the school of [target_quirk.school]. [patient.p_Their()] class is [target_quirk.psyonic_level_string]")) + else + owner.visible_message(span_notice("[owner] backs off from [patient]."), + span_cyan("Target is not a psyonic.")) + +// Лечим мозги и брейнтравмы. +/datum/action/cooldown/spell/pointed/psyonic/psyonic_focus + name = "Psyonic Focus" + desc = "Try to restore patients brain to its natural initial condition, fixing brain damage. Has a chance to heal traumas. Can be cast over distance." + button_icon = 'icons/obj/medical/organs/organs.dmi' + button_icon_state = "brain-smooth" + cooldown_time = 1 SECONDS + mana_cost = 40 + target_msg = "You feel like someone is messing with your brains." + active_msg = "You prepare to heal someones mind..." + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_focus/New(Target) + . = ..() + if(secondary_school == "Redaction") + cast_power += 1 + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_focus/is_valid_target(atom/cast_on) + if(!ishuman(cast_on)) + return FALSE + if(issynthetic(cast_on) && secondary_school != "Psychokinesis") + to_chat(owner, span_notice("I dont know how to work with synths.")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_focus/cast(mob/living/carbon/human/cast_on) + . = ..() + if(cast_on.can_block_magic(antimagic_flags)) + to_chat(cast_on, span_notice("Your mind is being healed by a psyonic nearby.")) + else + to_chat(cast_on, span_warning(target_msg)) + owner.Beam(cast_on, icon_state = "blood_light", time = 5 SECONDS) + owner.visible_message(span_warning("[owner] seems to concentrate on something."), + span_notice("You start concentrating your energy to heal [cast_on]s brains.")) + if(!do_after(owner, 5 SECONDS, cast_on, IGNORE_SLOWDOWNS | IGNORE_TARGET_LOC_CHANGE, TRUE)) + accident_harm(cast_on) + else + fix_brainz(cast_on) + drain_mana() + return TRUE + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_focus/proc/fix_brainz(mob/living/carbon/human/cast_on) + var/b_damage = cast_on.get_organ_loss(ORGAN_SLOT_BRAIN) + if(b_damage > 0) + cast_on.adjustOrganLoss(ORGAN_SLOT_BRAIN, -10 * cast_power) + + var/traumas = cast_on.get_traumas() + if(traumas) + var/datum/brain_trauma/trauma = pick(traumas) + if(trauma.resilience != TRAUMA_RESILIENCE_ABSOLUTE) + cast_on.cure_trauma_type(resilience = trauma.resilience) + cast_on.apply_status_effect(/datum/status_effect/drugginess, 20 SECONDS) + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_focus/proc/accident_harm(mob/living/carbon/human/cast_on) + cast_on.adjustOrganLoss(ORGAN_SLOT_BRAIN, 15 * cast_power, 101) + to_chat(cast_on, span_bolddanger("You head hurts!")) + +// Читаем разум. Выдаёт: последние сейлоги, интент, настоящее имя, воспоминания, намёк на работу, намёк на то, что в антаг_датум что то есть. +/datum/action/cooldown/spell/touch/psyonic/psyonic_mind_read + name = "Psyonic Mind Read" + desc = "Rudely intrude into targets thoughts." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "mindread" + cooldown_time = 3 SECONDS + mana_cost = 40 + stamina_cost = 40 + target_msg = "You feel someone else in your head." + + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to read someones mind.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_mind_read/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(ishuman(victim)) + var/mob/living/carbon/human/human_victim = victim + if(human_victim.is_wearing_tinfoil_hat()) + to_chat(human_victim, span_clockred("Your tinfoil hat vibrates, protecting your brain from some kind of invisible rays!")) + to_chat(owner, span_clockred("As soon as you touch [human_victim]s head, suddnely pictures of your own mind appear! Looks like the tinfoil hat on their head is interfering.")) + drain_mana() + return TRUE + if(human_victim.mind && human_victim.stat != DEAD) + if(human_victim.can_block_magic(antimagic_flags)) + to_chat(human_victim, span_bolddanger("Psionic nearby tries to read your mind!")) + else + to_chat(human_victim, span_warning(target_msg)) + owner.visible_message(span_warning("[owner] presses his thumb onto [victim]s forehead."), + span_notice("You press your thumb onto [victim]s forehead and begin reading them.")) + to_chat(victim, span_danger("[owner] presses a thumb onto your forehead and holds it there. It burns sligthly!")) + if(do_after(mendicant, 10 SECONDS, human_victim, IGNORE_SLOWDOWNS, TRUE)) + read_mind(human_victim) + drain_mana() + return TRUE + else + return FALSE + else + return FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_mind_read/proc/read_mind(mob/living/carbon/human/patient) + if(patient.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0)) + to_chat(owner, span_warning("As you reach into [patient]'s mind, \ + you are stopped by a mental blockage. It seems you've been foiled.")) + return + + if(issynthetic(patient) && secondary_school != "Psychokinesis") + to_chat(owner, span_notice("I dont know how to work with synths. It's just zeros and ones. How am I supposed to get info out of this metal bucket?")) + return + + var/text_to_show = "" + + var/list/recent_speech = patient.copy_recent_speech(copy_amount = 10) + if(length(recent_speech)) + text_to_show += span_boldnotice("You catch some drifting memories of their past conversations...") + "
" + for(var/spoken_memory in recent_speech) + text_to_show += span_notice("[spoken_memory]") + "
" + + text_to_show += span_notice("You find that their intent is to [patient.combat_mode ? "harm" : "help"]...") + "
" + text_to_show += span_notice("You uncover that [patient.p_their()] true identity is [patient.mind.name].") + "
" + if(cast_power >= 3) + text_to_show += span_notice("You can vaguely read their memories: ") + examine_block(span_italics(get_memories(patient))) + text_to_show += span_notice("You try to read their job: ") + examine_block(span_italics(get_job_fluff(patient))) + if(patient.mind.enslaved_to || IS_HYPNOTIZED(patient)) + text_to_show += span_boldnotice("[patient.p_Their()] will is not free.") + "
" + var/datum/mind/mind_to_read = patient.mind + if(prob(20 * cast_power) && mind_to_read.antag_datums) + if(IS_WIZARD(patient)) + text_to_show += span_notice("You can feel strong potential pulsing in this individual.") + "
" + else if(IS_HERETIC(patient)) + text_to_show += span_notice("Reality bends around you and goes back to normal, as you try to read [patient.p_their()] mind.") + "
" + var/mob/living/carbon/human/human_owner = owner + human_owner.add_mood_event("gates_of_mansus", /datum/mood_event/gates_of_mansus) + else if(IS_CULTIST(patient)) + text_to_show += span_red("Your mind is assaulted with torrents of blood and gore, as you try to dig deeper.") + "
" + else // Там очень много ролей, в том числе не антажных, а мага, еретика и культиста я думаю и без этой способности найти легко. Тем более мы читаем воспоминания, что более имбово + text_to_show += span_notice("You also can feel something hidden within [patient.p_their()] mind, but it's not readable.") + "
" + + to_chat(owner, examine_block(span_infoplain(text_to_show))) + +// Возвращает размытый текст о профессии +/datum/action/cooldown/spell/touch/psyonic/psyonic_mind_read/proc/get_job_fluff(mob/living/carbon/human/patient) + var/datum/mind/mind_to_read = patient.mind + var/datum/job/patient_job = mind_to_read.assigned_role + var/text_to_return = "" + if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_SECURITY) + text_to_return += "This persons job involves beating up mimes and clowns." + "
" + else if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_CENTRAL_COMMAND) + text_to_return += "This persons is a greatest authority on this station." + "
" + else if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_CAPTAIN) + text_to_return += "This persons is likely to have megalomania." + "
" + else if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_COMMAND) + text_to_return += "This persons calling is commanding others." + "
" + else if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_SERVICE) + text_to_return += "This persons labor is about servicing others." + "
" + else if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_CARGO) + text_to_return += "This person works physically a lot." + "
" + else if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_ENGINEERING) + text_to_return += "This person keeps station alive." + "
" + else if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_SCIENCE) + text_to_return += "This person is an egghead." + "
" + else if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_MEDICAL) + text_to_return += "This person is accustomed with wounds, blood and etc." + "
" + else if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_SILICON) + text_to_return += "This is en etenral mankinds servant." + "
" + else if(patient_job.departments_bitflags & DEPARTMENT_BITFLAG_ASSISTANT) + text_to_return += "This persons mind reeks of freedom." + "
" + else + text_to_return += "This person is truly free. They are not obligated with any duties." + "
" + + return span_notice(text_to_return) + +// Возвращает воспоминания разума. Имба против таторов, так как там хранится код от аплинка. А ну и банковский айди. +/datum/action/cooldown/spell/touch/psyonic/psyonic_mind_read/proc/get_memories(mob/living/carbon/human/patient) + var/datum/mind/mind_to_read = patient.mind + if(mind_to_read) + var/itogo_text = "" + for(var/key in mind_to_read.memories) + var/datum/memory/mem = mind_to_read.memories[key] + itogo_text += mem.name + "
" + if(itogo_text == "") + itogo_text = "[patient.p_Their()] head is empty." + return itogo_text + else + return "I cant read [patient.p_their()] memories. Maybe there are none?" + "
" + +// Stun batong на минималках. Исчезает после одного удара +/datum/action/cooldown/spell/touch/psyonic/psyonic_agony + name = "Psyonic Agony" + desc = "Deals pain." + button_icon = 'icons/obj/weapons/baton.dmi' + button_icon_state = "stunbaton_active" + cooldown_time = 0.5 SECONDS + mana_cost = 40 + stamina_cost = 0 + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to deal pain.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = TRUE // Упс :) + +/datum/action/cooldown/spell/touch/psyonic/psyonic_agony/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(ishuman(victim)) + var/mob/living/carbon/human/human_victim = victim + if(human_victim.is_wearing_tinfoil_hat()) + to_chat(human_victim, span_clockred("Your tinfoil hat vibrates, protecting your brain from some kind of invisible rays!")) + to_chat(owner, span_clockred("As soon as you touch [human_victim], your own body hurts as hell! Looks like the tinfoil hat on their head is interfering.")) + psyonic_attack(owner) + drain_mana() + return TRUE + if(human_victim.can_block_magic(antimagic_flags)) + to_chat(human_victim, span_notice("Psionic nearby tries to attack you, but fails.")) + to_chat(owner, span_notice("You can't attack them. They have some kind of protection.")) + return FALSE + if(issynthetic(human_victim) && secondary_school != "Psychokinesis") + human_victim.visible_message(span_danger("[owner] slaps [human_victim] with his hand. Nothing happens. Wow!"), + span_warning("You slap [human_victim], but nothing happens. You cannot transfer your energy through metal."), + blind_message = span_hear("You hear a slap.")) + return FALSE + else + to_chat(human_victim, span_warning("Pain floods your body as soon as [owner] touches you!.")) + psyonic_attack(human_victim) + log_combat(owner, human_victim, "psyonically stunned") + drain_mana() + return TRUE + else + return FALSE + +// Прок удара +/datum/action/cooldown/spell/touch/psyonic/psyonic_agony/proc/psyonic_attack(mob/living/carbon/human/patient) + patient.apply_damage(35, STAMINA) // Стандартный стан батонг + addtimer(CALLBACK(src, PROC_REF(apply_stun_effect), patient), 2 SECONDS) + +/datum/action/cooldown/spell/touch/psyonic/psyonic_agony/proc/apply_stun_effect(mob/living/carbon/human/patient) + patient.visible_message(span_danger("[owner] slaps [patient] with his hand, sparks flying out of it!"), + span_warning("You slap [patient], stunning him."), + blind_message = span_hear("You hear a slap and an electrical crackling afterwards.")) + var/trait_check = HAS_TRAIT(patient, TRAIT_BATON_RESISTANCE) //var since we check it in out to_chat as well as determine stun duration + if(!patient.IsKnockdown()) + to_chat(patient, span_warning("Your muscles seize, making you collapse[trait_check ? ", but your body quickly recovers..." : "!"]")) + + if(!trait_check) + patient.Knockdown((cast_power/2) SECONDS) + +// Станит на непродолжительный срок(~0.5 сек) и заставляет выкинуть вещи из рук +/datum/action/cooldown/spell/pointed/psyonic/psyonic_spasm + name = "Psyonic Spasm" + desc = "Activate neurons in victims mucles, briefly stunning them and forcing to drop everything in their hands. Can be cast over distance. Silent." + button_icon = 'tff_modular/modules/psyonics/icons/actions.dmi' + button_icon_state = "spasm" + cooldown_time = 1 SECONDS + mana_cost = 40 + target_msg = "Your muscles spasm!" + active_msg = "You prepare to stun a target..." + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_spasm/New(Target) + . = ..() + if(secondary_school == "Energistics") + cast_power += 1 + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_spasm/is_valid_target(atom/cast_on) + if(!ishuman(cast_on)) + return FALSE + if(issynthetic(cast_on) && secondary_school != "Psychokinesis") + to_chat(owner, span_notice("I dont know how to work with synths.")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_spasm/cast(mob/living/carbon/human/cast_on) + . = ..() + if(cast_on.is_wearing_tinfoil_hat()) + to_chat(cast_on, span_clockred("Your tinfoil hat vibrates, protecting your brain from some kind of invisible rays!")) + to_chat(owner, span_clockred("As soon as you try to spasm [cast_on], your own body twitches! Looks like the tinfoil hat on their head is interfering.")) + drain_mana() + stun(owner) + return TRUE + if(cast_on.can_block_magic(antimagic_flags)) + to_chat(cast_on, span_warning("Your body is assaulted with psyonic energy!")) + else + to_chat(cast_on, span_warning(target_msg)) + log_combat(owner, cast_on, "psyonically spasmed") + stun(cast_on) + drain_mana() + return TRUE + +// Сам стан +/datum/action/cooldown/spell/pointed/psyonic/psyonic_spasm/proc/stun(mob/living/carbon/human/cast_on) + cast_on.Stun(0.2 SECONDS * cast_power) + +/** + * Гипнотизирует игрока заданной фразой и даёт брейнтравму с ней + * + * Условия: + * * 30 секунд ожидания + * * в агрограбе + * * без движения жертвы или псионика + */ +/datum/action/cooldown/spell/touch/psyonic/psyonic_hypnosis + name = "Psyonic Hypnosis" + desc = "Implant a looping pattern into victims head." + button_icon = 'tff_modular/modules/psyonics/icons/actions.dmi' + button_icon_state = "hypno" + cooldown_time = 10 SECONDS + + mana_cost = 25 // Стоит немного + stamina_cost = 50 // Но выматывет + target_msg = "Your get a headache." + + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to hypnotize a victim.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = FALSE // No + +/datum/action/cooldown/spell/touch/psyonic/psyonic_hypnosis/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(ishuman(victim) && mendicant.grab_state == GRAB_AGGRESSIVE && mendicant.pulling == victim) + var/mob/living/carbon/human/human_victim = victim + if(human_victim.is_wearing_tinfoil_hat()) + to_chat(human_victim, span_clockred("Your tinfoil hat vibrates, protecting your brain from some kind of invisible rays!")) + to_chat(owner, span_clockred("As soon as you touch [human_victim]s head, you feel incredibly sleepy! Looks like the tinfoil hat on their head is interfering.")) + drain_mana() + addtimer(CALLBACK(owner, TYPE_PROC_REF(/mob/living, Stun), 60, TRUE, TRUE), 15) + return TRUE + if(HAS_MIND_TRAIT(human_victim, TRAIT_UNCONVERTABLE)) // Не работает на людей с МЩ + to_chat(owner, span_warning("Victims mind is too strong for you to penetrate.")) + return FALSE + if(human_victim.can_block_magic(antimagic_flags)) + to_chat(human_victim, span_boldwarning("Psionic nearby tries to hypnotize you!")) + else + to_chat(human_victim, span_warning(target_msg)) + owner.visible_message(span_warning("[owner] firmly grabs [victim]s and begins creepely staring onto them."), + span_notice("You grab [victim]s head and begin implanting a thought into them.")) + var/player_input = tgui_input_text(mendicant, "Hypnophrase", "Input the hypnophrase", max_length = MAX_MESSAGE_LEN) + if(!player_input) + return FALSE + if(do_after(mendicant, (10 - cast_power) SECONDS, human_victim, IGNORE_SLOWDOWNS, TRUE)) + hypnotize(human_victim, player_input) + else + to_chat(owner, span_warning("You failed to hypnotize the victim.")) + drain_mana() + return TRUE + else + to_chat(owner, span_notice("You need to grab a human in aggressive grab to hypnotize them.")) + return FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_hypnosis/proc/hypnotize(mob/living/carbon/human/patient, hypnophrase) + patient.cure_trauma_type(/datum/brain_trauma/hypnosis, TRAUMA_RESILIENCE_SURGERY) + + owner.log_message("hypnotised [key_name(patient)] with the phrase '[hypnophrase]'", LOG_ATTACK, color="red") + + patient.log_message("has been hypnotised by the phrase '[hypnophrase]' spoken by [key_name(owner)]", LOG_VICTIM, color="orange", log_globally = FALSE) + + addtimer(CALLBACK(patient, TYPE_PROC_REF(/mob/living/carbon, gain_trauma), /datum/brain_trauma/hypnosis, TRAUMA_RESILIENCE_SURGERY, hypnophrase), 1 SECONDS) + addtimer(CALLBACK(patient, TYPE_PROC_REF(/mob/living, Stun), 60, TRUE, TRUE), 15) + +// Ослепляет цель на дистанции на ~15 секунд. Способность максимального уровня +/datum/action/cooldown/spell/pointed/psyonic/psyonic_blind + name = "Psyonic Blind" + desc = "Interfere with the way neuron signals are transmitted in the victims eyes." + button_icon_state = "blind" + ranged_mousepointer = 'icons/effects/mouse_pointers/blind_target.dmi' + cooldown_time = 1 SECONDS + mana_cost = 60 + target_msg = "You eyes hurt!" + active_msg = "You prepare to blind a target..." + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_blind/is_valid_target(atom/cast_on) + if(!ishuman(cast_on)) + return FALSE + else + var/mob/living/carbon/human/victim = cast_on + if(victim.is_blind()) + to_chat(owner, span_notice("[victim] is already blind.")) + return FALSE + if(issynthetic(cast_on) && secondary_school != "Psychokinesis") + to_chat(owner, span_notice("I dont know how to work with synths.")) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_blind/cast(mob/living/carbon/human/cast_on) + . = ..() + if(cast_on.is_wearing_tinfoil_hat()) + to_chat(cast_on, span_clockred("Your tinfoil hat vibrates, protecting your brain from some kind of invisible rays!")) + to_chat(owner, span_clockred("As soon as you try to blind [cast_on], your own eyes close on its own! Looks like the tinfoil hat on their head is interfering.")) + drain_mana() + blind(owner) + return TRUE + if(cast_on.can_block_magic(antimagic_flags)) + to_chat(cast_on, span_warning("Your eyes are burned with psyonic energy!")) + else + to_chat(cast_on, span_warning(target_msg)) + log_combat(owner, cast_on, "psyonically blinded") + blind(cast_on) + drain_mana() + return TRUE + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_blind/proc/blind(mob/living/carbon/human/cast_on) + cast_on.adjust_temp_blindness( (10 + cast_power * 2) SECONDS) + +#undef IS_HYPNOTIZED diff --git a/tff_modular/modules/psyonics/code/cyberimp.dm b/tff_modular/modules/psyonics/code/cyberimp.dm new file mode 100644 index 00000000000..130ade04a83 --- /dev/null +++ b/tff_modular/modules/psyonics/code/cyberimp.dm @@ -0,0 +1,40 @@ +#define ORGAN_SLOT_BRAIN_PSYONIC "brain_psyonic" + +// Не позволяет мане регенерироваться +/obj/item/organ/internal/cyberimp/brain/anti_psyonic + name = "Psyonic Amplifier Model N" + desc = "This implant will prohibit psyonics from regenereting their energy." + icon_state = "brain_implant_rebooter" + slot = ORGAN_SLOT_BRAIN_PSYONIC + +/obj/item/organ/internal/cyberimp/brain/anti_psyonic/on_mob_insert(mob/living/carbon/organ_owner, special, movement_flags) + . = ..() + ADD_TRAIT(organ_owner, TRAIT_NO_PSYONICS, IMPLANT_TRAIT) + +/obj/item/organ/internal/cyberimp/brain/anti_psyonic/on_mob_remove(mob/living/carbon/organ_owner, special) + . = ..() + REMOVE_TRAIT(organ_owner, TRAIT_NO_PSYONICS, IMPLANT_TRAIT) + +// Увеличивает реген маны в 2 раза +/obj/item/organ/internal/cyberimp/brain/pro_psyonic + name = "Psyonic Amplifier Model A" + desc = "This implant will boost psyonics energy regeneration." + icon_state = "brain_implant_rebooter" + slot = ORGAN_SLOT_BRAIN_PSYONIC + +/obj/item/organ/internal/cyberimp/brain/pro_psyonic/on_mob_insert(mob/living/carbon/organ_owner, special, movement_flags) + . = ..() + ADD_TRAIT(organ_owner, TRAIT_PRO_PSYONICS, IMPLANT_TRAIT) + +/obj/item/organ/internal/cyberimp/brain/pro_psyonic/on_mob_remove(mob/living/carbon/organ_owner, special) + . = ..() + REMOVE_TRAIT(organ_owner, TRAIT_PRO_PSYONICS, IMPLANT_TRAIT) + +/datum/supply_pack/medical/psyonic_implants + name = "Psyonic Implants" + desc = "A crate containing two experimental psyonic implants, which work ONLY on psyonic users. No warranty." + cost = CARGO_CRATE_VALUE * 5 + contains = list(/obj/item/organ/internal/cyberimp/brain/anti_psyonic = 1, + /obj/item/organ/internal/cyberimp/brain/pro_psyonic = 1) + crate_name = "Psyonic implant crate" + discountable = SUPPLY_PACK_RARE_DISCOUNTABLE diff --git a/tff_modular/modules/psyonics/code/documents.dm b/tff_modular/modules/psyonics/code/documents.dm new file mode 100644 index 00000000000..81a6386c6ac --- /dev/null +++ b/tff_modular/modules/psyonics/code/documents.dm @@ -0,0 +1,75 @@ +/obj/item/card/psyonic_license + name = "psyonic license" + desc = "An official license given to psyonic users by the NanoTrasen Psyonics and Eugenics Division itself." + icon = 'tff_modular/modules/psyonics/icons/card.dmi' + icon_state = "card_psy" + inhand_icon_state = "card-id" + lefthand_file = 'icons/mob/inhands/equipment/idcards_lefthand.dmi' + righthand_file = 'icons/mob/inhands/equipment/idcards_righthand.dmi' + w_class = WEIGHT_CLASS_TINY + pickup_sound = 'sound/items/handling/id_card/id_card_pickup1.ogg' + drop_sound = 'sound/items/handling/id_card/id_card_drop1.ogg' + sound_vary = TRUE + resistance_flags = FIRE_PROOF + var/datum/psyonic_licence_datum/owner_info + +/obj/item/card/psyonic_license/New(mob/living/carbon/human/owner) + . = ..() + owner_info = new(owner) + +/obj/item/card/psyonic_license/ui_interact(mob/user, datum/tgui/ui) + if(!owner_info) + balloon_alert(user, "The card isn't bound to anyone!") + return + + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "PsyonicLicense") + ui.set_autoupdate(FALSE) + ui.open() + +/obj/item/card/psyonic_license/ui_static_data(mob/user) + var/list/data = list() + + data["primary_school"] = owner_info.primary_school + data["secondary_school"] = owner_info.secondary_school + data["psyonic_level"] = owner_info.psyonic_level + data["owner_name"] = owner_info.owner_name + data["owner_age"] = owner_info.owner_age + data["owner_preview"] = owner_info.owner_preview + data["owner_species"] = owner_info.owner_species + + return data + +/datum/psyonic_licence_datum + var/datum/weakref/original_owner + var/owner_name + var/owner_age + var/psyonic_level + var/primary_school + var/secondary_school + var/owner_species + var/icon/owner_preview + +/datum/psyonic_licence_datum/New(mob/living/carbon/human/human_owner) + . = ..() + original_owner = WEAKREF(human_owner) + if(original_owner && original_owner.resolve()) + var/mob/living/carbon/human/owner = original_owner.resolve() + if(!istype(owner, /mob/living/carbon/human)) + return + if(!owner.ispsyonic()) + return + var/datum/quirk/psyonic/quirk_holder = owner.get_quirk(/datum/quirk/psyonic) + psyonic_level = quirk_holder.psyonic_level_string + primary_school = quirk_holder.school + secondary_school = quirk_holder.secondary_school + owner_name = owner.real_name + owner_age = owner.age + + if (!owner.dna.species.lore_protected && owner.dna.features["custom_species"]) + owner_species = "[owner.dna.features["custom_species"]]" + else + owner_species = "[owner.dna.species.name]" + + owner_preview = icon2base64(getFlatIcon(owner, SOUTH, no_anim = TRUE)) diff --git a/tff_modular/modules/psyonics/code/energistics.dm b/tff_modular/modules/psyonics/code/energistics.dm new file mode 100644 index 00000000000..37b47cd7ed2 --- /dev/null +++ b/tff_modular/modules/psyonics/code/energistics.dm @@ -0,0 +1,203 @@ +/// Школа энергетики. 6 спеллов +/// Spark - создаёт искры в указанном месте +/// Discharge - разряжает АПЦ/Батарейку. Даёт ману в зависимости от кол-ва энергии +/// Laser - стрелеят концентрированным пучком фотонов, пусть и не самым сильным. +/// Distrupt - создаёт ЭМИ с небольшим радиусом. +/// Elecrocute - добавляет мутацию shock touch +/// Freeze - заковывает моба в лёд на небольшой промежуток. + +// Добавить школу внушения +/mob/living/carbon/human/proc/try_add_energistics_school(tier = 0, additional_school = 0) + if(tier >= 0) + var/datum/action/new_action = new /datum/action/cooldown/spell/touch/psyonic/psyonic_discharge(src.mind || src, tier, additional_school) + new_action.Grant(src) + var/datum/action/new_action2 = new /datum/action/cooldown/spell/pointed/psyonic/psyonic_spark(src.mind || src, tier, additional_school) + new_action2.Grant(src) + if(tier >= 1) + var/datum/action/new_action = new /datum/action/cooldown/spell/basic_projectile/psyonic_laser(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 2) + var/datum/action/new_action = new /datum/action/cooldown/spell/touch/psyonic/psyonic_emp(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 3) + var/datum/action/new_action = new /datum/action/cooldown/spell/psyonic/psionic_electrocute(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 4) + var/datum/action/new_action = new /datum/action/cooldown/spell/pointed/projectile/psyonic/psyonic_freeze(src.mind || src) + new_action.Grant(src) + +// Разрядка АПЦ или батареек в обмен на ману +/datum/action/cooldown/spell/touch/psyonic/psyonic_discharge + name = "Psyonic Discharge" + desc = "Try to discharge battery and convert electricity into raw psyonic energy." + button_icon = 'modular_nova/modules/aesthetics/cells/cell.dmi' + button_icon_state = "icell" + cooldown_time = 30 SECONDS + mana_cost = 0 + stamina_cost = 15 + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to discharge an energy source.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_discharge/is_valid_target(atom/cast_on) + return isatom(cast_on) + +/datum/action/cooldown/spell/touch/psyonic/psyonic_discharge/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(HAS_TRAIT(mendicant, TRAIT_MINDSHIELD)) // Womp womp + to_chat(mendicant, span_warning("As soon as you touch [victim], your energy dissipates without a trace. Mindshield implant messes up your concentration.")) + return FALSE + if(istype(victim, /obj/item/stock_parts/power_store) || istype(victim, /obj/machinery/power/apc)) + owner.visible_message(span_warning("[owner] presses his hands against [victim]."), + span_notice("You press your hands against [victim]."), + blind_message = span_hear("You hear electrical crackling.")) + if(do_after(mendicant, 2.5 SECONDS, victim, IGNORE_SLOWDOWNS, TRUE)) + var/datum/quirk/psyonic/quirk_holder = mendicant.get_quirk(/datum/quirk/psyonic) + if(!quirk_holder) + return FALSE + if(istype(victim, /obj/item/stock_parts/power_store)) + var/obj/item/stock_parts/power_store/batt = victim + var/to_charge = (batt.charge / STANDARD_CELL_VALUE) + batt.use(batt.charge(), TRUE) + quirk_holder.mana_level = clamp(quirk_holder.mana_level + to_charge, 0, quirk_holder.max_mana) + else if(istype(victim, /obj/machinery/power/apc)) + var/obj/machinery/power/apc/target_apc = victim + var/obj/item/stock_parts/power_store/batt = target_apc.cell + if(!batt) + to_chat(owner, span_notice("There is no battery in this APC.")) + return FALSE + var/to_charge = (batt.charge() / (STANDARD_BATTERY_CHARGE/10)) + batt.use(batt.charge(), TRUE) + quirk_holder.mana_level = clamp(quirk_holder.mana_level + to_charge, 0, quirk_holder.max_mana) + else + to_chat(owner, span_notice("You've failed to discharge energy.")) + return TRUE + else + return FALSE + +// Создаёт искры в указанном месте +/datum/action/cooldown/spell/pointed/psyonic/psyonic_spark + name = "Psyonic Spark" + desc = "Cause some sparks to appear at a place of your choice." + button_icon = 'icons/effects/effects.dmi' + button_icon_state = "blessed" + cooldown_time = 1 SECONDS + mana_cost = 10 + active_msg = "You prepare to create sparks..." + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_spark/is_valid_target(atom/cast_on) + if(!isturf(cast_on)) + return FALSE + return TRUE + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_spark/cast(turf/cast_on) + . = ..() + var/mob/living/carbon/human/caster = owner + caster.emote_snap() + var/datum/effect_system/spark_spread/sparks = new + sparks.set_up(5, 1, cast_on) + sparks.attach(cast_on) + sparks.start() + drain_mana() + return TRUE + +// Стреляет по направлению куклы псионика фотонной пушкой. Считайте аналог флешки +/datum/action/cooldown/spell/basic_projectile/psyonic_laser + name = "Photon Laser" + desc = "Channels psyonic energy into a weak concentrated photon laser." + button_icon = 'icons/obj/weapons/guns/projectiles.dmi' + button_icon_state = "solarflare" + cooldown_time = 0 SECONDS + spell_requirements = NONE + mana_cost = 10 + projectile_type = /obj/projectile/energy/photon + +/datum/action/cooldown/spell/basic_projectile/psyonic_laser/cast(atom/cast_on) + var/mob/living/carbon/human/caster = owner + var/datum/quirk/psyonic/quirk_holder = caster.get_quirk(/datum/quirk/psyonic) + if(!(quirk_holder && (quirk_holder.mana_level - mana_cost) >= 0)) + return FALSE + else + quirk_holder.mana_level -= mana_cost + ..() + +// Создаёт ЕМП в месте удара руки +/datum/action/cooldown/spell/touch/psyonic/psyonic_emp + name = "Psyonic EMP" + desc = "Try to cause a small local EMP." + button_icon = 'icons/obj/weapons/grenade.dmi' + button_icon_state = "emp" + cooldown_time = 15 SECONDS + mana_cost = 40 + stamina_cost = 40 + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to cause an EMP.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = TRUE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_emp/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(isatom(victim)) + empulse(victim, 1, cast_power/2) + drain_mana() + return TRUE + else + return FALSE + +// Даёт мутацию Shock Touch +/datum/action/cooldown/spell/psyonic/psionic_electrocute + name = "Psyonic Shock Touch" + desc = "Force yourself to recieve shock touch mutation." + cooldown_time = 60 SECONDS + mana_cost = 60 + stamina_cost = 60 + +/datum/action/cooldown/spell/psyonic/psionic_electrocute/is_valid_target(atom/cast_on) + return !issynthetic(cast_on) + +/datum/action/cooldown/spell/psyonic/psionic_electrocute/cast(mob/living/cast_on) + . = ..() + if(!ishuman(cast_on)) + return FALSE + var/mob/living/carbon/human/to_mutate = cast_on + if(!to_mutate.can_mutate()) + return FALSE + to_mutate.dna.add_mutation(/datum/mutation/human/shock, MUT_OTHER) + drain_mana() + return TRUE + +// Стреляет снарядом вотчера, замораживая жертву. Требует почти максимально возможный запас маны +/datum/action/cooldown/spell/pointed/projectile/psyonic/psyonic_freeze + name = "Psyonic Freeze" + desc = "Fire freezing shark at a target, encasing them in an ice prison." + button_icon = 'icons/effects/freeze.dmi' + button_icon_state = "ice_cube" + cooldown_time = 1 SECONDS + mana_cost = 80 + cast_range = 9 + active_msg = "You prepare to fire ice shard..." + deactive_msg = "You relax." + projectile_type = /obj/projectile/temp/watcher/psyonic_freeze + +/datum/action/cooldown/spell/pointed/projectile/psyonic/psyonic_freeze/is_valid_target(atom/cast_on) + if(!isliving(cast_on)) + return FALSE + return TRUE + +/datum/action/cooldown/spell/pointed/projectile/psyonic/psyonic_freeze/cast(mob/living/cast_on) + drain_mana() + . = ..() + return TRUE + +// Вывел в отдельный тип, потому что в оригинальном ice_wing снаряде видимо баг(?) и он не замораживает, хотя должен. +/obj/projectile/temp/watcher/psyonic_freeze + name = "freezing blast" + damage = 0 // Нет дамага, вместо этого замораживает + +/obj/projectile/temp/watcher/psyonic_freeze/apply_status(mob/living/target) + if(HAS_TRAIT(target, TRAIT_RESISTCOLD)) // Вот тут у ice_wing лишний ! + return + target.apply_status_effect(/datum/status_effect/freon/watcher/psyonic_freeze) + +/datum/status_effect/freon/watcher/psyonic_freeze + duration = 4 // 4 секунды вместо 8 + can_melt = TRUE diff --git a/tff_modular/modules/psyonics/code/psychokinesis.dm b/tff_modular/modules/psyonics/code/psychokinesis.dm new file mode 100644 index 00000000000..802aa52f9eb --- /dev/null +++ b/tff_modular/modules/psyonics/code/psychokinesis.dm @@ -0,0 +1,293 @@ +/// Школа психокинетики +/// Имеет 6 спеллов. +/// Psi lighter - создаёт миниатюрный огонёк на кончиках пальцев. Работает как зажигалка. +/// Psi blade - создаёт в руке пси-клинок. Урон увеличивается в зависимости от уровня. +/// Psi tool - создаёт в руке универсальный инструмент. +/// Tinker - чинит integrity чего бы то ни было. +/// Psyforce - даёт "клешни жизни" для вскрытия дверей +/// Telekinesis - даёт мутацию телекинеза. + +// Добавляет школу психокинетики +/mob/living/carbon/human/proc/try_add_psychokinesis_school(tier = 0, additional_school = 0) + if(tier >= 0) + var/datum/action/new_action = new /datum/action/cooldown/spell/conjure_item/psyonic/psilighter(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 1) + var/datum/action/new_action = new /datum/action/cooldown/spell/conjure_item/psyonic/psiblade(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 2) + var/datum/action/new_action = new /datum/action/cooldown/spell/conjure_item/psyonic/psitool(src.mind || src, tier, additional_school) + new_action.Grant(src) + var/datum/action/new_action2 = new /datum/action/cooldown/spell/touch/psyonic/psyonic_tinker(src.mind || src, tier, additional_school) + new_action2.Grant(src) + if(tier >= 3) + var/datum/action/new_action = new /datum/action/cooldown/spell/touch/psyonic/psyonic_force(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 4) + var/datum/action/new_action = new /datum/action/cooldown/spell/psyonic/psionic_telekinesis(src.mind || src, tier, additional_school) + new_action.Grant(src) + +// Спавнит зажигалку в руке. Очень полезно +/datum/action/cooldown/spell/conjure_item/psyonic/psilighter + name = "Psi lighter" + desc = "Concentrates psyonic energy to create a small flame in your hand." + button_icon = 'icons/obj/cigarettes.dmi' + button_icon_state = "match_lit" + cooldown_time = 1.5 SECONDS + item_type = /obj/item/psyonic_fire + mana_cost = 5 + stamina_cost = 0 + +// Спавнит пси-клинок в руке. Сила зависит от уровня псионика +/datum/action/cooldown/spell/conjure_item/psyonic/psiblade + name = "Psi blade" + desc = "Concentrates psyonic energy to create a sharp blade in your hand." + button_icon = 'icons/obj/weapons/transforming_energy.dmi' + button_icon_state = "blade" + cooldown_time = 1.5 SECONDS + item_type = /obj/item/melee/psyonic_blade + mana_cost = 40 + stamina_cost = 0 + +// Спавнит омни инструмент в руке псионика. Аналог абдукторского +/datum/action/cooldown/spell/conjure_item/psyonic/psitool + name = "Psi tool" + desc = "Concentrates psyonic energy to create a universal tool." + button_icon = 'icons/obj/antags/abductor.dmi' + button_icon_state = "omnitool" + cooldown_time = 1.5 SECONDS + item_type = /obj/item/psyonic_omnitool + mana_cost = 30 + stamina_cost = 0 + +/datum/action/cooldown/spell/conjure_item/psyonic/psiblade/New(Target) + . = ..() + if(secondary_school == "Psychokinesis") + cast_power += 1 + +/datum/action/cooldown/spell/conjure_item/psyonic/psiblade/make_item(atom/caster) + var/obj/item/made_item = new item_type(caster.loc, cast_power) + LAZYADD(item_refs, WEAKREF(made_item)) + var/mob/living/carbon/human/caster_pawn = owner + caster_pawn.emote_snap() + return made_item + +// Аналог клешней жизни +/datum/action/cooldown/spell/touch/psyonic/psyonic_force + name = "Psyonic Force" + desc = "Concentrates psyonic energy to force a door open." + button_icon = 'icons/mob/actions/actions_spells.dmi' + button_icon_state = "knock" + cooldown_time = 3 SECONDS + mana_cost = 50 + stamina_cost = 50 + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to force a door open.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_force/is_valid_target(atom/cast_on) + return istype(cast_on, /obj/machinery/door/airlock) + +/datum/action/cooldown/spell/touch/psyonic/psyonic_force/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(isatom(victim)) + if(istype(victim, /obj/machinery/door/airlock)) + var/obj/machinery/door/airlock/door_to_force = victim + owner.visible_message(span_warning("[owner] targets their hands at [victim], like they are some kind of jedi."), + span_notice("You psyonically grab [victim], trying to force it open.")) + if(do_after(mendicant, 5 SECONDS, victim, IGNORE_SLOWDOWNS, TRUE)) + force_door_open(door_to_force, mendicant) + drain_mana() + return TRUE + else + return FALSE + else + return FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_force/proc/force_door_open(obj/machinery/door/airlock/door_to_force, mob/living/carbon/user) + if(door_to_force.seal) + to_chat(user, span_warning("Remove the seal first!")) + return + if(door_to_force.locked) + to_chat(user, span_warning("The airlock's bolts prevent it from being forced!")) + return + if(door_to_force.welded) + to_chat(user, span_warning("It's welded, it won't budge!")) + return + if(door_to_force.hasPower()) + if(!door_to_force.density) + return + if(!door_to_force.prying_so_hard) + playsound(src, 'sound/machines/airlock/airlock_alien_prying.ogg', 100, TRUE) + door_to_force.prying_so_hard = TRUE + door_to_force.open(BYPASS_DOOR_CHECKS) + door_to_force.take_damage(25, BRUTE, 0, 0) + if(door_to_force.density && !door_to_force.open(BYPASS_DOOR_CHECKS)) + to_chat(user, span_warning("Despite your attempts, [src] refuses to open.")) + door_to_force.prying_so_hard = FALSE + return + +// Даёт мутацию телекинеза +/datum/action/cooldown/spell/psyonic/psionic_telekinesis + name = "Telekinesis" + desc = "Force yourself to recieve telekinesis mutation." + cooldown_time = 60 SECONDS + mana_cost = 80 + stamina_cost = 80 + +/datum/action/cooldown/spell/psyonic/psionic_telekinesis/is_valid_target(atom/cast_on) + return !issynthetic(cast_on) + +/datum/action/cooldown/spell/psyonic/psionic_telekinesis/cast(mob/living/cast_on) + . = ..() + if(!ishuman(cast_on)) + return FALSE + var/mob/living/carbon/human/to_mutate = cast_on + if(!to_mutate.can_mutate()) + return FALSE + to_mutate.dna.add_mutation(/datum/mutation/human/telekinesis, MUT_OTHER) + drain_mana() + +// Восстанавливает Integrity атома. Позволяет чинить многие нечинимые иными способами вещи +/datum/action/cooldown/spell/touch/psyonic/psyonic_tinker + name = "Psyonic Tinker" + desc = "Restore somethings condition to its normal state." + button_icon = 'icons/obj/tools.dmi' + button_icon_state = "wrench" + cooldown_time = 3 SECONDS + mana_cost = 40 + stamina_cost = 50 + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to tinker.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_tinker/is_valid_target(atom/cast_on) + return cast_on.uses_integrity + +/datum/action/cooldown/spell/touch/psyonic/psyonic_tinker/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(isatom(victim)) + var/atom/to_fix = victim + if((to_fix.get_integrity() >= to_fix.max_integrity) || !to_fix.uses_integrity) + return FALSE + owner.visible_message(span_warning("[owner] presses his hands onto [victim]."), + span_notice("You grab [victim], trying to repair it.")) + if(do_after(mendicant, 6 SECONDS, victim, IGNORE_SLOWDOWNS, TRUE)) + to_fix.update_integrity(clamp(to_fix.get_integrity()+(50*cast_power), 1, to_fix.max_integrity)) + drain_mana() + return TRUE + else + return FALSE + +/obj/item/melee/psyonic_blade + name = "psyonic blade" + desc = "A concentrated collection of particles and energy that looks like a swords blade.." + icon = 'icons/obj/weapons/transforming_energy.dmi' + icon_state = "blade" + inhand_icon_state = "blade" + lefthand_file = 'icons/mob/inhands/weapons/swords_lefthand.dmi' + righthand_file = 'icons/mob/inhands/weapons/swords_righthand.dmi' + w_class = WEIGHT_CLASS_HUGE + force = 10 + throwforce = 10 + hitsound = 'sound/items/weapons/blade1.ogg' + attack_verb_continuous = list("attacks", "slashes", "stabs", "slices", "tears", "lacerates", "rips", "dices", "cuts") + attack_verb_simple = list("attack", "slash", "stab", "slice", "tear", "lacerate", "rip", "dice", "cut") + sharpness = SHARP_EDGED + block_chance = 0 + item_flags = DROPDEL | ABSTRACT | HAND_ITEM | EXAMINE_SKIP + color = COLOR_BRIGHT_BLUE + +/obj/item/melee/psyonic_blade/New(loc, power) + . = ..() + force = 10 + power*1.5 + block_chance = power*5 + +/obj/item/psyonic_fire + name = "small psyonic fire" + desc = "Small bluish fire, that jumps on your fingers and surprisigly doesn't burn them." + icon = 'icons/obj/weapons/hand.dmi' + icon_state = "greyscale" + color = COLOR_BRIGHT_BLUE + inhand_icon_state = "greyscale" + light_range = 2 + light_power = 2 + light_color = LIGHT_COLOR_LIGHT_CYAN + light_on = TRUE + damtype = BURN + force = 5 + attack_verb_continuous = list("burns", "singes") + attack_verb_simple = list("burn", "singe") + resistance_flags = FIRE_PROOF + w_class = WEIGHT_CLASS_HUGE + light_system = OVERLAY_LIGHT + toolspeed = 2 + tool_behaviour = TOOL_WELDER + item_flags = DROPDEL | ABSTRACT | HAND_ITEM | EXAMINE_SKIP + heat = HIGH_TEMPERATURE_REQUIRED - 100 + +// Копирка с абдукторского +/obj/item/psyonic_omnitool + name = "psyonic omnitool" + desc = "Space Swiss Army Knife, able to shapeshift itself to fulfill psyonics needs." + icon = 'icons/obj/antags/abductor.dmi' + lefthand_file = 'icons/mob/inhands/antag/abductor_lefthand.dmi' + righthand_file = 'icons/mob/inhands/antag/abductor_righthand.dmi' + icon_state = "omnitool" + inhand_icon_state = "silencer" + toolspeed = 1 + tool_behaviour = TOOL_SCREWDRIVER + color = COLOR_BRIGHT_BLUE + usesound = 'sound/items/pshoom/pshoom.ogg' + var/list/tool_list = list() + item_flags = DROPDEL | ABSTRACT | HAND_ITEM | EXAMINE_SKIP + +/obj/item/psyonic_omnitool/New(loc) + . = ..() + tool_list = list( + "Crowbar" = image(icon = 'icons/obj/tools.dmi', icon_state = "crowbar"), + "Multitool" = image(icon = 'icons/obj/devices/tool.dmi', icon_state = "multitool"), + "Screwdriver" = image(icon = 'icons/obj/tools.dmi', icon_state = "screwdriver_map"), + "Wirecutters" = image(icon = 'icons/obj/tools.dmi', icon_state = "cutters_map"), + "Wrench" = image(icon = 'icons/obj/tools.dmi', icon_state = "wrench"), + ) + +/obj/item/psyonic_omnitool/get_all_tool_behaviours() + return list( + TOOL_CROWBAR, + TOOL_MULTITOOL, + TOOL_SCREWDRIVER, + TOOL_WIRECUTTER, + TOOL_WRENCH, + ) + +/obj/item/psyonic_omnitool/examine() + . = ..() + . += " The mode is: [tool_behaviour]" + +/obj/item/psyonic_omnitool/attack_self(mob/user) + if(!user) + return + + var/tool_result = show_radial_menu(user, src, tool_list, custom_check = CALLBACK(src, PROC_REF(check_menu), user), require_near = TRUE, tooltips = TRUE) + if(!check_menu(user)) + return + switch(tool_result) + if("Crowbar") + tool_behaviour = TOOL_CROWBAR + if("Multitool") + tool_behaviour = TOOL_MULTITOOL + if("Screwdriver") + tool_behaviour = TOOL_SCREWDRIVER + if("Wirecutters") + tool_behaviour = TOOL_WIRECUTTER + if("Wrench") + tool_behaviour = TOOL_WRENCH + +/obj/item/psyonic_omnitool/proc/check_menu(mob/user) + if(!istype(user)) + return FALSE + if(user.incapacitated || !user.Adjacent(src)) + return FALSE + return TRUE + diff --git a/tff_modular/modules/psyonics/code/redaction.dm b/tff_modular/modules/psyonics/code/redaction.dm new file mode 100644 index 00000000000..9c797baa0a8 --- /dev/null +++ b/tff_modular/modules/psyonics/code/redaction.dm @@ -0,0 +1,379 @@ +#define HALFWAYCRITDEATH ((HEALTH_THRESHOLD_CRIT + HEALTH_THRESHOLD_DEAD) * 0.5) + +/// Школа лечения +/// Имеет 5 спеллов в данный момент +/// Roentgen - обычный мед скан, работающий на дистанции +/// Меnding - лечит кровь, открытые раны и окси урон. Также удаляет импланты/ксеноморфов из тела при определённых условиях. +/// Ethanol Synthesis - если интент на харма, то "превращает" упитанность в алкоголь на дистанции. Любой другой - наоборот. +/// Cleansing - лечит токс урон +/// Revive - пытается оживить труп + +// Выдать школу лечения +/mob/living/carbon/human/proc/try_add_redaction_school(tier = 0, additional_school = 0) + if(tier >= 0) + var/datum/action/new_action = new /datum/action/cooldown/spell/pointed/psyonic/psyonic_roentgen(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 1) + var/datum/action/new_action = new /datum/action/cooldown/spell/touch/psyonic/psyonic_mending(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 2) + var/datum/action/new_action2 = new /datum/action/cooldown/spell/pointed/psyonic/psyonic_drunkness(src.mind || src, tier, additional_school) + new_action2.Grant(src) + if(tier >= 3) + var/datum/action/new_action = new /datum/action/cooldown/spell/touch/psyonic/psyonic_cleansing(src.mind || src, tier, additional_school) + new_action.Grant(src) + if(tier >= 4) + var/datum/action/new_action = new /datum/action/cooldown/spell/touch/psyonic/psyonic_revival(src.mind || src, tier, additional_school) + new_action.Grant(src) + +// Мед сканер на расстоянии +/datum/action/cooldown/spell/pointed/psyonic/psyonic_roentgen + name = "Roentgen" + desc = "Try to read target's vital energy and determine their state." + button_icon = 'tff_modular/modules/psyonics/icons/actions.dmi' + button_icon_state = "roentgen" + + cooldown_time = 1 SECONDS + + mana_cost = 10 + target_msg = "You feel like someone is looking deep into you." + + active_msg = "You prepare to scan a target..." + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_roentgen/New(Target) + . = ..() + if(secondary_school == "Redaction") + cast_power += 1 + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_roentgen/is_valid_target(atom/cast_on) + if(!ishuman(cast_on)) + return FALSE + + return TRUE + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_roentgen/cast(mob/living/carbon/human/cast_on) + . = ..() + if(cast_on.can_block_magic(antimagic_flags)) + to_chat(cast_on, span_notice("Your body is being read by a psyonic nearby.")) + else + to_chat(cast_on, span_warning(target_msg)) + if(cast_power > 2) + healthscan(owner, cast_on, SCANNER_VERBOSE, TRUE, tochat = TRUE) + else + healthscan(owner, cast_on, SCANNER_VERBOSE, FALSE, tochat = TRUE) + drain_mana() + return TRUE + +/obj/item/melee/touch_attack/psyonic_mending + name = "psyonic sparks" + desc = "Concentrated psyonic energy in a hand." + icon = 'icons/obj/weapons/hand.dmi' + icon_state = "greyscale" + color = COLOR_VERY_PALE_LIME_GREEN + inhand_icon_state = "greyscale" + light_range = 2 + light_power = 1 + light_color = LIGHT_COLOR_LIGHT_CYAN + light_on = TRUE + +// Восстанавливает кровь, окси урон, открытые травмы. Не лечит другие типы урона. Если вторичка - психокинетика, то вынимает импланты. +// Если уровень Эпсилон - удаляет лярвы ксеноморфов. +/datum/action/cooldown/spell/touch/psyonic/psyonic_mending + name = "Psyonic Mending" + desc = "You can try to restore patients bloodloss, bones, open wounds and partially oxygen level in blood. Does not heal brute, burn, \ + and toxic damage. With Psychokinesis as secondary school also can remove small implants. At Epsilon level can remove xenomorph larvae." + button_icon = 'tff_modular/modules/psyonics/icons/actions.dmi' + button_icon_state = "mending_touch" + cooldown_time = 3 SECONDS + mana_cost = 25 + stamina_cost = 25 + target_msg = "You body numbs a little." + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to mend a patient.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = TRUE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_mending/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(ishuman(victim)) + var/mob/living/carbon/human/human_victim = victim + if(issynthetic(human_victim) && secondary_school != "Psychokinesis") + to_chat(owner, span_notice("I dont know how to work with synths.")) + return FALSE + if(human_victim.can_block_magic(antimagic_flags)) + to_chat(human_victim, span_notice("Psionic nearby tries to mend you.")) + else + to_chat(human_victim, span_warning(target_msg)) + if(!do_after(mendicant, 5 SECONDS, human_victim, IGNORE_SLOWDOWNS, TRUE)) + accident_harm(human_victim) + else + try_heal_all(human_victim) + drain_mana() + return TRUE + else + return FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_mending/proc/accident_harm(mob/living/carbon/human/patient) + patient.take_bodypart_damage(5, wound_bonus = 100) + +/datum/action/cooldown/spell/touch/psyonic/psyonic_mending/proc/try_heal_all(mob/living/carbon/human/patient) + if(patient.blood_volume < BLOOD_VOLUME_NORMAL) + patient.blood_volume += ((BLOOD_VOLUME_NORMAL - patient.blood_volume) / 5) * cast_power // Эффективнее когда крови мало + + if(patient.all_wounds) + var/datum/wound/wound2fix = patient.all_wounds[1] + wound2fix.remove_wound() + playsound(patient, 'sound/effects/wounds/crack2.ogg', 40, TRUE) + + if(patient.getOxyLoss() >= OXYLOSS_PASSOUT_THRESHOLD-10) + patient.adjustOxyLoss(-cast_power*5, forced = TRUE) + + if(patient.implants && secondary_school == "Psychokinesis" && cast_power >= 2) // Невольно удаляет импланты, если есть + var/obj/item/implant/imp_2_del = pick(patient.implants) + var/atom/drop_loc = imp_2_del.drop_location() + imp_2_del.removed(patient) + if(drop_loc) + imp_2_del.forceMove(drop_loc) + patient.visible_message( + span_warning("[patient]s skin rips open, [imp_2_del] flies out of it and then the wound suddenly heals."), + span_danger("You feel implant inside you starts to move and rips itself out! The resulting wound quickly closes itself though."), + ) + + if(patient.get_organ_slot("parasite_egg") && cast_power >=4) // Удаляем ксеноморфов + var/obj/item/organ/body_egg/parasite = patient.get_organ_slot("parasite_egg") + parasite.owner.vomit(VOMIT_CATEGORY_BLOOD | MOB_VOMIT_KNOCKDOWN | MOB_VOMIT_HARM) + parasite.owner.visible_message( + span_warning("[patient] twitches, gags and vomits a living creqture with blood! Gross!"), + span_bolddanger("Suddenly you feel sharp pain in your chest, then something starts moving up your throat. \ + Before you can react somethign slips past your lips with a mix of vomit and blood!"), + ) + var/atom/drop_loc = parasite.drop_location() + parasite.Remove(parasite.owner) + if(drop_loc) + parasite.forceMove(drop_loc) + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_drunkness + name = "Ethanol Body Synthesis" + desc = "Convert fat masses to ethanol in combat mode, vice versa otherwise. Works with time on distance, but not on synthetics." + button_icon = 'icons/obj/drinks/bottles.dmi' + button_icon_state = "beer" + cooldown_time = 1 SECONDS + mana_cost = 30 + stamina_cost = 30 + active_msg = "You prepare to convert fat tissues..." + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_drunkness/is_valid_target(atom/cast_on) + if(!ishuman(cast_on)) + return FALSE + if(issynthetic(cast_on) ) + to_chat(owner, span_notice("It's a synth. What am I supposed to convert? Oil?")) + return FALSE + return TRUE + +/datum/action/cooldown/spell/pointed/psyonic/psyonic_drunkness/cast(mob/living/carbon/human/cast_on) + . = ..() + cast_on.apply_status_effect(/datum/status_effect/psyonic_fat_conversion, 5 * cast_power SECONDS, !cast_on.combat_mode) + drain_mana() + return TRUE + +/// С каждым тиком конвертируем или жир в алкоголь, или алкоголь в жир +/datum/status_effect/psyonic_fat_conversion + id = "psyonic_fat_conversion" + alert_type = null + remove_on_fullheal = TRUE + var/eth2fat = TRUE + +/datum/status_effect/psyonic_fat_conversion/on_creation(mob/living/new_owner, duration = 10 SECONDS, eth2fat = TRUE) + src.duration = duration + src.eth2fat = eth2fat + return ..() + +/datum/status_effect/psyonic_fat_conversion/tick(seconds_between_ticks) + var/mob/living/carbon/human/human_owner = owner + var/fat = human_owner.nutrition + var/drunk = human_owner.get_drunk_amount() + if(eth2fat && !drunk) // если нет алкашки, то и конвертировать нечего + return + if(eth2fat) // алкашку в жир + human_owner.adjust_drunk_effect(-(drunk/6)) + human_owner.adjust_nutrition(drunk) + if(!eth2fat && fat) // жир в алкашку. За 25 тиков полностью обезжирим человека! + human_owner.adjust_drunk_effect(fat/125) + human_owner.adjust_nutrition(-(fat/25)) + +// Лечит токс урон. +/datum/action/cooldown/spell/touch/psyonic/psyonic_cleansing + name = "Psyonic Cleansing" + desc = "Filters patient blood out of toxins and removes accumulated radiation." + button_icon = 'tff_modular/modules/psyonics/icons/actions.dmi' + button_icon_state = "cleansing" + cooldown_time = 3 SECONDS + mana_cost = 35 + stamina_cost = 40 + target_msg = "Your insides itch." + + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to cleanse a patient.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = TRUE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_cleansing/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(ishuman(victim)) + var/mob/living/carbon/human/human_victim = victim + if(issynthetic(human_victim) && secondary_school != "Psychokinesis") + to_chat(owner, span_notice("I dont know how to work with synths. Why would I even try to? They dont have toxins.")) + return FALSE + if(human_victim.can_block_magic(antimagic_flags)) + to_chat(human_victim, span_notice("Psionic nearby tries to cleanse you.")) + else + to_chat(human_victim, span_warning(target_msg)) + if(!do_after(mendicant, 5 SECONDS, human_victim, IGNORE_SLOWDOWNS, TRUE)) + accident_harm(human_victim) + else + try_heal_all(human_victim) + drain_mana() + return TRUE + else + return FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_cleansing/proc/accident_harm(mob/living/carbon/human/patient) + patient.apply_damage(25, TOX, BODY_ZONE_CHEST) + +/datum/action/cooldown/spell/touch/psyonic/psyonic_cleansing/proc/try_heal_all(mob/living/carbon/human/patient) + if(patient.getToxLoss() > 0) + patient.adjustToxLoss(clamp(-(patient.getToxLoss()/3)*cast_power, -35, 0), forced = TRUE) + +/** + * Пытается оживить труп + * + * Логика прока: + * 1. Смотрит есть ли причина по которой нельзя дефибнуть, пытается её устранить + * 2. Если не удалось устранить - не оживляет + * 3. Если удалось устранить причину - проверяет можно ли дефибнуть снова. Если появилась другая - не оживляет. Всё ок - оживляет. + */ +/datum/action/cooldown/spell/touch/psyonic/psyonic_revival + name = "Psyonic Revival" + desc = "Ability to trick death itself. Call for the bodys soul in the other realm in attempt to restore its vessel condition to an... acceptable levels." + button_icon = 'tff_modular/modules/psyonics/icons/actions.dmi' + button_icon_state = "revive" + cooldown_time = 3 SECONDS + mana_cost = 80 + stamina_cost = 160 + + hand_path = /obj/item/melee/touch_attack/psyonic_mending + draw_message = span_notice("You ready your hand to revive a patient.") + drop_message = span_notice("You lower your hand.") + can_cast_on_self = FALSE + +/datum/action/cooldown/spell/touch/psyonic/psyonic_revival/cast_on_hand_hit(obj/item/melee/touch_attack/hand, atom/victim, mob/living/carbon/mendicant) + if(ishuman(victim)) + var/mob/living/carbon/human/human_victim = victim + var/synth_check = (secondary_school == "Psychokinesis" || !issynthetic(human_victim)) + if(human_victim.stat == DEAD && synth_check) + owner.visible_message(span_notice("[owner] kneels before the body of [victim], lowers their hands onto cadavers chest and begins... meditating?"), + span_notice("You kneel before the cadaver, lower your hands onto their chest and start to concentrate energy. You better not \ + get disturbed, or else...")) + var/obj/effect/abstract/particle_holder/particle_effect = new(human_victim, /particles/droplets/psyonic) + if(!do_after(mendicant, 25 SECONDS, human_victim, IGNORE_SLOWDOWNS, TRUE)) + accident_harm(owner) // Ауч. Больно бьёт по псионику + else + try_heal_all(human_victim) + if(particle_effect) + QDEL_NULL(particle_effect) + drain_mana() + return TRUE + else if(issynthetic(human_victim) && human_victim.stat == DEAD) + to_chat(owner, span_warning("Your psyonic energy does not work very well with synths.")) + return FALSE + else + return FALSE + else + return FALSE + +// 25 токса + 50 брута + 1 травма + позор роду псионическому +/datum/action/cooldown/spell/touch/psyonic/psyonic_revival/proc/accident_harm(mob/living/carbon/human/unlucky_guy) + unlucky_guy.apply_damage(25, TOX, BODY_ZONE_CHEST) + unlucky_guy.take_bodypart_damage(25, wound_bonus = 100) + unlucky_guy.take_bodypart_damage(25, wound_bonus = 100, sharpness = SHARP_EDGED) + unlucky_guy.visible_message(span_warning("Something inside of [unlucky_guy]s body cracks!"), + span_bolddanger("Your revival energy backfired at you, causing severe injuries!"), + blind_message = span_hear("You hear bones breaking.")) + +/datum/action/cooldown/spell/touch/psyonic/psyonic_revival/proc/can_defib_human(mob/living/carbon/human/patient) + var/defib_result = patient.can_defib() + var/fail_reason + var/synth_check = (secondary_school == "Psychokinesis") + switch (defib_result) + if (DEFIB_FAIL_SUICIDE) + fail_reason = "Patient has left this world on his terms. You can not restore him." + if (DEFIB_FAIL_NO_HEART) + fail_reason = "Patient's heart is missing and you are not Alpha tier to create it out of air." + if (DEFIB_FAIL_FAILING_HEART) + var/obj/item/organ/heart/target_heart = patient.get_organ_slot(ORGAN_SLOT_HEART) + if(target_heart) + target_heart.operated = TRUE + if((target_heart.organ_flags & ORGAN_ORGANIC) || synth_check) // Only fix organic heart + patient.setOrganLoss(ORGAN_SLOT_HEART, 60) + else + fail_reason = "Patient's heart is made out of metals and plastics. You can not work with that." + if (DEFIB_FAIL_TISSUE_DAMAGE) + patient.adjustBruteLoss(patient.getBruteLoss()/2) + patient.adjustFireLoss(patient.getFireLoss()/2) + if ((patient.getBruteLoss() >= MAX_REVIVE_BRUTE_DAMAGE) || (patient.getFireLoss() >= MAX_REVIVE_FIRE_DAMAGE)) + fail_reason = "Patient's body is too flimsy to support life, but your energy partially healed that. Maybe try again?" + if (DEFIB_FAIL_HUSK) + patient.cure_husk() + if(HAS_TRAIT(patient, TRAIT_HUSK)) + fail_reason = "Patient's body is a mere husk, and you can not cure them." + if (DEFIB_FAIL_FAILING_BRAIN) + var/obj/item/organ/brain/target_brain = patient.get_organ_slot(ORGAN_SLOT_BRAIN) + if(target_brain) + if((target_brain.organ_flags & ORGAN_ORGANIC) || synth_check) // Only fix organic heart + patient.setOrganLoss(ORGAN_SLOT_BRAIN, 60) + else + fail_reason = "Patient's brain is made out of metals and plastics. You can not work with that." + if (DEFIB_FAIL_NO_INTELLIGENCE) + fail_reason = "Patient is braindead. Your energy doesnt course through such body." + if (DEFIB_FAIL_NO_BRAIN) + fail_reason = "Patient's brain is missing and even if you were Alpha tier, you could not restore him.." + if (DEFIB_FAIL_BLACKLISTED) + fail_reason = "Patient soul is linked to the dead realm with death grip. You can not restore him." + if (DEFIB_FAIL_DNR) + fail_reason = "Patient cannot be restored due to star misalignment." + return fail_reason + +/datum/action/cooldown/spell/touch/psyonic/psyonic_revival/proc/try_heal_all(mob/living/carbon/human/patient) + var/fail_reason = can_defib_human(patient) // first to possibly cure something + fail_reason = can_defib_human(patient) // second to actually try revival + if(fail_reason) + owner.visible_message(span_warning(fail_reason)) + else + var/defib_result = patient.can_defib() + if (defib_result == DEFIB_POSSIBLE) + var/total_brute = patient.getBruteLoss() + var/total_burn = patient.getFireLoss() + + var/need_mob_update = FALSE + if (patient.health > HALFWAYCRITDEATH) + need_mob_update += patient.adjustOxyLoss(patient.health - HALFWAYCRITDEATH, updating_health = FALSE) + else + var/overall_damage = total_brute + total_burn + patient.getToxLoss() + patient.getOxyLoss() + var/mobhealth = patient.health + need_mob_update += patient.adjustOxyLoss((mobhealth - HALFWAYCRITDEATH) * (patient.getOxyLoss() / overall_damage), updating_health = FALSE) + need_mob_update += patient.adjustToxLoss((mobhealth - HALFWAYCRITDEATH) * (patient.getToxLoss() / overall_damage), updating_health = FALSE, forced = TRUE) // force tox heal for toxin lovers too + need_mob_update += patient.adjustFireLoss((mobhealth - HALFWAYCRITDEATH) * (total_burn / overall_damage), updating_health = FALSE) + need_mob_update += patient.adjustBruteLoss((mobhealth - HALFWAYCRITDEATH) * (total_brute / overall_damage), updating_health = FALSE) + if(need_mob_update) + patient.updatehealth() + owner.visible_message(span_green("Revival successful.")) + playsound(src, 'sound/effects/ghost.ogg', 40, FALSE) + patient.set_heartattack(FALSE) + if(defib_result == DEFIB_POSSIBLE) + patient.grab_ghost() + patient.revive() + patient.emote("gasp") + patient.set_jitter_if_lower(200 SECONDS) + to_chat(patient, "[CONFIG_GET(string/blackoutpolicy)]") + SEND_SIGNAL(patient, COMSIG_LIVING_MINOR_SHOCK) + log_combat(owner, patient, "psyonically revived") + +#undef HALFWAYCRITDEATH diff --git a/tff_modular/modules/psyonics/icons/actions.dmi b/tff_modular/modules/psyonics/icons/actions.dmi new file mode 100644 index 00000000000..beb7a648626 Binary files /dev/null and b/tff_modular/modules/psyonics/icons/actions.dmi differ diff --git a/tff_modular/modules/psyonics/icons/card.dmi b/tff_modular/modules/psyonics/icons/card.dmi new file mode 100644 index 00000000000..f7a2eee1ba0 Binary files /dev/null and b/tff_modular/modules/psyonics/icons/card.dmi differ diff --git a/tgstation.dme b/tgstation.dme index 14f3ecd3edc..4e14f21e83a 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -8900,6 +8900,14 @@ #include "tff_modular\modules\poster_contest\contraband.dm" #include "tff_modular\modules\poster_contest\official.dm" #include "tff_modular\modules\poster_contest\winners_items\code.dm" +#include "tff_modular\modules\psyonics\code\_psyonics.dm" +#include "tff_modular\modules\psyonics\code\_quirk.dm" +#include "tff_modular\modules\psyonics\code\coersion.dm" +#include "tff_modular\modules\psyonics\code\cyberimp.dm" +#include "tff_modular\modules\psyonics\code\documents.dm" +#include "tff_modular\modules\psyonics\code\energistics.dm" +#include "tff_modular\modules\psyonics\code\psychokinesis.dm" +#include "tff_modular\modules\psyonics\code\redaction.dm" #include "tff_modular\modules\quirks\code\_quirk.dm" #include "tff_modular\modules\redsec\code\vending.dm" #include "tff_modular\modules\redsec_reskins\code\beret_reskin.dm" diff --git a/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tff/psyonic_school.tsx b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tff/psyonic_school.tsx new file mode 100644 index 00000000000..ec60f99b2ee --- /dev/null +++ b/tgui/packages/tgui/interfaces/PreferencesMenu/preferences/features/character_preferences/tff/psyonic_school.tsx @@ -0,0 +1,15 @@ +import { FeatureChoiced } from '../../base'; +import { FeatureDropdownInput } from '../../dropdowns'; + +export const psyonic_school: FeatureChoiced = { + name: 'School', + description: 'Choose a school, which abilities you shall receive.', + component: FeatureDropdownInput, +}; + +export const psyonic_school_secondary: FeatureChoiced = { + name: 'Secondary School', + description: + 'Choose a secondary school. Abilities in it will be less powerful.', + component: FeatureDropdownInput, +}; diff --git a/tgui/packages/tgui/interfaces/PsyonicLicense.tsx b/tgui/packages/tgui/interfaces/PsyonicLicense.tsx new file mode 100644 index 00000000000..bd664883817 --- /dev/null +++ b/tgui/packages/tgui/interfaces/PsyonicLicense.tsx @@ -0,0 +1,120 @@ +import { useBackend } from '../backend'; +import { Icon, Image, LabeledList, Section, Stack } from '../components'; +import { Window } from '../layouts'; + +type Data = { + character_preview: string; + primary_school: string; + secondary_school: string; + psyonic_level: string; + owner_name: string; + owner_age: number; + owner_species: string; + owner_preview: string; +}; + +export const PsyonicLicense = (props) => { + const { act, data } = useBackend(); + const { + primary_school, + secondary_school, + psyonic_level, + owner_name, + owner_age, + owner_species, + owner_preview, + } = data; + return ( + + + + + + + +
+ {'Мы всегда наблюдаем'} +
+
+ + + +
+ + +
+ {/* */} + +
+
+ +
+ + + {data.owner_name} + + + {data.owner_age} + + + {data.owner_species} + + + {data.primary_school} + + + {data.secondary_school} + + + {data.psyonic_level} + + +
+
+
+ + +
+ { + 'Данная лицензия удостоверяет то, что пользователь является псиоником.' + } + { + ' Лицензия пользователя псионики позволяет пользователю применять способности в целях самозащиты, оказания лечебной помощи и в прочих, установленных законом случаев.' + } + { + ' Пользователь псионики обязан иметь лицензию при себе всё время, иначе его действия могут быть классифицированы СБ НТ как нелегальные и повлечь за собой юридические последствия.' + } +
+
+
+ + +
+ {'ПОДДЕЛКА ЛИЦЕНЗИИ КАРАЕТСЯ ЗАКОНОМ'} +
+
+
+
+
+ ); +};