diff --git a/aurorastation.dme b/aurorastation.dme index 6b28dff4c93..3cece0f186d 100644 --- a/aurorastation.dme +++ b/aurorastation.dme @@ -3060,6 +3060,11 @@ #include "code\modules\mob\living\simple_animal\hostile\hivebots\hivebot_harvester_projectiles.dm" #include "code\modules\mob\living\simple_animal\hostile\hivebots\hivebot_projectiles.dm" #include "code\modules\mob\living\simple_animal\hostile\psirens\psiren.dm" +#include "code\modules\mob\living\simple_animal\hostile\psirens\psiren_beckoner.dm" +#include "code\modules\mob\living\simple_animal\hostile\psirens\psiren_matriarch.dm" +#include "code\modules\mob\living\simple_animal\hostile\psirens\psiren_omen.dm" +#include "code\modules\mob\living\simple_animal\hostile\psirens\psiren_projectiles.dm" +#include "code\modules\mob\living\simple_animal\hostile\psirens\psiren_psi_damage.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\aquatic.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\cavern.dm" #include "code\modules\mob\living\simple_animal\hostile\retaliate\clown.dm" diff --git a/code/controllers/subsystems/hallucinations.dm b/code/controllers/subsystems/hallucinations.dm index 17fb488150d..7b2af9003c8 100644 --- a/code/controllers/subsystems/hallucinations.dm +++ b/code/controllers/subsystems/hallucinations.dm @@ -342,11 +342,11 @@ SUBSYSTEM_DEF(hallucinations) return TRUE -/datum/controller/subsystem/hallucinations/proc/deliver_adpi_message(var/mob/living/target, var/message) +/datum/controller/subsystem/hallucinations/proc/deliver_adpi_message(mob/living/target, message, message_type = /atom/movable/screen/text/screen_text/mental_message) /// % chance to pick one of the general thematic ADPI messages. Enjoy, code peekers; this is all you get! if(prob(15)) message = pick("The water is dripping dripping dripping all around you.","Water flowing over stone, but there is no stone.","The waterfall roars o'er the cliff's edge.","Water, water, water. You are drowning.","Water, water, water. You will be awake for it.","Drums, drums, drums, unrelenting.","The drumbeat draws e'er closer.","Tap, ta-tap, ta-tap, tap, ta-tap, ta-tap.","Tap, tap tap, ta-tap, tap-tap, ta-tap.","Ta-ta-tap, tap, ta-tap, tap, tap ta-tap.") - target.play_screen_text("[message]", /atom/movable/screen/text/screen_text/mental_message, COLOR_PURPLE) + target.play_screen_text("[message]", message_type, COLOR_PURPLE) to_chat(target, SPAN_CULT(FONT_LARGE("[message]"))) if(isskrell(target)) diff --git a/code/game/gamemodes/technomancer/spells/summon/summon_creature.dm b/code/game/gamemodes/technomancer/spells/summon/summon_creature.dm index 12d94f1f995..9292d80110d 100644 --- a/code/game/gamemodes/technomancer/spells/summon/summon_creature.dm +++ b/code/game/gamemodes/technomancer/spells/summon/summon_creature.dm @@ -37,7 +37,8 @@ "BEAR" = /mob/living/simple_animal/hostile/bear, "PSIREN LASHER" = /mob/living/simple_animal/hostile/psiren, "PSIREN DARTER" = /mob/living/simple_animal/hostile/psiren/ranged, - "PSIREN OMEN" = /mob/living/simple_animal/hostile/psiren/omen + "PSIREN OMEN" = /mob/living/simple_animal/hostile/psiren/omen, + "PSIREN BECKONER" = /mob/living/simple_animal/hostile/psiren/omen/beckoner, ) cooldown = 30 instability_cost = 10 diff --git a/code/modules/events/wandering_psirens.dm b/code/modules/events/wandering_psirens.dm index 013ff01e35f..7809e83008e 100644 --- a/code/modules/events/wandering_psirens.dm +++ b/code/modules/events/wandering_psirens.dm @@ -49,15 +49,19 @@ while (i <= num_groups) var/group_size = rand(group_size_min, group_size_max) for(var/j = 1, j <= group_size, j++) - if(prob(95)) + if(prob(75)) var/basic_psiren if (prob(50)) basic_psiren = new /mob/living/simple_animal/hostile/psiren(spawn_locations[i]) else basic_psiren = new /mob/living/simple_animal/hostile/psiren/ranged(spawn_locations[i]) spawned_psirens += WEAKREF(basic_psiren) else - var/mob/living/simple_animal/hostile/psiren/omen/new_omen = new(spawn_locations[i]) - spawned_psirens += WEAKREF(new_omen) + if (prob(75)) + var/mob/living/simple_animal/hostile/psiren/omen/new_omen = new(spawn_locations[i]) + spawned_psirens += WEAKREF(new_omen) + else + var/mob/living/simple_animal/hostile/psiren/omen/beckoner/new_beckoner = new(spawn_locations[i]) + spawned_psirens += WEAKREF(new_beckoner) CHECK_TICK i++ diff --git a/code/modules/mob/living/simple_animal/hostile/psirens/psiren.dm b/code/modules/mob/living/simple_animal/hostile/psirens/psiren.dm index 988567cb9a3..7108850b9b0 100644 --- a/code/modules/mob/living/simple_animal/hostile/psirens/psiren.dm +++ b/code/modules/mob/living/simple_animal/hostile/psirens/psiren.dm @@ -1,6 +1,6 @@ /// Psiren "Lasher" /mob/living/simple_animal/hostile/psiren - name = "Psiren" + name = "psiren lasher" desc = "A strange, squid-like xenofauna. Its eye is full of malevolence." icon = 'icons/mob/npc/psirens.dmi' icon_state = "psiren_lasher" @@ -10,10 +10,10 @@ blood_overlay_icon = 'icons/mob/npc/blood_overlay.dmi' health = 65 maxhealth = 65 - melee_damage_lower = 10 - melee_damage_upper = 10 + melee_damage_lower = 8 + melee_damage_upper = 8 armor_penetration = 40 - attack_flags = DAMAGE_FLAG_SHARP|DAMAGE_FLAG_EDGE|DAMAGE_FLAG_PSIONIC + attack_flags = DAMAGE_FLAG_EDGE|DAMAGE_FLAG_PSIONIC destroy_surroundings = FALSE break_stuff_probability = 0 attacktext = "lashes" @@ -49,6 +49,7 @@ smart_melee = TRUE smart_ranged = FALSE + /// Messages used by the psiren for its psychic damage effects. var/list/cursed_messages = list("DRINK DEEPLY OF THE FINAL NIGHT.", "YOUR LOVED ONES WILL NEVER KNOW OF YOUR JOY AGAIN.", "LEAVE THIS PLACE THROUGH THE NEAREST AIRLOCK.", @@ -80,6 +81,19 @@ "YOU ARE GUILTY OF YOUR FOREFATHERS SINS.", "YOUR MISSION FAILED BEFORE IT BEGAN, THE PHORON YOU SEEK WAS NEVER REAL. YOUR NATION WILL DROWN FOR ITS WANT. IT'S ALL YOUR FAULT.") + /// How much damage the psiren's attacks deal directly to the victim's morale on a successful hit. + var/psi_damage = -0.1 + + /// How much extra psychic damage is dealt per point of psi-sensitivity. + var/psi_sensitivity_multiplier = -0.05 + + /// The chance per successful hit of triggering an ADPI message on the target. + var/adpi_chance = 5 + +/** + * Psirens have a unique ability to telepathically retaliate against anyone who tries to examine them. + * It only works if the psiren is alive, and the target isn't mindshielded. + */ /mob/living/simple_animal/hostile/psiren/get_examine_text(mob/user, distance, is_adjacent, infix, suffix) // If the victim has any psi-protection, we give them a completely normal examine proc. if (stat == DEAD || user.is_psi_blocked(src, FALSE)) @@ -88,147 +102,22 @@ // Otherwise HIJACK IT. return list(FONT_HUGE(SPAN_CULT(pick(cursed_messages)))) -/datum/moodlet/psiren_psi_damage - // Descriptor and Initial logic is bespoke for this moodlet. - moodlet_descriptor = "residual psionic pressure." - initial_descriptor = null - duration = 15.0 MINUTES - var/list/cursed_messages = list("DRINK DEEPLY OF THE FINAL NIGHT.", - "YOUR LOVED ONES WILL NEVER KNOW OF YOUR JOY AGAIN.", - "LEAVE THIS PLACE THROUGH THE NEAREST AIRLOCK.", - "DEATH IS BUT THE FIRST DANCE IN ETERNITY.", - "YOU DESERVE ALL OF YOUR SUFFERING.", - "THE STARS HAVE FORGOTTEN YOUR NAME.", - "YOUR BONES REMEMBER CRIMES YOU NEVER COMMITTED.", - "EVERY GOD YOU HAVE EVER PRAYED TO IS DEAD.", - "THE UNIVERSE REGRETS YOUR CREATION.", - "YOU HAVE ALREADY FAILED.", - "YOU WILL NEVER WAKE UP FROM THIS.", - "YOUR SCREAMS ARRIVE BEFORE YOU MAKE THEM.", - "YOU WERE WARNED.", - "THE RIVERS RUN COLD WITH MEMORY.", - "THE LEMURIAN SEA HUNGERS.", - "THE LEMURIAN SEA HAS BECOME YOUR TOMB.", - "THERE IS A DRUMBEAT BELOW REALITY.", - "THE SUNSET LASTS FOREVER HERE.", - "YOU CANNOT HIDE INSIDE YOUR OWN HEAD.", - "I KNOW WHAT YOU DREAM ABOUT.", - "I AM WEARING YOUR VOICE.", - "YOU INVITED ME IN", - "THE SCC SENT YOU HERE AS A FEAST FOR GIBBERING MOUTHS. YOUR LEADERS SENT YOU HERE TO DIE.", - "YOUR MEMORY FURNISHES A REPAST FOR THE MATRIARCH.", - "THE NIGHT SHEDS A TEAR TO TELL YOU OF FEAR, OF SORROW, OF PAIN.", - "THE SCC HAS ALREADY VOIDED YOUR CONTRACT, THEY KNEW YOU WOULD DIE HERE.", - "I SAW YOU WHEN YOU THOUGHT YOU WERE ALONE.", - "THE BLOOD WILL NEVER WASH FROM YOUR HANDS.", - "YOU ARE GUILTY OF YOUR FOREFATHERS SINS.", - "YOUR MISSION FAILED BEFORE IT BEGAN, THE PHORON YOU SEEK WAS NEVER REAL. YOUR NATION WILL DROWN FOR ITS WANT. IT'S ALL YOUR FAULT.") - -/datum/moodlet/psiren_psi_damage/get_moodlet_descriptor() - // This SHOULD exist but I'm doing my due diligence in checking. - var/mob/grandparent = astype(astype(morale_component?.resolve(), MORALE_COMPONENT)?.parent, /mob) - if (!grandparent) - return // No message, nobody to send a message to. - - // Having any psi-protection will let you see the underlying moodlet. - if (grandparent.is_psi_blocked(null, FALSE)) +/// Protection from the ranged attacks of other psirens. +/mob/living/simple_animal/hostile/psiren/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit) + var/mob/shooter = astype(hitting_projectile?.firer, /mob) + if (!shooter || shooter.faction != "psiren") return ..() - var/spooky_message = pick(cursed_messages) - if (prob(10)) - grandparent.play_screen_text(spooky_message, /atom/movable/screen/text/screen_text/adpi_message, COLOR_PURPLE) + return BULLET_ACT_BLOCK - return FONT_HUGE(SPAN_CULT(spooky_message)) - -/datum/moodlet/psiren_psi_damage/send_initial_description(datum/owner) - astype(astype(morale_component?.resolve(), MORALE_COMPONENT)?.parent, /mob)?.play_screen_text(pick(cursed_messages), /atom/movable/screen/text/screen_text/adpi_message, COLOR_PURPLE) - return - -/** - * Special bullet type used for Psiren Darters - * Very low damage, but very high penetration. Resisted (or strengthened) by psi-sensitivity. - */ -/obj/projectile/bullet/pistol/psiren - name = "kinesis bolt" - damage = 8 - armor_penetration = 40 - sharp = 1 - embed = FALSE - damage_flags = DAMAGE_FLAG_BULLET | DAMAGE_FLAG_PSIONIC - muzzle_type = /obj/effect/projectile/muzzle/laser/psiren - tracer_type = /obj/effect/projectile/tracer/laser/psiren - impact_type = /obj/effect/projectile/impact/laser/psiren - icon_state = "purple_laser" - - /// How much damage this attack deals directly to the victim's morale on a successful hit. - var/psi_damage = -0.1 - - /// How much extra psychic damage is dealt per point of psi-sensitivity. - var/psi_sensitivity_multiplier = -0.05 - - /// The chance per successful hit of triggering an ADPI message on the target. - var/adpi_chance = 0 - -/obj/projectile/bullet/pistol/psiren/omen - damage = 3 - armor_penetration = 60 - psi_damage = -5.0 - psi_sensitivity_multiplier = -2.5 - adpi_chance = 5 - -/** - * Psiren ranged attacks deal a unique form of "psychic damage" which is governed by the full suite of rules regarding Psionic Sensitivity. - * This is in addition to their physical "Telekinetic" damage component. Being immune to the psychic damage does not make one immune to the physical damage. - * One can be immune to the psychic damage for a very large variety of reasons, not all of them static. - * - * "Psychic damage" attacks a character's Morale statistic rather than physical health, which inflicts gradually stacking penalties to Skill Tests. - */ -/obj/projectile/bullet/pistol/psiren/on_hit(atom/target, blocked, def_zone) +/// Psiren melee attacks deal psychic damage, just like their ranged attacks. +/mob/living/simple_animal/hostile/psiren/on_attack_mob(mob/hit_mob, obj/item/organ/external/limb) . = ..() - if (. != BULLET_ACT_HIT || !firer) - return . + try_deal_psychic_damage(hit_mob, src, psi_damage, psi_sensitivity_multiplier, adpi_chance) - var/mob/living/victim = target - // Immunity from telepathy will fully negate the psychic damage (but not physical damage) component. - // This is getting checked before the morale_comp because I want to give Active Psi Protection a chance to give the player feedback that a psychic effect occurred. - if (!istype(victim) || victim.stat == DEAD || victim.is_psi_blocked(firer, FALSE)) - return . - - // A morale component is also required for psychic damage. - var/datum/component/morale/morale_comp = victim.GetComponent(MORALE_COMPONENT) - if (!morale_comp) - return . - - var/victim_psi_sensitivity = victim.check_psi_sensitivity() - var/datum/moodlet/psi_damage_moodlet = morale_comp.load_moodlet(/datum/moodlet/psiren_psi_damage, FALSE) - psi_damage_moodlet.set_moodlet(psi_damage_moodlet.get_morale_modifier() + psi_damage + min(0, (victim_psi_sensitivity * psi_sensitivity_multiplier))) - psi_damage_moodlet.refresh_moodlet() - - if (!prob(adpi_chance) || !SShallucinations) - return . - // At this point in time all checks that would be satisfied by the ADPI subsystem have already been handled, - // now we just need to fetch a message and send one. - var/adpi_message = SShallucinations.pick_adpi_message(victim, TRUE) - if (!adpi_message) - return . - - SShallucinations.deliver_adpi_message(victim, adpi_message) - -/obj/effect/projectile/muzzle/laser/psiren - icon_state = "muzzle_scc" // I just like the pattern here - light_color = COLOR_VIOLET - -/obj/effect/projectile/tracer/laser/psiren - icon_state = "beam_scc" - light_color = COLOR_VIOLET - -/obj/effect/projectile/impact/laser/psiren - icon_state = "impact_scc" // I also just like the pattern here - light_color = COLOR_VIOLET - - -/// Psiren "Darter" +/// Psiren "Darter", a simple ranged attacker with weak damage, high penetration, and weak psychic damage. /mob/living/simple_animal/hostile/psiren/ranged + name = "psiren darter" icon_state = "psiren_darter" icon_living = "psiren_darter" icon_dead = "psiren_darter_dead" @@ -236,79 +125,12 @@ projectilesound = 'sound/weapons/wave.ogg' projectiletype = /obj/projectile/bullet/pistol/psiren +/// Smoke effect used for the Omen. /datum/effect/effect/system/smoke_spread/psiren smoke_type = /obj/effect/smoke/psiren smoke_duration = 20 SECONDS +/// "Ink Cloud" colored smoke effect. /obj/effect/smoke/psiren color = "#21093a" time_to_live = 20 SECONDS - -/** - * Psiren "Omen", a support-caster type enemy that primarily inflicts direct psychic damage. - * Relatively squishy for a medium enemy, they can react exactly once per being shot with a cloud of violet smoke. - */ -/mob/living/simple_animal/hostile/psiren/omen - icon_state = "psiren_omen" - icon_living = "psiren_omen" - icon_dead = "psiren_omen_dead" - health = 120 - maxhealth = 120 - ranged = TRUE - projectilesound = 'sound/weapons/wave.ogg' - projectiletype = /obj/projectile/bullet/pistol/psiren/omen - var/has_ink = TRUE - -/mob/living/simple_animal/hostile/psiren/omen/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit) - if (has_ink) - var/datum/effect/effect/system/smoke_spread/psiren/S = new/datum/effect/effect/system/smoke_spread/psiren() - S.set_up(5, 0, loc, null) - S.start() - has_ink = FALSE - return ..() - -/mob/living/simple_animal/hostile/psiren/matriarch - name = "Matriarch" - icon = 'icons/mob/npc/psiren_matriarch.dmi' - icon_state = "psiren_matriarch" - icon_living = "psiren_matriarch" - icon_dead = "psiren_matriarch_dead" - health = 9999 - maxhealth = 9999 - // Speaks via Xenoglossia. - universal_speak = TRUE - universal_understand = TRUE - mob_size = MOB_LARGE - mob_weight = MOB_WEIGHT_SUPERHEAVY - melee_damage_lower = 35 - melee_damage_upper = 40 - melee_reach = 2 - armor_penetration = 30 - meat_amount = 5 - meat_type = /obj/item/reagent_containers/food/snacks/squidmeat/psiren_tentacle_meat - butchering_products = list(/obj/item/reagent_containers/food/snacks/squidmeat/psiren_body_meat = 5) - -/mob/living/simple_animal/hostile/psiren/matriarch/Initialize() - . = ..() - add_spell(new /spell/targeted/eject_ink_cloud, "const_spell_ready") - -/spell/targeted/eject_ink_cloud - name = "Eject Ink Cloud" - desc = "Eject a cloud of Lemurian fog." - feedback = "CEIB" - range = 0 - spell_flags = INCLUDEUSER - charge_max = 30 SECONDS - max_targets = 1 - - invocation_type = SpI_EMOTE - invocation = "ejects a cloud of Lemurian fog." - - hud_state = "cloud" - -/spell/targeted/eject_ink_cloud/cast(mob/target, mob/living/user as mob) - ..() - var/datum/effect/effect/system/smoke_spread/psiren/S = new/datum/effect/effect/system/smoke_spread/psiren() - S.set_up(15, 0, user.loc, null) - S.start() - return TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/psirens/psiren_beckoner.dm b/code/modules/mob/living/simple_animal/hostile/psirens/psiren_beckoner.dm new file mode 100644 index 00000000000..ee0289942f3 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/psirens/psiren_beckoner.dm @@ -0,0 +1,81 @@ +/** + * A larger and older variant of an Omen. The Beckoner is a tanky medium support enemy. + * They share their attack and defense pattern with the Omen. + * But also gain a periodic psychic damage aura. + */ +/mob/living/simple_animal/hostile/psiren/omen/beckoner + name = "psiren beckoner" + icon = 'icons/mob/npc/psiren_beckoner.dmi' + icon_state = "psiren_beckoner" + icon_living = "psiren_beckoner" + icon_dead = "psiren_beckoner_dead" + health = 300 + maxhealth = 300 + ranged = TRUE + projectilesound = 'sound/weapons/wave.ogg' + projectiletype = /obj/projectile/bullet/pistol/psiren/omen + psi_damage = -5.0 + psi_sensitivity_multiplier = -2.5 + adpi_chance = 20 + cursed_messages = list( + "Please help me, I'm trapped outside the airlocks!", + "You feel drawn toward the nearest airlock.", + "Fresh air... no, vacuum... feels right somehow.", + "The void feels strangely comforting.", + "Please come save me, I'm in a lifepod just outside!", + "The ship feels unbearably cramped.", + "You need fresh air, go for a walk outside.", + "You don't need a space suit.", + "Did someone just knock on the hull?", + "I left something outside. I should go get it.", + "Come outside.", + "The nearest airlock suddenly seems very important.", + "Someone is calling for help outside!") + + /// Next scheduled time to fire a "beckon" pulse. + var/next_aura_pulse + + /// Minimum time between beckoning pulses. + var/min_pulse_time = 3 MINUTES + + /// Maximum time between beckoning pulses. + var/max_pulse_time = 9 MINUTES + + /// Maximum radial distance at which the Omen can hit a target with a psi damage pulse. + var/pulse_range_meters = 50 // Meters + + /// Base psi-damage dealt per pulse. This falls off with radial distance. + var/aura_psi_damage = -2.5 + +/mob/living/simple_animal/hostile/psiren/omen/beckoner/Initialize() + . = ..() + next_aura_pulse = REALTIMEOFDAY + rand(min_pulse_time, max_pulse_time) + hunting_pulse() + +/mob/living/simple_animal/hostile/psiren/omen/beckoner/Life(seconds_per_tick, times_fired) + . = ..() + if (stat == DEAD || next_aura_pulse > REALTIMEOFDAY) + return + + next_aura_pulse = REALTIMEOFDAY + rand(min_pulse_time, max_pulse_time) + hunting_pulse() + +/mob/living/simple_animal/hostile/psiren/omen/beckoner/proc/hunting_pulse() + // TODO: Really should port tg's handling of per-z-level mob lists. + for (var/mob/player as anything in GLOB.player_list) + if (!player || z != player.z || player.stat == DEAD) + continue + + var/dx = player.x - x + var/dy = player.y - y + var/r = sqrt((dx * dx) + (dy * dy)) + if (r > pulse_range_meters) + continue + + try_deal_psychic_damage(player, + src, + r <= 1 ? aura_psi_damage : aura_psi_damage / r, + r <= 1 ? psi_sensitivity_multiplier : psi_sensitivity_multiplier / r, + 100, + /datum/moodlet/psiren_psi_damage/beckoner, + pick(cursed_messages)) diff --git a/code/modules/mob/living/simple_animal/hostile/psirens/psiren_matriarch.dm b/code/modules/mob/living/simple_animal/hostile/psirens/psiren_matriarch.dm new file mode 100644 index 00000000000..901791934c5 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/psirens/psiren_matriarch.dm @@ -0,0 +1,45 @@ +/mob/living/simple_animal/hostile/psiren/matriarch + name = "Matriarch" + icon = 'icons/mob/npc/psiren_matriarch.dmi' + icon_state = "psiren_matriarch" + icon_living = "psiren_matriarch" + icon_dead = "psiren_matriarch_dead" + health = 9999 + maxhealth = 9999 + // Speaks via Xenoglossia. + universal_speak = TRUE + universal_understand = TRUE + mob_size = MOB_LARGE + mob_weight = MOB_WEIGHT_SUPERHEAVY + melee_damage_lower = 35 + melee_damage_upper = 40 + melee_reach = 2 + armor_penetration = 30 + meat_amount = 5 + meat_type = /obj/item/reagent_containers/food/snacks/squidmeat/psiren_tentacle_meat + butchering_products = list(/obj/item/reagent_containers/food/snacks/squidmeat/psiren_body_meat = 5) + +/mob/living/simple_animal/hostile/psiren/matriarch/Initialize() + . = ..() + add_spell(new /spell/targeted/eject_ink_cloud, "const_spell_ready") + +/spell/targeted/eject_ink_cloud + name = "Eject Ink Cloud" + desc = "Eject a cloud of Lemurian fog." + feedback = "CEIB" + range = 0 + spell_flags = INCLUDEUSER + charge_max = 30 SECONDS + max_targets = 1 + + invocation_type = SpI_EMOTE + invocation = "ejects a cloud of Lemurian fog." + + hud_state = "cloud" + +/spell/targeted/eject_ink_cloud/cast(mob/target, mob/living/user as mob) + ..() + var/datum/effect/effect/system/smoke_spread/psiren/S = new/datum/effect/effect/system/smoke_spread/psiren() + S.set_up(15, 0, user.loc, null) + S.start() + return TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/psirens/psiren_omen.dm b/code/modules/mob/living/simple_animal/hostile/psirens/psiren_omen.dm new file mode 100644 index 00000000000..9c91aa83636 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/psirens/psiren_omen.dm @@ -0,0 +1,26 @@ +/** + * Psiren "Omen", a ranged support-caster type enemy. + * Their attacks deal almost no physical damage at all, but very high psychic damage. + * Relatively squishy for a medium enemy, they can react exactly once per being shot with a cloud of violet smoke. + */ +/mob/living/simple_animal/hostile/psiren/omen + name = "psiren omen" + icon_state = "psiren_omen" + icon_living = "psiren_omen" + icon_dead = "psiren_omen_dead" + health = 120 + maxhealth = 120 + ranged = TRUE + projectilesound = 'sound/weapons/wave.ogg' + projectiletype = /obj/projectile/bullet/pistol/psiren/omen + var/has_ink = TRUE + +/mob/living/simple_animal/hostile/psiren/omen/bullet_act(obj/projectile/hitting_projectile, def_zone, piercing_hit) + . = ..() + if (. == BULLET_ACT_BLOCK || !has_ink) + return + + var/datum/effect/effect/system/smoke_spread/psiren/S = new/datum/effect/effect/system/smoke_spread/psiren() + S.set_up(5, 0, loc, null) + S.start() + has_ink = FALSE diff --git a/code/modules/mob/living/simple_animal/hostile/psirens/psiren_projectiles.dm b/code/modules/mob/living/simple_animal/hostile/psirens/psiren_projectiles.dm new file mode 100644 index 00000000000..e83efd57455 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/psirens/psiren_projectiles.dm @@ -0,0 +1,62 @@ +/** + * Special bullet type used for Psiren Darters + * Very low damage, but very high penetration. Resisted (or strengthened) by psi-sensitivity. + */ +/obj/projectile/bullet/pistol/psiren + name = "kinesis bolt" + damage = 4 + armor_penetration = 40 + sharp = 1 + embed = FALSE + damage_flags = DAMAGE_FLAG_BULLET | DAMAGE_FLAG_PSIONIC + pass_flags = PASSTABLE | PASSGLASS | PASSGRILLE | PASSRAILING + muzzle_type = /obj/effect/projectile/muzzle/laser/psiren + tracer_type = /obj/effect/projectile/tracer/laser/psiren + impact_type = /obj/effect/projectile/impact/laser/psiren + icon_state = "purple_laser" + + /// How much damage this attack deals directly to the victim's morale on a successful hit. + var/psi_damage = -0.1 + + /// How much extra psychic damage is dealt per point of psi-sensitivity. + var/psi_sensitivity_multiplier = -0.05 + + /// The chance per successful hit of triggering an ADPI message on the target. + var/adpi_chance = 0 + +/** + * Variant psiren bullet type for Omens. + * Almost no physical damage at all, but very high psychic damage. + */ +/obj/projectile/bullet/pistol/psiren/omen + damage = 1 + armor_penetration = 60 + psi_damage = -5.0 + psi_sensitivity_multiplier = -2.5 + adpi_chance = 5 + +/** + * Psiren ranged attacks deal a unique form of "psychic damage" which is governed by the full suite of rules regarding Psionic Sensitivity. + * This is in addition to their physical "Telekinetic" damage component. Being immune to the psychic damage does not make one immune to the physical damage. + * One can be immune to the psychic damage for a very large variety of reasons, not all of them static. + * + * "Psychic damage" attacks a character's Morale statistic rather than physical health, which inflicts gradually stacking penalties to Skill Tests. + */ +/obj/projectile/bullet/pistol/psiren/on_hit(atom/target, blocked, def_zone) + . = ..() + if (. != BULLET_ACT_HIT || !firer) + return + + try_deal_psychic_damage(target, firer, psi_damage, psi_sensitivity_multiplier, adpi_chance) + +/obj/effect/projectile/muzzle/laser/psiren + icon_state = "muzzle_scc" // I just like the pattern here + light_color = COLOR_VIOLET + +/obj/effect/projectile/tracer/laser/psiren + icon_state = "beam_scc" + light_color = COLOR_VIOLET + +/obj/effect/projectile/impact/laser/psiren + icon_state = "impact_scc" // I also just like the pattern here + light_color = COLOR_VIOLET diff --git a/code/modules/mob/living/simple_animal/hostile/psirens/psiren_psi_damage.dm b/code/modules/mob/living/simple_animal/hostile/psirens/psiren_psi_damage.dm new file mode 100644 index 00000000000..2e766116b20 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/psirens/psiren_psi_damage.dm @@ -0,0 +1,106 @@ +/** + * All of the moodlet functionality required for the psychic damage effect, + * is contained within this file. + */ +/datum/moodlet/psiren_psi_damage + // Descriptor and Initial logic is bespoke for this moodlet. + moodlet_descriptor = "residual psionic pressure." + initial_descriptor = null + duration = 15.0 MINUTES + var/list/cursed_messages = list("DRINK DEEPLY OF THE FINAL NIGHT.", + "YOUR LOVED ONES WILL NEVER KNOW OF YOUR JOY AGAIN.", + "LEAVE THIS PLACE THROUGH THE NEAREST AIRLOCK.", + "DEATH IS BUT THE FIRST DANCE IN ETERNITY.", + "YOU DESERVE ALL OF YOUR SUFFERING.", + "THE STARS HAVE FORGOTTEN YOUR NAME.", + "YOUR BONES REMEMBER CRIMES YOU NEVER COMMITTED.", + "EVERY GOD YOU HAVE EVER PRAYED TO IS DEAD.", + "THE UNIVERSE REGRETS YOUR CREATION.", + "YOU HAVE ALREADY FAILED.", + "YOU WILL NEVER WAKE UP FROM THIS.", + "YOUR SCREAMS ARRIVE BEFORE YOU MAKE THEM.", + "YOU WERE WARNED.", + "THE RIVERS RUN COLD WITH MEMORY.", + "THE LEMURIAN SEA HUNGERS.", + "THE LEMURIAN SEA HAS BECOME YOUR TOMB.", + "THERE IS A DRUMBEAT BELOW REALITY.", + "THE SUNSET LASTS FOREVER HERE.", + "YOU CANNOT HIDE INSIDE YOUR OWN HEAD.", + "I KNOW WHAT YOU DREAM ABOUT.", + "I AM WEARING YOUR VOICE.", + "YOU INVITED ME IN", + "THE SCC SENT YOU HERE AS A FEAST FOR GIBBERING MOUTHS. YOUR LEADERS SENT YOU HERE TO DIE.", + "YOUR MEMORY FURNISHES A REPAST FOR THE MATRIARCH.", + "THE NIGHT SHEDS A TEAR TO TELL YOU OF FEAR, OF SORROW, OF PAIN.", + "THE SCC HAS ALREADY VOIDED YOUR CONTRACT, THEY KNEW YOU WOULD DIE HERE.", + "I SAW YOU WHEN YOU THOUGHT YOU WERE ALONE.", + "THE BLOOD WILL NEVER WASH FROM YOUR HANDS.", + "YOU ARE GUILTY OF YOUR FOREFATHERS SINS.", + "YOUR MISSION FAILED BEFORE IT BEGAN, THE PHORON YOU SEEK WAS NEVER REAL. YOUR NATION WILL DROWN FOR ITS WANT. IT'S ALL YOUR FAULT.") + +/// Unique psi-damage variant used by the beckoner +/datum/moodlet/psiren_psi_damage/beckoner + duration = 10.0 MINUTES + cursed_messages = list( + "Please help me, I'm trapped outside the airlocks!", + "You feel drawn toward the nearest airlock.", + "Fresh air... no, vacuum... feels right somehow.", + "The void feels strangely comforting.", + "Please come save me, I'm in a lifepod just outside!", + "The ship feels unbearably cramped.", + "You need fresh air, go for a walk outside.", + "You don't need a space suit.", + "Did someone just knock on the hull?", + "I left something outside. I should go get it.", + "Come outside.", + "The nearest airlock suddenly seems very important.", + "Someone is calling for help outside!") + +/datum/moodlet/psiren_psi_damage/get_moodlet_descriptor() + // This SHOULD exist but I'm doing my due diligence in checking. + var/mob/grandparent = astype(astype(morale_component?.resolve(), MORALE_COMPONENT)?.parent, /mob) + if (!grandparent) + return // No message, nobody to send a message to. + + // Having any psi-protection will let you see the underlying moodlet. + if (grandparent.is_psi_blocked(null, FALSE)) + return ..() + + var/spooky_message = pick(cursed_messages) + if (prob(10)) + grandparent.play_screen_text(spooky_message, /atom/movable/screen/text/screen_text/adpi_message/psiren_spam, COLOR_PURPLE) + + return FONT_HUGE(SPAN_CULT(spooky_message)) + +/datum/moodlet/psiren_psi_damage/send_initial_description(datum/owner) + astype(astype(morale_component?.resolve(), MORALE_COMPONENT)?.parent, /mob)?.play_screen_text(pick(cursed_messages), /atom/movable/screen/text/screen_text/adpi_message/psiren_spam, COLOR_PURPLE) + return + +/proc/try_deal_psychic_damage(mob/living/victim, mob/attacker, psi_damage = -0.1, psi_sensitivity_multiplier = -0.05, adpi_chance = 0, moodlet_type = /datum/moodlet/psiren_psi_damage, message_override) + // Immunity from telepathy will fully negate the psychic damage (but not physical damage) component. + // This is getting checked before the morale_comp because I want to give Active Psi Protection a chance to give the player feedback that a psychic effect occurred. + if (!istype(victim) || victim.stat == DEAD || victim.is_psi_blocked(attacker, FALSE)) + return FALSE + + // A morale component is also required for psychic damage. + var/datum/component/morale/morale_comp = victim.GetComponent(MORALE_COMPONENT) + if (!morale_comp) + return FALSE + + var/victim_psi_sensitivity = victim.check_psi_sensitivity() + var/datum/moodlet/psi_damage_moodlet = morale_comp.load_moodlet(moodlet_type, FALSE) + psi_damage_moodlet.set_moodlet(psi_damage_moodlet.get_morale_modifier() + psi_damage + min(0, (victim_psi_sensitivity * psi_sensitivity_multiplier))) + psi_damage_moodlet.refresh_moodlet() + + if (!prob(adpi_chance) || !SShallucinations) + return FALSE + + // At this point in time all checks that would be satisfied by the ADPI subsystem have already been handled, + // now we just need to fetch a message and send one. + var/adpi_message = message_override ? message_override : SShallucinations.pick_adpi_message(victim, TRUE) + if (!adpi_message) + return FALSE + + victim.play_screen_text("[adpi_message]", /atom/movable/screen/text/screen_text/adpi_message/psiren_spam, COLOR_PURPLE) + to_chat(victim, SPAN_CULT(FONT_LARGE("[adpi_message]"))) + return TRUE diff --git a/html/changelogs/hellfirejag-psiren-update.yml b/html/changelogs/hellfirejag-psiren-update.yml new file mode 100644 index 00000000000..6be2f443ccf --- /dev/null +++ b/html/changelogs/hellfirejag-psiren-update.yml @@ -0,0 +1,10 @@ +author: Hellfirejag +delete-after: True +changes: + - rscadd: "Psiren Darters, Omens, and Beckoners can now shoot through windows, while dealing less damage overall." + - rscadd: "Psiren attacks are now less likely to cause bleeds." + - rscadd: "Psiren variants now have specific names." + - rscadd: "Psychic damage messages now print to random locations on the screen." + - rscadd: "Added a new psiren subtype, the Beckoner." + - rscadd: "Psiren Beckoners can now appear in wandering psiren events" + - rscadd: "Technomancers can now summon Psiren Beckoners" diff --git a/icons/mob/npc/psiren_beckoner.dmi b/icons/mob/npc/psiren_beckoner.dmi new file mode 100644 index 00000000000..585578d7e2d Binary files /dev/null and b/icons/mob/npc/psiren_beckoner.dmi differ